edf2csv 0.8.81 → 0.8.83

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.
@@ -268,6 +268,34 @@ toStdout = false) {
268
268
  // carries no fraction anyway, and is byte-for-byte `toFixed` everywhere else.
269
269
  lines.push(`Timed from ${fixed(startsAt, 3)}s (first sample; --start and --end use this clock)`);
270
270
  }
271
+ /*
272
+ And the window, when one was asked for, which this report did not mention at all.
273
+
274
+ `--info` is the mode whose purpose is to say what a conversion will do, and every other
275
+ flag that changes what gets written is visible in it: `--channels` puts `(not selected)`
276
+ in the OUTPUT column, `--gzip` changes the names there, `--layout long` changes the
277
+ sentence under the table, `--annotations-only` replaces the estimate outright. `--start`
278
+ and `--end` changed one number:
279
+
280
+ $ edf2csv rec.edf --info $ edf2csv rec.edf --info --start 1s --end 2s
281
+ Duration 3s (3 records of 1s) Duration 3s (3 records of 1s)
282
+ ... ...
283
+ Would write 1,155 rows, roughly 22.2 KB. Would write 385 rows, roughly 7.4 KB.
284
+
285
+ Byte-for-byte identical otherwise — and the Duration line, which is what a reader checks a
286
+ window against, goes on describing the whole file above an estimate that describes a third
287
+ of it. The window is in `plan.range` already, which is the same thing `Timed from` above
288
+ was added for: it governs the estimate printed below and was simply never shown.
289
+
290
+ In seconds rather than through `formatDuration`, for the reason that line gives: these are
291
+ numbers meant to be typed back into `--start` and `--end`, and "16m 40s" is not one.
292
+ */
293
+ const { range } = plan;
294
+ if (!range.isWholeRecording && Number.isFinite(range.startSeconds) && Number.isFinite(range.endSeconds)) {
295
+ const records = range.endRecord - range.startRecord;
296
+ lines.push(`Window ${fixed(range.startSeconds, 3)}s to ${fixed(range.endSeconds, 3)}s ` +
297
+ `(${grouped(records)} of ${counted(file.recordCount, 'data record')})`);
298
+ }
271
299
  lines.push(`Size ${formatBytes(file.fileSize)}`);
272
300
  if (header.patientId)
273
301
  lines.push(`Patient ${printable(header.patientId)}`);
@@ -498,6 +526,19 @@ toStdout = false) {
498
526
  // zero; not when the first record's timekeeping TAL puts the recording elsewhere. Both
499
527
  // of the fields above are lengths and neither says where that length sits.
500
528
  first_sample_seconds: plan.range.recordingStartSeconds,
529
+ /*
530
+ The window, under the names `metadata.json` gives it, which is the rule this document
531
+ follows for everything describing the run.
532
+
533
+ The text form did not show it either until 0.8.82; here the only trace of `--start`
534
+ and `--end` was `estimate.rows` coming back smaller, with nothing in the document
535
+ saying why. A survey that runs `--info --json` over a folder with a window on the
536
+ command line archives a description of each recording in which the window does not
537
+ appear.
538
+ */
539
+ start_seconds: plan.range.startSeconds,
540
+ end_seconds: plan.range.endSeconds,
541
+ whole_recording: plan.range.isWholeRecording,
501
542
  annotation_channels: file.annotationSignals.length,
502
543
  // Named as `summaryJson` names the same count, so a prediction and a conversion read
503
544
  // the same field.
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Human-readable output for the terminal.\n *\n * Everything here is plain text with no colour codes, so piping to a file or a log\n * produces exactly what appeared on screen.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport { fixed, formatBytes, formatDuration, plain } from '../format/number.js';\nimport { counted, grouped } from '../format/list.js';\nimport { escapeCharacter, escapeJsonText, unprintablePattern } from '../format/unprintable.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { withoutFileRateWarning } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\nimport { VERSION } from '../version.js';\n\n/** Where terminal prose wraps. The width --help is written to, and the ANSI default. */\nconst WRAP_COLUMNS = 80;\n\n/** The continuation indent under a `warning: ` prefix, which is the only one there is. */\nconst HINT_INDENT = ' '.repeat(9);\n\n/**\n * One unbreakable unit of a wrapped line: a quoted span, or a run of non-space.\n *\n * The lookbehind is what keeps an apostrophe out of it. `the file's own header and it's` has\n * two of them, and without the guard `'s own header and it'` is one quoted span — a token\n * with spaces in it that then refuses to break. A quote opening a span is at the start of a\n * word, so that is what is required of it.\n */\nconst WORD = /(?<=^|[\\s(])(?:\"[^\"]*\"|'[^']*')|\\S+/gu;\n\n/**\n * Greedy word wrap, `indent` on every line including the first.\n *\n * Only free prose goes through this. The aligned parts of `--info` — the `Format`/`Size`\n * key-value lines and the channel table — are laid out in columns, and re-flowing a column\n * is how you turn a table into a paragraph.\n *\n * A word wider than the column is left to overrun rather than broken. The long words here\n * are file paths and quoted channel labels, and neither survives being split across lines:\n * the point of printing a path is that it can be copied back out.\n *\n * A quoted span is one word for the same reason, however many spaces are inside it. The\n * sentence above is the promise, and it held only for the paths that have no spaces in them:\n * an interrupted conversion said `Files already written to \"/tmp/a very long destination\n * folder name with` / `many spaces here indeed\" are incomplete`, splitting the one thing on\n * the line that has to be copied whole — and quoting is what a path with spaces in it is\n * given in the first place. A word wider than the column already overruns rather than\n * breaking; this is that rule reaching the words it was written for.\n *\n * Which is also why the gap between two words is reproduced rather than normalised to one\n * space. Splitting on `\\s+` and rejoining with `' '` re-flowed everything, and a quoted value\n * is not prose: a channel labelled `EEG A` was offered back as `Did you mean \"EEG A\"?`, and\n * a destination given as `-my nightly` as `Write it as one argument instead:\n * '--out=-my nightly'`. Following either gets a different channel or a different directory\n * than the one the sentence is about, and the run before it had already quoted the name\n * correctly on the line above.\n */\nexport function wrap(text: string, indent = '', width = WRAP_COLUMNS): string {\n const lines: string[] = [];\n let line = indent;\n let at = 0;\n for (const match of text.matchAll(WORD)) {\n const piece = match[0];\n const gap = text.slice(at, match.index);\n at = match.index + piece.length;\n if (line === indent) line += piece;\n else if (line.length + gap.length + piece.length <= width) line += gap + piece;\n else {\n lines.push(line);\n line = indent + piece;\n }\n }\n if (line !== indent) lines.push(line);\n return lines.join('\\n');\n}\n\nfunction table(rows: readonly (readonly string[])[], alignRight: ReadonlySet<number>): string {\n if (rows.length === 0) return '';\n const width: number[] = [];\n for (const row of rows) {\n row.forEach((cell, i) => {\n width[i] = Math.max(width[i] ?? 0, cell.length);\n });\n }\n return rows\n .map((row) =>\n row\n .map((cell, i) => {\n const w = width[i] ?? 0;\n return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);\n })\n .join(' ')\n .trimEnd(),\n )\n .join('\\n');\n}\n\n/**\n * Make header text safe to print to a terminal.\n *\n * EDF identification fields and channel labels are free text copied verbatim out of the\n * file, and `--info` puts them straight on stdout. A header carrying ANSI escapes could\n * therefore drive the reader's terminal — `\\x1b[2J\\x1b[H` clears the screen and homes the\n * cursor, which is enough to hide the rest of the output or repaint it as something else.\n * Nobody writes an EDF header that way on purpose, which is exactly why a file that does\n * should not be trusted with the terminal.\n *\n * Control bytes are shown as their escape instead, so a corrupt field stays diagnosable\n * rather than being silently swallowed. This affects display only: `channels.csv` and\n * `metadata.json` still copy the field verbatim, and CSV quoting already makes that safe.\n */\nexport function printable(text: string): string {\n // The character class and the escape are shared with the conversion, which asks the same\n // question of an annotation description; see src/format/unprintable.ts.\n return text.replace(unprintablePattern('gu'), escapeCharacter);\n}\n\n/**\n * The same protection for text that is meant to span lines.\n *\n * `printable` escapes newlines along with everything else, which is right for a channel\n * label — one has no business containing a line break, and it would break the `--info`\n * table's alignment. It is wrong for a whole message: several are written on two lines,\n * and Node's own option errors run to three. Escaping those turned the break into text:\n *\n * error: No channel named \"ECQ\". Did you mean \"ECG\"?\\x0aRun with --info to list ...\n *\n * Each line is escaped on its own, so nothing here gains the ability to drive a terminal.\n * A carriage return is still escaped, so no line can be repainted after it is printed —\n * which is the property that mattered. A newline can only add a line, never overwrite one.\n */\nexport function printableLines(text: string, indent = ''): string {\n return text\n .split('\\n')\n .map((line, index) => {\n const safe = printable(line);\n // The first line is the one that follows `error: ` or `warning: `, so it is the line\n // a log gets grepped for, and it stays whole at whatever width it runs to.\n if (index === 0) return safe;\n\n /*\n Callers write the continuation indent one of two ways: most pass it here and leave\n their message lines flush, but the two usage builders bake ` ` into the string\n itself because they are printed without an `error: ` prefix in front. Reading the\n line's own leading space when none was passed keeps both working, and keeps the\n wrap aligned under the same column either way.\n */\n const body = safe.trimStart();\n const pad = indent === '' ? safe.slice(0, safe.length - body.length) : indent;\n\n /*\n A continuation that is a command is copied, not read.\n\n `edf2csv -- \"--chanels\"` is the line the unknown-option error ends on, and it exists\n to be pasted back into the shell. Wrapping puts `edf2csv --` on one line and the\n flag on the next, and what gets pasted is half a command. Prose survives being\n re-flowed and a command does not, so the two are told apart rather than being\n treated alike and hoping today's strings stay short.\n */\n if (body.startsWith('edf2csv ')) return pad + body;\n return wrap(body, pad);\n })\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * How many events a conversion would write, when that is already known.\n *\n * Null when it is not. `--info` reads the whole annotation channel of a discontinuous file,\n * because that is where its record times are; a continuous one it reads only far enough to\n * find the origin. So the count is in hand for one of the two, and the line below said \"How\n * many events there are cannot be told from the header\" for both — true of the header, and\n * beside the point on a file whose events had just been read and counted.\n */\n events: number | null = null,\n /**\n * Whether the run would stream to stdout, in which case no file is written at all.\n *\n * The OUTPUT column is \"Named as they will be written. --info is read to find out what a\n * run leaves behind, and a script that opens the name it was given must find a file there\"\n * — and under `--stdout` it named `signals.csv`, or `signals.csv.gz`, for a run that\n * creates no directory and no file. The same table already tells a channel that will not\n * be converted from one the file gives nothing to convert; this is the third thing it\n * could not say.\n */\n toStdout = false,\n): string {\n const { header } = file;\n const lines: string[] = [];\n\n /*\n Escaped, like every other value that came out of the filesystem.\n\n A path is untrusted text: a folder may be named with an ESC byte, and a file name may\n hold a newline on every platform this runs on. The `[n/m]` header a batch prints has\n always escaped it and these two lines did not, so one line of a run reached the terminal\n as `study/esc\\x1b[31mred.edf` and the next as a live colour change — and a name holding a\n newline split `Wrote` across two lines, so the summary reported a path that reads as two.\n NONPRINTABLE_LABEL exists because a header field can carry these bytes; a directory entry\n can carry them just as easily.\n */\n lines.push(`File ${printable(file.path)}`);\n lines.push(`Format ${describeFormat(header)}`);\n /*\n The two raw fields, with edges.\n\n Echoed bare, they ran into each other and into the parenthetical, and a header field is\n free to be empty or to be nothing but padding — `trimField` takes the padding off, so\n both of those arrive here as \"\". A file whose date field is blank printed\n\n Recorded 22.15.00 (unparseable)\n\n where the one value on the line is the *time* and nothing says so, and a file with both\n blank printed `Recorded (unparseable)`: a label, a gap, and a verdict about nothing\n visible. ` . . ` in both fields came out `. . . .`, which is four fields or two\n depending on how the reader counts.\n\n Quoted, which is what `START_TIME_UNREADABLE` — the warning printed under this very line,\n about these very fields — has always done: `(\"\" and \"\")`. Same argument 0.7.x made for the\n time-range refusals, in its words: without them \"the value ran into the sentence ... and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible\". `--info --json` has the two as separate strings and never had the problem.\n */\n lines.push(\n `Recorded ${\n formatWallClock(header.startDateTime)?.replace('T', ' ') ??\n `\"${printable(header.startDateRaw)}\" \"${printable(header.startTimeRaw)}\" (unparseable)`\n }`,\n );\n lines.push(\n `Duration ${formatDuration(file.durationSeconds)} (${counted(file.recordCount, 'record')} of ${plain(header.recordDuration)}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n /*\n Which way the two differ decides what to call it, and the parenthetical used to say\n \"includes discontinuities\" both ways round.\n\n A span LONGER than the duration is the gap case this line was written for: 3 records of\n 1s covering 11 seconds. A span SHORTER than the duration cannot be a gap — it is records\n that overlap, which an EDF+D file gets when a device re-sends a buffer. Three records of\n 1s starting at 0, 0.5 and 1 print:\n\n Duration 3s (3 records of 1s)\n Time span 2s (includes discontinuities)\n\n A recording covering less time than its own records account for, blamed on gaps it does\n not have — while the warning below it says, correctly, that two records overlap.\n\n A file holding both is described by whichever wins the subtraction, and the overlap\n warning is printed either way.\n */\n const overlapping = elapsedSpan < file.durationSeconds;\n lines.push(\n `Time span ${formatDuration(elapsedSpan)} ` +\n `(${overlapping ? 'records overlap in time' : 'includes discontinuities'})`,\n );\n }\n /*\n Where the samples begin, when that is not zero.\n\n 0.4.9 made the first record's timekeeping TAL the point a recording is timed from, so a\n file whose TALs start at +1000 writes `time_s` from 1000.000 and takes `--start` and\n `--end` on that same clock. None of that appeared here: the report said \"Duration 3s\",\n which reads as 0 to 3, and `--start 0 --end 1` then selected nothing and answered with\n \"The window is inside the recording but lands where there is no data ... Run with --info\n to see where the records actually sit\" — pointing at this report, which was the one place\n the number was missing. It is in `plan.range` already and governs the estimate printed\n below; it was simply never shown.\n */\n const startsAt = plan.range.recordingStartSeconds;\n if (Number.isFinite(startsAt) && Math.abs(startsAt) > 1e-9) {\n // In seconds rather than through formatDuration, because this number is meant to be\n // typed back in: `--start` takes `1000s`, and \"16m 40s\" is not something it accepts.\n // It is also how the empty-window warning renders the window it was given.\n //\n // Through `fixed` rather than `toFixed`, which switches to exponent notation at 1e21 —\n // and `--start 1e+21s` is refused by the time parser with \"uses an unknown unit \\\"e\\\"\",\n // so the one line that says which clock to use handed back a number that clock rejects.\n // Reachable from a conforming file: an EDF+ onset is plain digits of any length, and a\n // record duration large enough to keep samples apart at that magnitude is four\n // characters. `fixed` expands these with BigInt, which is exact past 2^53 where a double\n // carries no fraction anyway, and is byte-for-byte `toFixed` everywhere else.\n lines.push(\n `Timed from ${fixed(startsAt, 3)}s (first sample; --start and --end use this clock)`,\n );\n }\n lines.push(`Size ${formatBytes(file.fileSize)}`);\n if (header.patientId) lines.push(`Patient ${printable(header.patientId)}`);\n if (header.recordingId) lines.push(`Recording ${printable(header.recordingId)}`);\n\n const signals = file.dataSignals;\n lines.push('');\n /*\n Both counts through `counted`, which this function already uses one line up for the record\n count and which exists so a count and its noun cannot disagree.\n\n The annotation-channel half was pluralised by hand when it was fixed — a file carrying two\n of them read \"2 annotation channel\", and EDF+ permits more than one — and the signal half\n had been hand-rolled since it was written. Two spellings of one rule in one line, in the\n file that imports the rule.\n */\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0 ? ` + ${counted(annotationCount, 'annotation channel')}` : '';\n lines.push(`Channels ${counted(signals.length, 'signal')}${annotationPart}`);\n lines.push('');\n\n const rows: string[][] = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n // Rendered as a group so that two channels recorded at different rates never show the\n // same figure in the RATE column, which is the one thing this table is asked to settle.\n const rateText = formatRates(signals.map((signal) => signal.samplingRate));\n for (const [row, signal] of signals.entries()) {\n rows.push([\n String(signal.index),\n printable(plan.columnNames.get(signal.index) ?? ''),\n printable(signal.label),\n printable(signal.physicalDimension),\n `${rateText[row]} Hz`,\n `${signal.physicalMin} to ${signal.physicalMax}`,\n /*\n A channel with no samples was reported as \"(not selected)\", which is a different\n thing and not true when it was named on --channels. `edf2csv rec.edf --info\n --channels unused` said the channel the command asked for had not been chosen, when\n what is actually the case is that the file gives it nothing to convert. The\n NO_SAMPLES warning below the table says so; the table contradicted it.\n */\n /*\n And under --annotations-only nothing was deselected either — the run writes no signal\n table at all, which is the same distinction one paragraph up.\n\n `--info --annotations-only --channels \"EEG Fpz-Cz\"` accepted the name, checked it\n against the file, and then printed \"(not selected)\" against the very channel it had\n just been asked for. The sentence three lines under the table already says the truth,\n \"and no signal data\", and `channels.csv` and `--info --json` both say it too — the\n cell is empty and `output_file` is null. The human table was the one place claiming a\n choice nobody made.\n */\n plan.writeSignals\n ? fileFor.has(signal.index)\n ? toStdout\n ? '(stdout)'\n : (fileFor.get(signal.index) as string)\n : signal.samplesPerRecord === 0\n ? '(no samples)'\n : '(not selected)'\n : '(no signal data)',\n ]);\n }\n /*\n A column head over nothing is not a table.\n\n The line above has just said `Channels 0 signals + 1 annotation channel`, and this then\n printed\n\n # COLUMN LABEL UNIT RATE RANGE OUTPUT\n\n with no row under it — seven headings for a set of no channels, on a recording holding\n only events, which is what `annotations-only.edf` in this repository is and what a\n companion event file out of a scoring system is. The same sentence `--channels \"#0\"` was\n given its own wording for: \"No channel at position #0. This file has signal channels at\n .\" states there are some and then names none.\n\n Nothing goes in its place. The count is stated one line up and the body three lines down\n says what the conversion would write; a table saying \"none\" twice more adds nothing.\n */\n if (signals.length > 0) {\n lines.push(table(rows, new Set([0])));\n lines.push('');\n }\n /*\n From here down --info stops laying out columns and starts explaining itself, so from\n here down it wraps. The key-value lines and the channel table above are aligned to each\n other and must not be re-flowed; these are sentences, and the longest of them ran to 156\n columns — which is not a line anyone reads, it is a line a terminal breaks somewhere.\n --help has been written to 80 since it existed and hints joined it in 0.7.1; --info is\n the mode whose whole purpose is being read by a person, and it was the last one guessing.\n */\n if (plan.groups.length > 1) {\n /*\n And the third thing this sentence could not say, after the OUTPUT column above it.\n\n `--stdout` on a recording with more than one rate is refused — it writes one table and\n this makes several — so no file is written and the run does not happen. The sentence\n described it anyway, three lines under a column saying `(stdout)` and six above the\n warning that the run would be refused:\n\n 0 EEG Fpz-Cz ... 256 Hz -250 to 250 (stdout)\n Sampling rates differ, so channels are written to 3 files, one per rate.\n warning: --stdout would refuse this run: needs exactly one table, but this\n recording produces 3 ...\n\n Three statements about one run, and the middle one names an outcome none of the others\n allows. What the rates decide is how many tables there are; where those go is the\n question this mode has already answered differently.\n */\n lines.push(\n wrap(\n plan.layout === 'long'\n ? `Sampling rates differ, and the long layout puts them in one table anyway: each row ` +\n `carries its own time, so nothing has to line up. No channel is resampled.`\n : toStdout\n ? `Sampling rates differ, so this recording makes ${counted(plan.groups.length, 'table')}, ` +\n `one per rate — more than --stdout can write. Converting into a directory writes one ` +\n `file each; --layout long puts them all in one table.`\n : `Sampling rates differ, so channels are written to ${counted(plan.groups.length, 'file')}, one per rate. ` +\n `No channel is resampled.`,\n ),\n );\n }\n /*\n The estimate describes the signal tables, and says so when that is not what will be\n written.\n\n Under --annotations-only there are no signal tables, and the line read \"Would write 0\n rows, roughly 0 B.\" for a conversion that goes on to write annotations.csv with three\n events in it. --info exists to say what a conversion will do; asserting it will write\n nothing, when it will write a file, is the one thing it must not do.\n\n How many events there are cannot be answered from the header — the annotation channel has\n to be read record by record, which is the scan --info is for avoiding. So it says which\n file, and that the count is not knowable this cheaply, rather than inventing a zero.\n */\n /*\n No signal table to describe, whichever way that came about.\n\n This asked only whether `--annotations-only` had been given. A recording that has no\n signal channels — one holding nothing but EDF+ annotations — has none either, and fell\n through to the estimate line: \"Would write 0 rows, roughly 0 B.\" for a conversion that\n goes on to write an annotations.csv with events in it, beside channels.csv and\n metadata.json. That is the sentence 0.4.51 removed, arriving by the other route.\n */\n if (!plan.writeSignals || plan.groups.length === 0) {\n // Named as they will be written. --info is read to find out what a run leaves behind,\n // and a script that opens the name it was given must find a file there.\n const suffix = plan.gzip ? '.csv.gz' : '.csv';\n lines.push(\n wrap(\n file.annotationSignals.length > 0\n ? events === null\n ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +\n 'many events there are cannot be told from the header, and finding out means ' +\n 'reading the annotation channel record by record.'\n : `Would write annotations${suffix} with ${counted(events, 'event')} and ` +\n `channels${suffix}, and no signal data.`\n : `Would write channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel.',\n ),\n );\n return lines.join('\\n');\n }\n\n // The estimate counts the characters of the CSV, which is what --gzip then compresses.\n // Reporting it as the size on disk would overstate a compressed conversion several-fold.\n const compressing = plan.gzip;\n lines.push(\n wrap(\n // A window narrow enough to select one sample is an ordinary thing to ask for, and this\n // read \"Would write 1 rows, roughly 22 B.\" — the slip 0.5.74 fixed on the lines above it\n // and missed here, because the recording that test builds never estimates exactly one.\n `Would write ${counted(plan.estimate.rows, 'row')}, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}.`,\n ),\n );\n\n return lines.join('\\n');\n}\n\n/**\n * The `--info` view as JSON, for surveying files from a script.\n *\n * `indent` is 2 for a single recording, matching what this has always printed, and null for\n * a batch — several pretty-printed documents run together are readable by a streaming parser\n * but not by anything that expects one record per line, and a batch is exactly where\n * line-oriented reading is wanted. null rather than undefined because a default parameter\n * takes effect when undefined is passed, which quietly restored the indentation this was\n * meant to drop; JSON.stringify itself wants undefined, so it is translated at the call.\n *\n * `--info` answers \"what is in this recording and what would converting it cost\", which\n * is exactly the question you want to ask across a directory of hundreds of recordings —\n * and the text table is the wrong shape for that. `--json` previously applied only to\n * conversions, so scripts had to parse the aligned columns or convert files just to learn\n * what was in them.\n *\n * Field names match `metadata.json` where the two describe the same thing, so a survey and\n * a conversion can be read by the same code.\n */\nexport function infoJson(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * The event count, on the files where `--info` has already read and counted them.\n *\n * `formatInfo` has taken this since 0.7.x and prints it — \"Would write annotations.csv with\n * 3 events\" — and the same number was dropped on the way to the JSON, which is the surface\n * a script reads and the reason `--info --json` exists. Null where the count is not in\n * hand, by the same rule `estimate` is null for a run that writes no signal table: a\n * continuous file is read only as far as its origin, so its events have not been counted\n * and there is no honest number to put here.\n */\n events: number | null = null,\n indent: number | null = 2,\n /** See `formatInfo`: under `--stdout` there is no file to name. */\n toStdout = false,\n): string {\n const { header } = file;\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n const document = JSON.stringify(\n {\n tool: TOOL,\n path: file.path,\n bytes: file.fileSize,\n format: describeFormat(header),\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n // For a discontinuous file this exceeds duration_seconds by the length of the gaps.\n time_span_seconds: plan.range.recordingEndSeconds - plan.range.recordingStartSeconds,\n // Where `time_s` begins, and the clock `--start` and `--end` are read against. Usually\n // zero; not when the first record's timekeeping TAL puts the recording elsewhere. Both\n // of the fields above are lengths and neither says where that length sits.\n first_sample_seconds: plan.range.recordingStartSeconds,\n annotation_channels: file.annotationSignals.length,\n // Named as `summaryJson` names the same count, so a prediction and a conversion read\n // the same field.\n annotations: events,\n channels: file.dataSignals.map((signal) => ({\n signal_index: signal.index,\n column: plan.columnNames.get(signal.index) ?? '',\n label: signal.label,\n unit: signal.physicalDimension,\n sampling_rate_hz: signal.samplingRate,\n samples_per_record: signal.samplesPerRecord,\n physical_min: signal.physicalMin,\n physical_max: signal.physicalMax,\n digital_min: signal.digitalMin,\n digital_max: signal.digitalMax,\n transducer: signal.transducer,\n prefiltering: signal.prefiltering,\n /*\n `-`, the placeholder `ConvertResult.outputDir` already uses for this mode, and not\n `null`: null is this document's word for \"would not be converted\", which is a\n different thing from \"converted, to a stream\".\n */\n output_file: fileFor.has(signal.index)\n ? toStdout\n ? '-'\n : (fileFor.get(signal.index) as string)\n : null,\n })),\n /*\n Null rather than zero when the run writes no signal table.\n\n The text form has refused to say \"Would write 0 rows, roughly 0 B.\" since 0.4.51,\n because a run that goes on to write an annotations.csv with events in it has not\n written nothing — and it is `--annotations-only`, or a recording holding only\n annotations, that reaches this. The JSON went on saying it to the surface a script\n reads. There is no estimate for a table that does not exist, and null is how this\n document already says that.\n */\n estimate:\n plan.writeSignals && plan.groups.length > 0\n ? {\n rows: plan.estimate.rows,\n // Character count of the CSV. With --gzip the file on disk is smaller than this.\n bytes: plan.estimate.bytes,\n exceeds_spreadsheet_limit: plan.estimate.exceedsSpreadsheetLimit,\n }\n : { rows: null, bytes: null, exceeds_spreadsheet_limit: false },\n // The plan's mixed-rate warning replaces the header parser's, as it does everywhere\n // else. This was the one consumer left out of that when 0.3.2 made the warning follow\n // --channels, so `--info --json` carried it twice: once counting the rates being\n // converted and once counting every rate in the file, with the same code and severity.\n // The file's own first, then the plan's, which is the order the text form prints them\n // in. Concatenating the other way round listed the same warnings about the same\n // recording in two different sequences depending on which form you asked for.\n warnings: withoutFileRateWarning(file.diagnostics)\n .concat(plan.diagnostics)\n .map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n\n/**\n * One diagnostic per `warning: ` line, prefixed so warnings are greppable; the hint below it\n * wrapped to the terminal.\n *\n * The hint has been on its own unprefixed continuation line since these gained hints at all,\n * so grepping for `warning:` never picked it up and wrapping it costs nothing that was being\n * relied on — which is what 0.6.132 got wrong when it left every diagnostic long on the\n * grounds that they are one line each. Half of that is true. The `warning:` head is a line\n * per diagnostic and stays one, at whatever width the message runs to; the hint underneath\n * it is prose addressed to a person reading a terminal, and 17 of them ran past 80 columns,\n * the widest to 180. At that width the second half of the advice is wherever the terminal\n * decided to put it, indented under nothing, and the 9-space rule that says \"this belongs to\n * the warning above\" is lost at exactly the moment there is enough text for it to matter.\n */\nexport function formatDiagnostics(diagnostics: readonly Diagnostic[]): string {\n return diagnostics\n .map((d) => {\n // Diagnostics quote channel labels, which come from the file, so they need the\n // same treatment as the --info table.\n const head = `${d.severity}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n${wrap(printable(d.hint), HINT_INDENT)}` : head;\n })\n .join('\\n');\n}\n\nexport function formatSummary(result: ConvertResult): string {\n const lines: string[] = [];\n const rows: string[][] = [];\n for (const file of result.files) {\n // `.csv.gz` is still a CSV, and its rows are still rows. The suffix test dropped the\n // unit from every line of a --gzip summary, so the numbers stood on their own.\n rows.push([\n ` ${file.name}`,\n grouped(file.rows),\n // Singular at one, like every other count this prints: a one-row table is what a\n // narrow window produces, and \"1 rows\" is the same slip 0.5.74 fixed elsewhere.\n /\\.csv(\\.gz)?$/u.test(file.name) ? (file.rows === 1 ? 'row' : 'rows') : '',\n ]);\n }\n lines.push(`Wrote ${printable(result.outputDir)}`); // Escaped; see the File line above.\n lines.push(table(rows, new Set([1])));\n lines.push(`Done in ${elapsed(result.elapsedMs)}.`);\n return lines.join('\\n');\n}\n\n/**\n * How long the run took, in a unit it can be stated in.\n *\n * `elapsedMs` is a difference of two `Date.now()` readings, so it is whole milliseconds, and\n * `(ms / 1000).toFixed(1)` rounds everything under fifty of them to `0.0`. That is the last\n * line of every conversion of a small recording — `--annotations-only` on a three-record\n * file, every fixture in this repository, and the transcript on three documentation pages,\n * all of which read `Done in 0.0s.` for a run that read a file and wrote three.\n *\n * The same refusal `formatDuration` and `formatBytes` make one file over, and for the reason\n * stated there: none of them is a rounding rule, and each declines to print a form the\n * quantity cannot take — 1023.999 KB is not \"1024 KB\", two records of 1e-15s are not \"0s\",\n * and a conversion that happened did not take no time.\n *\n * A clock that reports no change at all has measured something below what it can resolve,\n * which is a different statement from zero and is what it says.\n */\nexport function elapsed(milliseconds: number): string {\n /*\n And the other end of it, which could only say seconds.\n\n This is the one place in the tool that states a length of time without `formatDuration`,\n and it is the line under a summary whose `Duration` field is printed *with* it. A\n conversion of an overnight recording read:\n\n Duration 8h 00m 0s (28800 records of 1s)\n ...\n Done in 412.7s.\n\n Two lengths of time on one screen, one decomposed and one not, from the same program. 412\n seconds is a number a reader has to divide by sixty to hold, which is the whole reason\n `formatDuration` exists and the same complaint `grouped` makes about counts.\n\n Handed over at a minute, since below that the seconds are the readable form — and rounded\n to the tenth this already prints first, because `formatDuration` keeps three decimals and\n \"6m 52.734s\" claims a precision two `Date.now()` readings do not have.\n */\n if (milliseconds >= 60_000) return formatDuration(Math.round(milliseconds / 100) / 10);\n if (milliseconds >= 50) return `${(milliseconds / 1000).toFixed(1)}s`;\n return milliseconds > 0 ? `${milliseconds}ms` : 'under 1ms';\n}\n\n/**\n * Which version produced this record.\n *\n * `metadata.json` has carried it since the file existed, because a conversion should be\n * reproducible later. The two JSON *streams* did not, and they are the ones most likely to\n * outlive the run: `--json` exists to be piped into something, logged, or committed beside a\n * result, where the question a year on is which release's field names and rounding these are.\n * The same shape as metadata.json's, so a consumer reads one field either way.\n */\nconst TOOL = { name: 'edf2csv', version: VERSION } as const;\n\nexport function summaryJson(result: ConvertResult, indent: number | null = 2): string {\n const document = JSON.stringify(\n {\n tool: TOOL,\n output_dir: result.outputDir,\n files: result.files,\n annotations: result.annotationCount,\n duration_seconds: result.file.durationSeconds,\n records: result.file.recordCount,\n elapsed_ms: result.elapsedMs,\n warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Human-readable output for the terminal.\n *\n * Everything here is plain text with no colour codes, so piping to a file or a log\n * produces exactly what appeared on screen.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRates, formatWallClock } from '../edf/header.js';\nimport { fixed, formatBytes, formatDuration, plain } from '../format/number.js';\nimport { counted, grouped } from '../format/list.js';\nimport { escapeCharacter, escapeJsonText, unprintablePattern } from '../format/unprintable.js';\nimport type { ConversionPlan } from '../convert/plan.js';\nimport { withoutFileRateWarning } from '../convert/plan.js';\nimport type { ConvertResult } from '../convert/run.js';\nimport { VERSION } from '../version.js';\n\n/** Where terminal prose wraps. The width --help is written to, and the ANSI default. */\nconst WRAP_COLUMNS = 80;\n\n/** The continuation indent under a `warning: ` prefix, which is the only one there is. */\nconst HINT_INDENT = ' '.repeat(9);\n\n/**\n * One unbreakable unit of a wrapped line: a quoted span, or a run of non-space.\n *\n * The lookbehind is what keeps an apostrophe out of it. `the file's own header and it's` has\n * two of them, and without the guard `'s own header and it'` is one quoted span — a token\n * with spaces in it that then refuses to break. A quote opening a span is at the start of a\n * word, so that is what is required of it.\n */\nconst WORD = /(?<=^|[\\s(])(?:\"[^\"]*\"|'[^']*')|\\S+/gu;\n\n/**\n * Greedy word wrap, `indent` on every line including the first.\n *\n * Only free prose goes through this. The aligned parts of `--info` — the `Format`/`Size`\n * key-value lines and the channel table — are laid out in columns, and re-flowing a column\n * is how you turn a table into a paragraph.\n *\n * A word wider than the column is left to overrun rather than broken. The long words here\n * are file paths and quoted channel labels, and neither survives being split across lines:\n * the point of printing a path is that it can be copied back out.\n *\n * A quoted span is one word for the same reason, however many spaces are inside it. The\n * sentence above is the promise, and it held only for the paths that have no spaces in them:\n * an interrupted conversion said `Files already written to \"/tmp/a very long destination\n * folder name with` / `many spaces here indeed\" are incomplete`, splitting the one thing on\n * the line that has to be copied whole — and quoting is what a path with spaces in it is\n * given in the first place. A word wider than the column already overruns rather than\n * breaking; this is that rule reaching the words it was written for.\n *\n * Which is also why the gap between two words is reproduced rather than normalised to one\n * space. Splitting on `\\s+` and rejoining with `' '` re-flowed everything, and a quoted value\n * is not prose: a channel labelled `EEG A` was offered back as `Did you mean \"EEG A\"?`, and\n * a destination given as `-my nightly` as `Write it as one argument instead:\n * '--out=-my nightly'`. Following either gets a different channel or a different directory\n * than the one the sentence is about, and the run before it had already quoted the name\n * correctly on the line above.\n */\nexport function wrap(text: string, indent = '', width = WRAP_COLUMNS): string {\n const lines: string[] = [];\n let line = indent;\n let at = 0;\n for (const match of text.matchAll(WORD)) {\n const piece = match[0];\n const gap = text.slice(at, match.index);\n at = match.index + piece.length;\n if (line === indent) line += piece;\n else if (line.length + gap.length + piece.length <= width) line += gap + piece;\n else {\n lines.push(line);\n line = indent + piece;\n }\n }\n if (line !== indent) lines.push(line);\n return lines.join('\\n');\n}\n\nfunction table(rows: readonly (readonly string[])[], alignRight: ReadonlySet<number>): string {\n if (rows.length === 0) return '';\n const width: number[] = [];\n for (const row of rows) {\n row.forEach((cell, i) => {\n width[i] = Math.max(width[i] ?? 0, cell.length);\n });\n }\n return rows\n .map((row) =>\n row\n .map((cell, i) => {\n const w = width[i] ?? 0;\n return alignRight.has(i) ? cell.padStart(w) : cell.padEnd(w);\n })\n .join(' ')\n .trimEnd(),\n )\n .join('\\n');\n}\n\n/**\n * Make header text safe to print to a terminal.\n *\n * EDF identification fields and channel labels are free text copied verbatim out of the\n * file, and `--info` puts them straight on stdout. A header carrying ANSI escapes could\n * therefore drive the reader's terminal — `\\x1b[2J\\x1b[H` clears the screen and homes the\n * cursor, which is enough to hide the rest of the output or repaint it as something else.\n * Nobody writes an EDF header that way on purpose, which is exactly why a file that does\n * should not be trusted with the terminal.\n *\n * Control bytes are shown as their escape instead, so a corrupt field stays diagnosable\n * rather than being silently swallowed. This affects display only: `channels.csv` and\n * `metadata.json` still copy the field verbatim, and CSV quoting already makes that safe.\n */\nexport function printable(text: string): string {\n // The character class and the escape are shared with the conversion, which asks the same\n // question of an annotation description; see src/format/unprintable.ts.\n return text.replace(unprintablePattern('gu'), escapeCharacter);\n}\n\n/**\n * The same protection for text that is meant to span lines.\n *\n * `printable` escapes newlines along with everything else, which is right for a channel\n * label — one has no business containing a line break, and it would break the `--info`\n * table's alignment. It is wrong for a whole message: several are written on two lines,\n * and Node's own option errors run to three. Escaping those turned the break into text:\n *\n * error: No channel named \"ECQ\". Did you mean \"ECG\"?\\x0aRun with --info to list ...\n *\n * Each line is escaped on its own, so nothing here gains the ability to drive a terminal.\n * A carriage return is still escaped, so no line can be repainted after it is printed —\n * which is the property that mattered. A newline can only add a line, never overwrite one.\n */\nexport function printableLines(text: string, indent = ''): string {\n return text\n .split('\\n')\n .map((line, index) => {\n const safe = printable(line);\n // The first line is the one that follows `error: ` or `warning: `, so it is the line\n // a log gets grepped for, and it stays whole at whatever width it runs to.\n if (index === 0) return safe;\n\n /*\n Callers write the continuation indent one of two ways: most pass it here and leave\n their message lines flush, but the two usage builders bake ` ` into the string\n itself because they are printed without an `error: ` prefix in front. Reading the\n line's own leading space when none was passed keeps both working, and keeps the\n wrap aligned under the same column either way.\n */\n const body = safe.trimStart();\n const pad = indent === '' ? safe.slice(0, safe.length - body.length) : indent;\n\n /*\n A continuation that is a command is copied, not read.\n\n `edf2csv -- \"--chanels\"` is the line the unknown-option error ends on, and it exists\n to be pasted back into the shell. Wrapping puts `edf2csv --` on one line and the\n flag on the next, and what gets pasted is half a command. Prose survives being\n re-flowed and a command does not, so the two are told apart rather than being\n treated alike and hoping today's strings stay short.\n */\n if (body.startsWith('edf2csv ')) return pad + body;\n return wrap(body, pad);\n })\n .join('\\n');\n}\n\n/** The `--info` view: what is in this recording, and what would converting it produce. */\nexport function formatInfo(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * How many events a conversion would write, when that is already known.\n *\n * Null when it is not. `--info` reads the whole annotation channel of a discontinuous file,\n * because that is where its record times are; a continuous one it reads only far enough to\n * find the origin. So the count is in hand for one of the two, and the line below said \"How\n * many events there are cannot be told from the header\" for both — true of the header, and\n * beside the point on a file whose events had just been read and counted.\n */\n events: number | null = null,\n /**\n * Whether the run would stream to stdout, in which case no file is written at all.\n *\n * The OUTPUT column is \"Named as they will be written. --info is read to find out what a\n * run leaves behind, and a script that opens the name it was given must find a file there\"\n * — and under `--stdout` it named `signals.csv`, or `signals.csv.gz`, for a run that\n * creates no directory and no file. The same table already tells a channel that will not\n * be converted from one the file gives nothing to convert; this is the third thing it\n * could not say.\n */\n toStdout = false,\n): string {\n const { header } = file;\n const lines: string[] = [];\n\n /*\n Escaped, like every other value that came out of the filesystem.\n\n A path is untrusted text: a folder may be named with an ESC byte, and a file name may\n hold a newline on every platform this runs on. The `[n/m]` header a batch prints has\n always escaped it and these two lines did not, so one line of a run reached the terminal\n as `study/esc\\x1b[31mred.edf` and the next as a live colour change — and a name holding a\n newline split `Wrote` across two lines, so the summary reported a path that reads as two.\n NONPRINTABLE_LABEL exists because a header field can carry these bytes; a directory entry\n can carry them just as easily.\n */\n lines.push(`File ${printable(file.path)}`);\n lines.push(`Format ${describeFormat(header)}`);\n /*\n The two raw fields, with edges.\n\n Echoed bare, they ran into each other and into the parenthetical, and a header field is\n free to be empty or to be nothing but padding — `trimField` takes the padding off, so\n both of those arrive here as \"\". A file whose date field is blank printed\n\n Recorded 22.15.00 (unparseable)\n\n where the one value on the line is the *time* and nothing says so, and a file with both\n blank printed `Recorded (unparseable)`: a label, a gap, and a verdict about nothing\n visible. ` . . ` in both fields came out `. . . .`, which is four fields or two\n depending on how the reader counts.\n\n Quoted, which is what `START_TIME_UNREADABLE` — the warning printed under this very line,\n about these very fields — has always done: `(\"\" and \"\")`. Same argument 0.7.x made for the\n time-range refusals, in its words: without them \"the value ran into the sentence ... and\n the surrounding spaces — the actual reason a shell-built argument went wrong — are\n invisible\". `--info --json` has the two as separate strings and never had the problem.\n */\n lines.push(\n `Recorded ${\n formatWallClock(header.startDateTime)?.replace('T', ' ') ??\n `\"${printable(header.startDateRaw)}\" \"${printable(header.startTimeRaw)}\" (unparseable)`\n }`,\n );\n lines.push(\n `Duration ${formatDuration(file.durationSeconds)} (${counted(file.recordCount, 'record')} of ${plain(header.recordDuration)}s)`,\n );\n const elapsedSpan = plan.range.recordingEndSeconds - plan.range.recordingStartSeconds;\n if (Math.abs(elapsedSpan - file.durationSeconds) > 1e-9) {\n /*\n Which way the two differ decides what to call it, and the parenthetical used to say\n \"includes discontinuities\" both ways round.\n\n A span LONGER than the duration is the gap case this line was written for: 3 records of\n 1s covering 11 seconds. A span SHORTER than the duration cannot be a gap — it is records\n that overlap, which an EDF+D file gets when a device re-sends a buffer. Three records of\n 1s starting at 0, 0.5 and 1 print:\n\n Duration 3s (3 records of 1s)\n Time span 2s (includes discontinuities)\n\n A recording covering less time than its own records account for, blamed on gaps it does\n not have — while the warning below it says, correctly, that two records overlap.\n\n A file holding both is described by whichever wins the subtraction, and the overlap\n warning is printed either way.\n */\n const overlapping = elapsedSpan < file.durationSeconds;\n lines.push(\n `Time span ${formatDuration(elapsedSpan)} ` +\n `(${overlapping ? 'records overlap in time' : 'includes discontinuities'})`,\n );\n }\n /*\n Where the samples begin, when that is not zero.\n\n 0.4.9 made the first record's timekeeping TAL the point a recording is timed from, so a\n file whose TALs start at +1000 writes `time_s` from 1000.000 and takes `--start` and\n `--end` on that same clock. None of that appeared here: the report said \"Duration 3s\",\n which reads as 0 to 3, and `--start 0 --end 1` then selected nothing and answered with\n \"The window is inside the recording but lands where there is no data ... Run with --info\n to see where the records actually sit\" — pointing at this report, which was the one place\n the number was missing. It is in `plan.range` already and governs the estimate printed\n below; it was simply never shown.\n */\n const startsAt = plan.range.recordingStartSeconds;\n if (Number.isFinite(startsAt) && Math.abs(startsAt) > 1e-9) {\n // In seconds rather than through formatDuration, because this number is meant to be\n // typed back in: `--start` takes `1000s`, and \"16m 40s\" is not something it accepts.\n // It is also how the empty-window warning renders the window it was given.\n //\n // Through `fixed` rather than `toFixed`, which switches to exponent notation at 1e21 —\n // and `--start 1e+21s` is refused by the time parser with \"uses an unknown unit \\\"e\\\"\",\n // so the one line that says which clock to use handed back a number that clock rejects.\n // Reachable from a conforming file: an EDF+ onset is plain digits of any length, and a\n // record duration large enough to keep samples apart at that magnitude is four\n // characters. `fixed` expands these with BigInt, which is exact past 2^53 where a double\n // carries no fraction anyway, and is byte-for-byte `toFixed` everywhere else.\n lines.push(\n `Timed from ${fixed(startsAt, 3)}s (first sample; --start and --end use this clock)`,\n );\n }\n /*\n And the window, when one was asked for, which this report did not mention at all.\n\n `--info` is the mode whose purpose is to say what a conversion will do, and every other\n flag that changes what gets written is visible in it: `--channels` puts `(not selected)`\n in the OUTPUT column, `--gzip` changes the names there, `--layout long` changes the\n sentence under the table, `--annotations-only` replaces the estimate outright. `--start`\n and `--end` changed one number:\n\n $ edf2csv rec.edf --info $ edf2csv rec.edf --info --start 1s --end 2s\n Duration 3s (3 records of 1s) Duration 3s (3 records of 1s)\n ... ...\n Would write 1,155 rows, roughly 22.2 KB. Would write 385 rows, roughly 7.4 KB.\n\n Byte-for-byte identical otherwise — and the Duration line, which is what a reader checks a\n window against, goes on describing the whole file above an estimate that describes a third\n of it. The window is in `plan.range` already, which is the same thing `Timed from` above\n was added for: it governs the estimate printed below and was simply never shown.\n\n In seconds rather than through `formatDuration`, for the reason that line gives: these are\n numbers meant to be typed back into `--start` and `--end`, and \"16m 40s\" is not one.\n */\n const { range } = plan;\n if (!range.isWholeRecording && Number.isFinite(range.startSeconds) && Number.isFinite(range.endSeconds)) {\n const records = range.endRecord - range.startRecord;\n lines.push(\n `Window ${fixed(range.startSeconds, 3)}s to ${fixed(range.endSeconds, 3)}s ` +\n `(${grouped(records)} of ${counted(file.recordCount, 'data record')})`,\n );\n }\n lines.push(`Size ${formatBytes(file.fileSize)}`);\n if (header.patientId) lines.push(`Patient ${printable(header.patientId)}`);\n if (header.recordingId) lines.push(`Recording ${printable(header.recordingId)}`);\n\n const signals = file.dataSignals;\n lines.push('');\n /*\n Both counts through `counted`, which this function already uses one line up for the record\n count and which exists so a count and its noun cannot disagree.\n\n The annotation-channel half was pluralised by hand when it was fixed — a file carrying two\n of them read \"2 annotation channel\", and EDF+ permits more than one — and the signal half\n had been hand-rolled since it was written. Two spellings of one rule in one line, in the\n file that imports the rule.\n */\n const annotationCount = file.annotationSignals.length;\n const annotationPart =\n annotationCount > 0 ? ` + ${counted(annotationCount, 'annotation channel')}` : '';\n lines.push(`Channels ${counted(signals.length, 'signal')}${annotationPart}`);\n lines.push('');\n\n const rows: string[][] = [['#', 'COLUMN', 'LABEL', 'UNIT', 'RATE', 'RANGE', 'OUTPUT']];\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n // Rendered as a group so that two channels recorded at different rates never show the\n // same figure in the RATE column, which is the one thing this table is asked to settle.\n const rateText = formatRates(signals.map((signal) => signal.samplingRate));\n for (const [row, signal] of signals.entries()) {\n rows.push([\n String(signal.index),\n printable(plan.columnNames.get(signal.index) ?? ''),\n printable(signal.label),\n printable(signal.physicalDimension),\n `${rateText[row]} Hz`,\n `${signal.physicalMin} to ${signal.physicalMax}`,\n /*\n A channel with no samples was reported as \"(not selected)\", which is a different\n thing and not true when it was named on --channels. `edf2csv rec.edf --info\n --channels unused` said the channel the command asked for had not been chosen, when\n what is actually the case is that the file gives it nothing to convert. The\n NO_SAMPLES warning below the table says so; the table contradicted it.\n */\n /*\n And under --annotations-only nothing was deselected either — the run writes no signal\n table at all, which is the same distinction one paragraph up.\n\n `--info --annotations-only --channels \"EEG Fpz-Cz\"` accepted the name, checked it\n against the file, and then printed \"(not selected)\" against the very channel it had\n just been asked for. The sentence three lines under the table already says the truth,\n \"and no signal data\", and `channels.csv` and `--info --json` both say it too — the\n cell is empty and `output_file` is null. The human table was the one place claiming a\n choice nobody made.\n */\n plan.writeSignals\n ? fileFor.has(signal.index)\n ? toStdout\n ? '(stdout)'\n : (fileFor.get(signal.index) as string)\n : signal.samplesPerRecord === 0\n ? '(no samples)'\n : '(not selected)'\n : '(no signal data)',\n ]);\n }\n /*\n A column head over nothing is not a table.\n\n The line above has just said `Channels 0 signals + 1 annotation channel`, and this then\n printed\n\n # COLUMN LABEL UNIT RATE RANGE OUTPUT\n\n with no row under it — seven headings for a set of no channels, on a recording holding\n only events, which is what `annotations-only.edf` in this repository is and what a\n companion event file out of a scoring system is. The same sentence `--channels \"#0\"` was\n given its own wording for: \"No channel at position #0. This file has signal channels at\n .\" states there are some and then names none.\n\n Nothing goes in its place. The count is stated one line up and the body three lines down\n says what the conversion would write; a table saying \"none\" twice more adds nothing.\n */\n if (signals.length > 0) {\n lines.push(table(rows, new Set([0])));\n lines.push('');\n }\n /*\n From here down --info stops laying out columns and starts explaining itself, so from\n here down it wraps. The key-value lines and the channel table above are aligned to each\n other and must not be re-flowed; these are sentences, and the longest of them ran to 156\n columns — which is not a line anyone reads, it is a line a terminal breaks somewhere.\n --help has been written to 80 since it existed and hints joined it in 0.7.1; --info is\n the mode whose whole purpose is being read by a person, and it was the last one guessing.\n */\n if (plan.groups.length > 1) {\n /*\n And the third thing this sentence could not say, after the OUTPUT column above it.\n\n `--stdout` on a recording with more than one rate is refused — it writes one table and\n this makes several — so no file is written and the run does not happen. The sentence\n described it anyway, three lines under a column saying `(stdout)` and six above the\n warning that the run would be refused:\n\n 0 EEG Fpz-Cz ... 256 Hz -250 to 250 (stdout)\n Sampling rates differ, so channels are written to 3 files, one per rate.\n warning: --stdout would refuse this run: needs exactly one table, but this\n recording produces 3 ...\n\n Three statements about one run, and the middle one names an outcome none of the others\n allows. What the rates decide is how many tables there are; where those go is the\n question this mode has already answered differently.\n */\n lines.push(\n wrap(\n plan.layout === 'long'\n ? `Sampling rates differ, and the long layout puts them in one table anyway: each row ` +\n `carries its own time, so nothing has to line up. No channel is resampled.`\n : toStdout\n ? `Sampling rates differ, so this recording makes ${counted(plan.groups.length, 'table')}, ` +\n `one per rate — more than --stdout can write. Converting into a directory writes one ` +\n `file each; --layout long puts them all in one table.`\n : `Sampling rates differ, so channels are written to ${counted(plan.groups.length, 'file')}, one per rate. ` +\n `No channel is resampled.`,\n ),\n );\n }\n /*\n The estimate describes the signal tables, and says so when that is not what will be\n written.\n\n Under --annotations-only there are no signal tables, and the line read \"Would write 0\n rows, roughly 0 B.\" for a conversion that goes on to write annotations.csv with three\n events in it. --info exists to say what a conversion will do; asserting it will write\n nothing, when it will write a file, is the one thing it must not do.\n\n How many events there are cannot be answered from the header — the annotation channel has\n to be read record by record, which is the scan --info is for avoiding. So it says which\n file, and that the count is not knowable this cheaply, rather than inventing a zero.\n */\n /*\n No signal table to describe, whichever way that came about.\n\n This asked only whether `--annotations-only` had been given. A recording that has no\n signal channels — one holding nothing but EDF+ annotations — has none either, and fell\n through to the estimate line: \"Would write 0 rows, roughly 0 B.\" for a conversion that\n goes on to write an annotations.csv with events in it, beside channels.csv and\n metadata.json. That is the sentence 0.4.51 removed, arriving by the other route.\n */\n if (!plan.writeSignals || plan.groups.length === 0) {\n // Named as they will be written. --info is read to find out what a run leaves behind,\n // and a script that opens the name it was given must find a file there.\n const suffix = plan.gzip ? '.csv.gz' : '.csv';\n lines.push(\n wrap(\n file.annotationSignals.length > 0\n ? events === null\n ? `Would write annotations${suffix} and channels${suffix}, and no signal data. How ` +\n 'many events there are cannot be told from the header, and finding out means ' +\n 'reading the annotation channel record by record.'\n : `Would write annotations${suffix} with ${counted(events, 'event')} and ` +\n `channels${suffix}, and no signal data.`\n : `Would write channels${suffix} and no signal data — and no annotations${suffix} ` +\n 'either, since this recording has no annotation channel.',\n ),\n );\n return lines.join('\\n');\n }\n\n // The estimate counts the characters of the CSV, which is what --gzip then compresses.\n // Reporting it as the size on disk would overstate a compressed conversion several-fold.\n const compressing = plan.gzip;\n lines.push(\n wrap(\n // A window narrow enough to select one sample is an ordinary thing to ask for, and this\n // read \"Would write 1 rows, roughly 22 B.\" — the slip 0.5.74 fixed on the lines above it\n // and missed here, because the recording that test builds never estimates exactly one.\n `Would write ${counted(plan.estimate.rows, 'row')}, roughly ` +\n `${formatBytes(plan.estimate.bytes)}${compressing ? ' before compression' : ''}.`,\n ),\n );\n\n return lines.join('\\n');\n}\n\n/**\n * The `--info` view as JSON, for surveying files from a script.\n *\n * `indent` is 2 for a single recording, matching what this has always printed, and null for\n * a batch — several pretty-printed documents run together are readable by a streaming parser\n * but not by anything that expects one record per line, and a batch is exactly where\n * line-oriented reading is wanted. null rather than undefined because a default parameter\n * takes effect when undefined is passed, which quietly restored the indentation this was\n * meant to drop; JSON.stringify itself wants undefined, so it is translated at the call.\n *\n * `--info` answers \"what is in this recording and what would converting it cost\", which\n * is exactly the question you want to ask across a directory of hundreds of recordings —\n * and the text table is the wrong shape for that. `--json` previously applied only to\n * conversions, so scripts had to parse the aligned columns or convert files just to learn\n * what was in them.\n *\n * Field names match `metadata.json` where the two describe the same thing, so a survey and\n * a conversion can be read by the same code.\n */\nexport function infoJson(\n file: EdfFile,\n plan: ConversionPlan,\n /**\n * The event count, on the files where `--info` has already read and counted them.\n *\n * `formatInfo` has taken this since 0.7.x and prints it — \"Would write annotations.csv with\n * 3 events\" — and the same number was dropped on the way to the JSON, which is the surface\n * a script reads and the reason `--info --json` exists. Null where the count is not in\n * hand, by the same rule `estimate` is null for a run that writes no signal table: a\n * continuous file is read only as far as its origin, so its events have not been counted\n * and there is no honest number to put here.\n */\n events: number | null = null,\n indent: number | null = 2,\n /** See `formatInfo`: under `--stdout` there is no file to name. */\n toStdout = false,\n): string {\n const { header } = file;\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n const document = JSON.stringify(\n {\n tool: TOOL,\n path: file.path,\n bytes: file.fileSize,\n format: describeFormat(header),\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n // For a discontinuous file this exceeds duration_seconds by the length of the gaps.\n time_span_seconds: plan.range.recordingEndSeconds - plan.range.recordingStartSeconds,\n // Where `time_s` begins, and the clock `--start` and `--end` are read against. Usually\n // zero; not when the first record's timekeeping TAL puts the recording elsewhere. Both\n // of the fields above are lengths and neither says where that length sits.\n first_sample_seconds: plan.range.recordingStartSeconds,\n /*\n The window, under the names `metadata.json` gives it, which is the rule this document\n follows for everything describing the run.\n\n The text form did not show it either until 0.8.82; here the only trace of `--start`\n and `--end` was `estimate.rows` coming back smaller, with nothing in the document\n saying why. A survey that runs `--info --json` over a folder with a window on the\n command line archives a description of each recording in which the window does not\n appear.\n */\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n annotation_channels: file.annotationSignals.length,\n // Named as `summaryJson` names the same count, so a prediction and a conversion read\n // the same field.\n annotations: events,\n channels: file.dataSignals.map((signal) => ({\n signal_index: signal.index,\n column: plan.columnNames.get(signal.index) ?? '',\n label: signal.label,\n unit: signal.physicalDimension,\n sampling_rate_hz: signal.samplingRate,\n samples_per_record: signal.samplesPerRecord,\n physical_min: signal.physicalMin,\n physical_max: signal.physicalMax,\n digital_min: signal.digitalMin,\n digital_max: signal.digitalMax,\n transducer: signal.transducer,\n prefiltering: signal.prefiltering,\n /*\n `-`, the placeholder `ConvertResult.outputDir` already uses for this mode, and not\n `null`: null is this document's word for \"would not be converted\", which is a\n different thing from \"converted, to a stream\".\n */\n output_file: fileFor.has(signal.index)\n ? toStdout\n ? '-'\n : (fileFor.get(signal.index) as string)\n : null,\n })),\n /*\n Null rather than zero when the run writes no signal table.\n\n The text form has refused to say \"Would write 0 rows, roughly 0 B.\" since 0.4.51,\n because a run that goes on to write an annotations.csv with events in it has not\n written nothing — and it is `--annotations-only`, or a recording holding only\n annotations, that reaches this. The JSON went on saying it to the surface a script\n reads. There is no estimate for a table that does not exist, and null is how this\n document already says that.\n */\n estimate:\n plan.writeSignals && plan.groups.length > 0\n ? {\n rows: plan.estimate.rows,\n // Character count of the CSV. With --gzip the file on disk is smaller than this.\n bytes: plan.estimate.bytes,\n exceeds_spreadsheet_limit: plan.estimate.exceedsSpreadsheetLimit,\n }\n : { rows: null, bytes: null, exceeds_spreadsheet_limit: false },\n // The plan's mixed-rate warning replaces the header parser's, as it does everywhere\n // else. This was the one consumer left out of that when 0.3.2 made the warning follow\n // --channels, so `--info --json` carried it twice: once counting the rates being\n // converted and once counting every rate in the file, with the same code and severity.\n // The file's own first, then the plan's, which is the order the text form prints them\n // in. Concatenating the other way round listed the same warnings about the same\n // recording in two different sequences depending on which form you asked for.\n warnings: withoutFileRateWarning(file.diagnostics)\n .concat(plan.diagnostics)\n .map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n\n/**\n * One diagnostic per `warning: ` line, prefixed so warnings are greppable; the hint below it\n * wrapped to the terminal.\n *\n * The hint has been on its own unprefixed continuation line since these gained hints at all,\n * so grepping for `warning:` never picked it up and wrapping it costs nothing that was being\n * relied on — which is what 0.6.132 got wrong when it left every diagnostic long on the\n * grounds that they are one line each. Half of that is true. The `warning:` head is a line\n * per diagnostic and stays one, at whatever width the message runs to; the hint underneath\n * it is prose addressed to a person reading a terminal, and 17 of them ran past 80 columns,\n * the widest to 180. At that width the second half of the advice is wherever the terminal\n * decided to put it, indented under nothing, and the 9-space rule that says \"this belongs to\n * the warning above\" is lost at exactly the moment there is enough text for it to matter.\n */\nexport function formatDiagnostics(diagnostics: readonly Diagnostic[]): string {\n return diagnostics\n .map((d) => {\n // Diagnostics quote channel labels, which come from the file, so they need the\n // same treatment as the --info table.\n const head = `${d.severity}: ${printable(d.message)}`;\n return d.hint ? `${head}\\n${wrap(printable(d.hint), HINT_INDENT)}` : head;\n })\n .join('\\n');\n}\n\nexport function formatSummary(result: ConvertResult): string {\n const lines: string[] = [];\n const rows: string[][] = [];\n for (const file of result.files) {\n // `.csv.gz` is still a CSV, and its rows are still rows. The suffix test dropped the\n // unit from every line of a --gzip summary, so the numbers stood on their own.\n rows.push([\n ` ${file.name}`,\n grouped(file.rows),\n // Singular at one, like every other count this prints: a one-row table is what a\n // narrow window produces, and \"1 rows\" is the same slip 0.5.74 fixed elsewhere.\n /\\.csv(\\.gz)?$/u.test(file.name) ? (file.rows === 1 ? 'row' : 'rows') : '',\n ]);\n }\n lines.push(`Wrote ${printable(result.outputDir)}`); // Escaped; see the File line above.\n lines.push(table(rows, new Set([1])));\n lines.push(`Done in ${elapsed(result.elapsedMs)}.`);\n return lines.join('\\n');\n}\n\n/**\n * How long the run took, in a unit it can be stated in.\n *\n * `elapsedMs` is a difference of two `Date.now()` readings, so it is whole milliseconds, and\n * `(ms / 1000).toFixed(1)` rounds everything under fifty of them to `0.0`. That is the last\n * line of every conversion of a small recording — `--annotations-only` on a three-record\n * file, every fixture in this repository, and the transcript on three documentation pages,\n * all of which read `Done in 0.0s.` for a run that read a file and wrote three.\n *\n * The same refusal `formatDuration` and `formatBytes` make one file over, and for the reason\n * stated there: none of them is a rounding rule, and each declines to print a form the\n * quantity cannot take — 1023.999 KB is not \"1024 KB\", two records of 1e-15s are not \"0s\",\n * and a conversion that happened did not take no time.\n *\n * A clock that reports no change at all has measured something below what it can resolve,\n * which is a different statement from zero and is what it says.\n */\nexport function elapsed(milliseconds: number): string {\n /*\n And the other end of it, which could only say seconds.\n\n This is the one place in the tool that states a length of time without `formatDuration`,\n and it is the line under a summary whose `Duration` field is printed *with* it. A\n conversion of an overnight recording read:\n\n Duration 8h 00m 0s (28800 records of 1s)\n ...\n Done in 412.7s.\n\n Two lengths of time on one screen, one decomposed and one not, from the same program. 412\n seconds is a number a reader has to divide by sixty to hold, which is the whole reason\n `formatDuration` exists and the same complaint `grouped` makes about counts.\n\n Handed over at a minute, since below that the seconds are the readable form — and rounded\n to the tenth this already prints first, because `formatDuration` keeps three decimals and\n \"6m 52.734s\" claims a precision two `Date.now()` readings do not have.\n */\n if (milliseconds >= 60_000) return formatDuration(Math.round(milliseconds / 100) / 10);\n if (milliseconds >= 50) return `${(milliseconds / 1000).toFixed(1)}s`;\n return milliseconds > 0 ? `${milliseconds}ms` : 'under 1ms';\n}\n\n/**\n * Which version produced this record.\n *\n * `metadata.json` has carried it since the file existed, because a conversion should be\n * reproducible later. The two JSON *streams* did not, and they are the ones most likely to\n * outlive the run: `--json` exists to be piped into something, logged, or committed beside a\n * result, where the question a year on is which release's field names and rounding these are.\n * The same shape as metadata.json's, so a consumer reads one field either way.\n */\nconst TOOL = { name: 'edf2csv', version: VERSION } as const;\n\nexport function summaryJson(result: ConvertResult, indent: number | null = 2): string {\n const document = JSON.stringify(\n {\n tool: TOOL,\n output_dir: result.outputDir,\n files: result.files,\n annotations: result.annotationCount,\n duration_seconds: result.file.durationSeconds,\n records: result.file.recordCount,\n elapsed_ms: result.elapsedMs,\n warnings: result.diagnostics.map((d) => ({ code: d.code, severity: d.severity, message: d.message })),\n },\n null,\n indent ?? undefined,\n );\n // A path is untrusted text on this surface too; see escapeJsonText.\n return escapeJsonText(document);\n}\n"]}
@@ -412,11 +412,29 @@ async function prepareOutputDir(dir, force) {
412
412
  */
413
413
  if (!existing && (await lstat(dir).catch(() => null))) {
414
414
  throw new ConversionError('OUTPUT_UNWRITABLE', `"${dir}" is a symbolic link to something that does not exist, so nothing can be ` +
415
- `written there.`, 'Remove the link, or choose a directory with --out. --force replaces a previous ' +
416
- 'output directory and cannot follow a link to nowhere.');
415
+ `written there.`, 'Remove the link, or choose a directory with --out. --force writes into a directory ' +
416
+ 'that is already there, and a link to nowhere is not one.');
417
417
  }
418
418
  if (!force) {
419
- throw new ConversionError('OUTPUT_EXISTS', `"${dir}" already exists.`, 'Pass --force to overwrite it, or --out to choose a different directory.');
419
+ /*
420
+ What `--force` does, rather than what it sounds like it does.
421
+
422
+ "Overwrite it" reads as a claim about the directory, and the neighbouring hint said so
423
+ outright — "--force replaces a previous output directory". It does neither. It writes
424
+ into the directory: files of the same name are replaced and everything else is left
425
+ exactly where it was, which is why `STALE_OUTPUT` exists to report what stayed. The
426
+ flag list has described it accurately since it was written — "write into the output
427
+ directory if it already exists" — so the one sentence a reader meets at the moment
428
+ they decide whether to pass it was the one contradicting the other two.
429
+
430
+ The difference is a directory that ends up holding two conversions. Convert a
431
+ mixed-rate recording and then a single-rate one into the same place with `--force`,
432
+ and `signals_256hz.csv` sits beside a fresh `signals.csv` under a channels.csv and a
433
+ metadata.json describing only the second — which is exactly the outcome a reader who
434
+ was told the directory would be replaced does not expect, and does not check for.
435
+ */
436
+ throw new ConversionError('OUTPUT_EXISTS', `"${dir}" already exists.`, 'Pass --force to write into it, leaving whatever else it holds, or --out to choose ' +
437
+ 'a different directory.');
420
438
  }
421
439
  }
422
440
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/convert/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executing a conversion.\n *\n * Everything is written in a single pass over the data records. All rate groups are\n * open at once and fed from the same batch of bytes, so a file is read once no\n * matter how many output tables it produces, and memory stays flat.\n */\n\nimport { createWriteStream, fstatSync } from 'node:fs';\nimport { lstat, mkdir, readdir, stat, writeFile } from 'node:fs/promises';\nimport { finished } from 'node:stream/promises';\nimport type { Writable } from 'node:stream';\nimport { createGzip, gzipSync } from 'node:zlib';\nimport path from 'node:path';\n\nimport type { RecordBatch } from '../edf/reader.js';\nimport { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRate, formatRates, formatWallClock, startsFormula } from '../edf/header.js';\nimport type { Diagnostic } from '../edf/errors.js';\nimport { EdfError } from '../edf/errors.js';\nimport type { Annotation } from '../edf/annotations.js';\nimport {\n BufferedLineWriter,\n DEFAULT_FLUSH_THRESHOLD,\n UTF8_BOM,\n csvRow,\n escapeCsvField,\n} from '../format/csv.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { escapeCharacter, escapeJsonText, unprintableIn } from '../format/unprintable.js';\nimport {\n makeSampleFormatter,\n makeTimeFormatter,\n newOffsetBudget,\n newSampleCacheBudget,\n plain,\n} from '../format/number.js';\nimport type { SampleFormatter } from '../format/number.js';\nimport { TIME_COLUMN } from './channels.js';\nimport { assertInputPath } from './options.js';\nimport { buildPlan, outputCsvName, withoutFileRateWarning } from './plan.js';\nimport type { ConversionPlan, PlanOptions, RateGroup } from './plan.js';\nimport { deriveRecordStarts, withTimingPromiseKept } from './timing.js';\nimport { sampleTimeIsInRange, toleranceFor } from './time-range.js';\nimport { VERSION as TOOL_VERSION } from '../version.js';\n\nexport { TOOL_VERSION };\n\nexport type ConversionErrorCode =\n | 'OUTPUT_EXISTS'\n | 'OUTPUT_UNWRITABLE'\n | 'INPUT_OUTPUT_COLLISION'\n | 'INPUT_UNREADABLE'\n | 'UNSUPPORTED_REQUEST'\n | 'CALLBACK_FAILED'\n | 'WRITE_FAILED';\n\n/**\n * Codes that mean the command cannot be carried out as written, rather than that something\n * about the file or the destination went wrong.\n *\n * The distinction is the one the exit codes draw: 1 is \"the file or the destination is the\n * problem\", 2 is \"the command line is the problem\". A caller with a `--stdout` conflict is\n * being told to change the flags — the hints say exactly that — so filing it under 1 sent\n * scripts looking at the disk. Exit 2 already covers checks that need the header first,\n * such as a `--channels` term matching nothing.\n */\nexport const USAGE_ERROR_CODES: ReadonlySet<ConversionErrorCode> = new Set(['UNSUPPORTED_REQUEST']);\n\nexport class ConversionError extends Error {\n readonly code: ConversionErrorCode;\n readonly hint: string | undefined;\n constructor(code: ConversionErrorCode, message: string, hint?: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConversionError';\n this.code = code;\n this.hint = hint;\n }\n}\n\nexport interface ConvertOptions extends PlanOptions {\n /**\n * Destination directory. Defaults to `defaultOutputDir(inputPath)`: the input's name with\n * its extension replaced by `_csv`, beside the input — `sleep-study.edf` gives\n * `sleep-study_csv`, not `sleep-study`.\n */\n outputDir?: string | undefined;\n /** Overwrite an existing output directory. */\n force?: boolean | undefined;\n /** Record a SHA-256 of the input in metadata.json. Costs one extra read of the file. */\n checksum?: boolean | undefined;\n /**\n * Write the signal CSV to stdout instead of to a directory.\n *\n * Only valid when the conversion produces exactly one signal file. In the default wide\n * layout a mixed-rate recording becomes several tables, and merging them into one stream\n * would mean inventing the samples this tool exists not to invent; `layout: 'long'` gives\n * one table for any recording, so it lifts the restriction. No sidecar files are written.\n */\n toStdout?: boolean | undefined;\n onProgress?: ((progress: ConversionProgress) => void) | undefined;\n}\n\nexport interface ConversionProgress {\n recordsDone: number;\n recordsTotal: number;\n bytesWritten: number;\n}\n\nexport interface WrittenFile {\n name: string;\n rows: number;\n}\n\nexport interface ConvertResult {\n outputDir: string;\n files: WrittenFile[];\n /**\n * True when a `--stdout` reader closed the pipe before the conversion finished.\n *\n * `edf2csv rec.edf --stdout | head -1` is an ordinary thing to type and not a failure, but\n * it is also not a conversion: the row count is rows formatted before the close was\n * noticed, which is neither the recording's total nor what the reader received.\n */\n readerHungUp: boolean;\n annotationCount: number;\n diagnostics: Diagnostic[];\n plan: ConversionPlan;\n file: EdfFile;\n elapsedMs: number;\n}\n\ninterface OpenGroup {\n group: RateGroup;\n writer: BufferedLineWriter;\n formatters: SampleFormatter[];\n formatTime: (recordStart: number, sample: number) => string;\n rows: number;\n /** Resolves once a compressed stream's bytes have reached the file behind it. */\n settled: Promise<void>;\n /** Detaches the compressor's error forwarding from a stream this tool does not own. */\n release: () => void;\n}\n\nexport async function convert(inputPath: string, options: ConvertOptions = {}): Promise<ConvertResult> {\n assertInputPath(inputPath);\n const startedAt = Date.now();\n const file = await EdfFile.open(inputPath);\n\n try {\n /*\n Hashed before a record is read, and only published if the file held still.\n\n A checksum taken afterwards cannot be trusted whichever descriptor it goes through. A\n file overwritten in place keeps its inode, so the open handle sees the new bytes too,\n and the old ones are simply gone — there is nowhere left to read what was converted.\n Taking it first at least means the hash describes the file the header was read from.\n\n What makes it a guarantee rather than a hope is the check at the end: if size or\n modification time moved at any point, the hash is dropped and the run says why. So\n `sha256` present means the file demonstrably did not change while it was read, and a\n recording still being written gets a null and a warning instead of a plausible hash of\n the wrong bytes. (A change reverted within the same modification timestamp would slip\n through; nothing short of copying the input first can close that.)\n */\n const checksumAtOpen = options.checksum === true ? await file.sha256() : null;\n\n // One pass over the annotation channel supplies everything annotation-related:\n // where each record sits in time, and the full event list. It reads the whole\n // file even when a window was requested, because an annotation inside the window\n // may be stored in a record outside it.\n const annotationData =\n file.annotationSignals.length > 0\n ? await file.readAnnotations()\n : { annotations: [] as Annotation[], recordStarts: [] as (number | null)[], malformed: 0 };\n\n const timing = deriveRecordStarts(file, annotationData);\n\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n options,\n );\n plan.diagnostics.push(...timing.diagnostics);\n\n if (options.toStdout === true) {\n const refusal = stdoutRefusal(file, plan);\n if (refusal) throw refusal;\n let readerHungUp = false;\n const written = await writeSignalFiles(file, plan, null, timing.starts, options, (hungUp) => {\n readerHungUp = hungUp;\n });\n if (await file.changedSinceOpen()) plan.diagnostics.push(inputChanged(false));\n return {\n outputDir: '-',\n files: written,\n readerHungUp,\n annotationCount: 0,\n // Over both lists, because the timing warnings are pushed onto the plan's.\n diagnostics: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timing.starts !== null),\n ...plan.diagnostics,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n { toStdout: true, gzip: plan.gzip },\n ),\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n /*\n A destination whose last component is `.` or `..` does not name a directory of its own.\n\n `prepareOutputDir` claims the final component with a single non-recursive mkdir, having\n created its parents recursively — which is what makes two conversions racing for one\n directory safe. `path.dirname(\"out/.\")` is `\"out\"`, so for these the parent step creates\n the destination itself and the claim then asks the filesystem to make `.` inside it,\n which always exists. The result was a refusal naming a directory this same run had just\n made, one line after making it:\n\n edf2csv rec.edf --out ./fresh/.\n error: \"./fresh/.\" already exists.\n Pass --force to overwrite it, or --out to choose a different directory.\n\n Exit 1, nothing converted, and an empty `fresh/` left on disk. `--force` does not help:\n the claim fails the same way whatever it is told, so the path was unusable rather than\n occupied.\n\n Normalised only in that case, so `--out ./converted` keeps the spelling it was given —\n that one is already how the directory is found on disk, which is what `output_dir`\n promises, and rewriting it to `converted` would churn every example for no gain.\n */\n const asked = options.outputDir ?? defaultOutputDir(inputPath);\n const outputDir = /(?:^|[\\\\/])\\.\\.?$/u.test(asked) ? path.normalize(asked) : asked;\n await assertInputDoesNotOverlapOutputs(inputPath, outputDir, file, plan, options);\n await prepareOutputDir(outputDir, options.force === true);\n\n const written: WrittenFile[] = [];\n\n if (plan.writeSignals && plan.groups.length > 0) {\n const signals = await writeSignalFiles(file, plan, outputDir, timing.starts, options);\n written.push(...signals);\n } else {\n const missing = noSignalFile(file, plan);\n if (missing) plan.diagnostics.push(missing);\n }\n\n const noEvents = noAnnotations(file, options);\n if (noEvents) plan.diagnostics.push(noEvents);\n\n let annotationsWritten = 0;\n if (file.annotationSignals.length > 0) {\n const window = requestedAnnotationWindow(options, plan.range.recordingStartSeconds);\n // Reported against the rows that will be written, not against the file; see\n // durationDiagnostics.\n plan.diagnostics.push(...durationDiagnostics(annotationData.annotations, window));\n plan.diagnostics.push(...descriptionDiagnostics(annotationData.annotations, window));\n const result = await writeAnnotationsCsv(\n outputDir,\n annotationData.annotations,\n window,\n options.gzip === true,\n options.bom === true,\n );\n written.push(result);\n annotationsWritten = result.rows;\n /*\n A run whose only table came out empty, which nothing said.\n\n `emptyWindow`'s own docstring states the rule this was breaking: \"everywhere else that\n a request produces nothing this tool says so: a --channels term matching nothing is an\n error, and --annotations-only on a file with no events raises NO_ANNOTATIONS.\" The\n second half is true only of a file with no annotation *channel*. Give the flag a\n recording whose channel carries nothing but timekeeping, or a window its events fall\n outside, and the run writes an annotations.csv holding one header line, prints \"Wrote\n … annotations.csv 0 rows\", exits 0 and passes --strict — which is exactly the shape\n EMPTY_WINDOW exists to refuse on the signal side, arriving by the other route.\n\n Only when there is no signal table either, so this is the whole of what the run\n produced. A windowed conversion of a recording whose events sit elsewhere is\n ordinary and its signals are the point; warning there would fire on most windows of\n most annotated recordings, which is how a warning stops being read.\n */\n if (!plan.writeSignals && annotationsWritten === 0) {\n plan.diagnostics.push(emptyAnnotations(annotationData.annotations.length, window, plan.gzip));\n }\n }\n\n written.push(\n await writeChannelsCsv(outputDir, file, plan, options.gzip === true, options.bom === true),\n );\n\n /*\n The file moved under the conversion. Said out loud, because nothing else shows it.\n\n The CSVs are still correct for the records that were read, and metadata.json still\n describes the file as it was opened — so the record is consistent with the output\n whatever happens here. What stops being true is that the checksum describes the bytes\n that were converted, since an in-place overwrite leaves nowhere to read them from. It\n is dropped rather than guessed at.\n */\n const changed = await file.changedSinceOpen();\n if (changed) plan.diagnostics.push(inputChanged(options.checksum === true));\n\n await writeMetadata(\n outputDir,\n inputPath,\n file,\n plan,\n written,\n annotationsWritten,\n changed ? null : checksumAtOpen,\n timing.starts !== null,\n options.bom === true,\n );\n\n const stale = await findStaleOutput(outputDir, written);\n\n return {\n outputDir,\n files: written,\n readerHungUp: false,\n annotationCount: annotationsWritten,\n // The directory path needs it too: `--gzip` changes the names, and two of these\n // sentences are a file name and nothing else. See withSidecarsNamed.\n diagnostics: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timing.starts !== null),\n ...plan.diagnostics,\n ...stale,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n { toStdout: false, gzip: plan.gzip },\n ),\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n } finally {\n await file.close();\n }\n}\n\n/*\n Files this tool produces, used to spot leftovers from an earlier conversion.\n\n The rate part has to allow every shape a filename can now take, not just the plain ones:\n\n signals_256hz.csv an integer rate\n signals_12_5hz.csv a fractional rate, decimal point written as an underscore\n signals_1_000e-7hz.csv a rate small enough to need exponent form (0.2.2)\n signals_256hz.csv.gz any of the above, compressed (0.3.0)\n signals_0hz_2.csv a second group whose rate slug collided (0.2.1)\n\n The previous `[\\w.]+hz` matched neither of the last two — `-` and `+` are not word\n characters, and the collision suffix falls after the `hz`. Both were introduced by\n recent changes and both silently stopped being recognised as this tool's own output, so\n leftovers of exactly those kinds went unreported: the one situation the warning exists\n for. Requiring a digit after the underscore keeps a user's own `signals_notes.csv` out.\n\n And `signals_Infinityhz.csv`, which is a name this tool writes and the digit rule then\n excluded. A record duration too small to divide into overflows the rate — `rateSlug` says\n `Infinityhz` and the writer opens that file, with the TIME_RESOLUTION warning beside it\n saying no rows will go in it — so a mixed-rate run of such a file leaves two `signals_*`\n files behind and the next conversion into that directory named one of them:\n\n warning: signals_1e+308hz.csv is left over from an earlier conversion into this\n directory and was not rewritten.\n\n with `signals_Infinityhz.csv` sitting beside it, from the same run, unmentioned. Spelled\n out rather than loosened, so a user's own `signals_notes.csv` stays out for the reason\n above; the check below holds the pattern against every name `rateSlug` can produce.\n*/\nconst OUTPUT_PATTERN =\n /^(signals(_(\\d[\\w.+-]*|Infinity)hz(_\\d+)?)?\\.csv(\\.gz)?|annotations\\.csv(\\.gz)?|channels\\.csv(\\.gz)?|metadata\\.json)$/u;\n\n/**\n * Detect output from a previous run that this one did not replace.\n *\n * `--force` overwrites files but does not empty the directory, so converting a\n * mixed-rate recording and then a single-rate one into the same place leaves\n * `signals_256hz.csv` sitting next to a fresh `signals.csv`. Both look current.\n * Nothing is deleted here — the user is told, and decides.\n */\nasync function findStaleOutput(\n outputDir: string,\n written: readonly WrittenFile[],\n): Promise<Diagnostic[]> {\n const entries = await readdir(outputDir).catch(() => null);\n if (!entries) return [];\n\n // metadata.json is rewritten on every run but is not part of the reported file list.\n const fresh = new Set([...written.map((f) => f.name), 'metadata.json']);\n const stale = entries.filter((name) => OUTPUT_PATTERN.test(name) && !fresh.has(name)).sort();\n if (stale.length === 0) return [];\n\n return [\n {\n code: 'STALE_OUTPUT',\n severity: 'warning',\n /*\n Through `listed`, like every other message that enumerates something this run does not\n control. How many stale files a directory holds is up to the directory, and a\n mixed-rate recording converted into a reused one is exactly how it fills up: 120 old\n `signals_<rate>hz.csv` files produced a single 2,373-character warning line. That is\n the failure `listed` was written for — its own comment quotes the 1,600-character\n version of it — and this was the one message still joining its own list.\n\n The hint said \"Delete them\" whatever the count, so one stale file read \"signals_999hz\n .csv is left over ... Delete them.\"\n */\n message:\n `${listed(stale)} ${stale.length === 1 ? 'is' : 'are'} left over from an earlier ` +\n `conversion into this directory and ${stale.length === 1 ? 'was' : 'were'} not rewritten.`,\n hint:\n `Delete ${stale.length === 1 ? 'it' : 'them'}, or convert into a fresh directory, so ` +\n 'the two runs do not get mixed up.',\n },\n ];\n}\n\nexport function defaultOutputDir(inputPath: string): string {\n /*\n The third export whose only argument is a path, and the one left handing a wrong value on.\n\n `path.basename` refuses a non-string with Node's own text — `The \"path\" argument must be\n of type string. Received type number (5)` — which names a parameter of `path`, not one of\n this function, and reaches a caller who never called `path`. `convert` got this check in\n 0.7.x and `EdfFile.open` in 0.8.9; this is what the api page tells you to call to find out\n where those two would write, so it is reached by exactly the same caller.\n */\n assertInputPath(inputPath);\n const base = path.basename(inputPath, path.extname(inputPath));\n return path.join(path.dirname(inputPath), `${base}_csv`);\n}\n\n/** Never let an output target resolve to the recording being read, even with --force. */\nasync function assertInputDoesNotOverlapOutputs(\n inputPath: string,\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n options: ConvertOptions,\n): Promise<void> {\n /*\n The names this run will actually write, compressed ones included.\n\n The rate files come from the plan and already carry `.csv.gz` under --gzip; the sidecars\n were spelled out here and did not. So a compressed run checked two names it would never\n write and missed the two it would: a recording sitting at <outdir>/channels.csv.gz was\n overwritten by its own conversion, with --force, reported as a success. The same file\n named signals.csv.gz was refused, which is what gives the oversight away.\n */\n const suffix = options.gzip === true ? '.csv.gz' : '.csv';\n const names = new Set(plan.groups.map((group) => group.fileName));\n if (file.annotationSignals.length > 0) names.add(`annotations${suffix}`);\n names.add(`channels${suffix}`);\n names.add('metadata.json');\n\n const inputResolved = path.resolve(inputPath);\n /*\n Tolerant of the input going away, like every other stat in this function.\n\n This one was bare, so a recording renamed or unlinked between `EdfFile.open` and here threw\n Node's own error straight out of `convert()`:\n\n Error: ENOENT: no such file or directory, stat './link.edf'\n\n with no `code` this tool defines and no hint — a sixth error type from a function the API\n page says throws five, on the one path where a file moving under a conversion is the\n documented ordinary case. The descriptor is already open, so the conversion itself is fine;\n what is lost is the inode comparison, and `samePath` below still answers the case that\n matters when the two names are the same name.\n */\n const inputInfo = await stat(inputPath).catch(() => null);\n\n for (const name of names) {\n const target = path.join(outputDir, name);\n const targetResolved = path.resolve(target);\n const targetInfo = await stat(target).catch(() => null);\n const samePath = targetResolved === inputResolved;\n const sameFile =\n targetInfo !== null &&\n inputInfo !== null &&\n targetInfo.dev === inputInfo.dev &&\n targetInfo.ino === inputInfo.ino;\n if (!samePath && !sameFile) continue;\n\n throw new ConversionError(\n 'INPUT_OUTPUT_COLLISION',\n `Output file \"${target}\" is the same file as the input recording.`,\n 'Choose a separate directory with --out. The input was not modified.',\n );\n }\n}\n\nasync function prepareOutputDir(dir: string, force: boolean): Promise<void> {\n /*\n Claim the directory with a single atomic mkdir rather than asking whether it exists\n and then creating it.\n\n Checking first left a window between the two: two conversions started together both\n saw \"not there\", both proceeded, and both opened write streams on the same signals.csv.\n Neither reported anything — both exited 0, having half-written one file between them.\n A non-recursive mkdir cannot do that. Exactly one caller creates the directory; every\n other one gets EEXIST from the filesystem and takes the already-exists path below.\n\n Parents are still created recursively, since --out ./a/b/c should work. Only the final\n component is the claim.\n */\n const parent = path.dirname(dir);\n if (parent && parent !== dir) {\n try {\n await mkdir(parent, { recursive: true });\n } catch (cause: unknown) {\n // A parent that is a regular file surfaces as EEXIST naming the parent, which reads\n // as though the destination already exists rather than as \"you cannot put a\n // directory inside a file\". Name the real obstacle instead of the errno.\n const info = await stat(parent).catch(() => null);\n if (info && !info.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": \"${parent}\" is a file, not a directory.`,\n 'Choose a destination whose parent directories are directories, with --out.',\n );\n }\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n createHint(cause),\n );\n }\n }\n\n let claimed = true;\n try {\n await mkdir(dir);\n } catch (cause: unknown) {\n if ((cause as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n createHint(cause),\n );\n }\n claimed = false;\n }\n\n if (!claimed) {\n const existing = await stat(dir).catch(() => null);\n\n // --force means \"replace my previous output\", not \"write output files into\n // whatever this happens to be\". Pointing it at a regular file needs saying so.\n if (existing && !existing.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a file, but the converted data needs a directory.`,\n 'Choose a directory with --out.',\n );\n }\n /*\n A link to nothing, which `stat` cannot see and `mkdir` will not write through.\n\n `stat` follows symbolic links, so a dangling one leaves `existing` null and fell through\n to the sentence below — which is the one claim that is not true of it:\n\n $ edf2csv rec.edf --out link-to-nowhere\n error: \"link-to-nowhere\" already exists.\n Pass --force to overwrite it, or --out to choose a different directory.\n\n Nothing is there. And following that advice made it worse: `--force` reached the writer\n and came back as `Writing to \"link-to-nowhere\" failed: part of the path does not exist.\n The files written so far are incomplete and should not be used` — a conversion failure,\n about files that were never written, advising the reader to make sure nothing is\n removing a directory that never existed.\n\n A broken link in a batch's destination is how this arrives: the run before it wrote into\n a mount that has since gone.\n */\n if (!existing && (await lstat(dir).catch(() => null))) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a symbolic link to something that does not exist, so nothing can be ` +\n `written there.`,\n 'Remove the link, or choose a directory with --out. --force replaces a previous ' +\n 'output directory and cannot follow a link to nowhere.',\n );\n }\n if (!force) {\n throw new ConversionError(\n 'OUTPUT_EXISTS',\n `\"${dir}\" already exists.`,\n 'Pass --force to overwrite it, or --out to choose a different directory.',\n );\n }\n }\n}\n\n/**\n * Turn a Node filesystem error into something a person can act on: an errno as a sentence,\n * so Node's own text never reaches the screen.\n *\n * Exported for the CLI's last-resort stdout listener, which is the third and last place a\n * write failure becomes a message and the one that was still printing `ENOSPC: no space left\n * on device, write`.\n */\nexport function describeFsError(cause: unknown): string {\n const code = (cause as NodeJS.ErrnoException | undefined)?.code;\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOSPC') return 'the disk is full';\n // The one that actually happens on a shared filesystem, and the one this list left out.\n if (code === 'EDQUOT') return 'you are over your disk quota on this filesystem';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n if (code === 'EROFS') return 'the filesystem is read-only';\n if (code === 'ENAMETOOLONG') return 'the path is too long';\n /*\n The two `writeHint` knew and this did not, which showed once the write failures started\n coming through here: `writeHint` reads the same errno off the same error and has a\n sentence for eleven of them, this had seven, and the gap was the two that only a write\n reaches. `EISDIR` is a directory sitting where an output file belongs, which is the\n commonest way to make one of these happen on purpose.\n\n `EPIPE` is deliberately not here. The writer treats a reader hanging up as the ordinary\n end of a pipeline rather than as a failure, so it never becomes a message.\n */\n if (code === 'EISDIR') return 'a directory is sitting there already';\n if (code === 'EMFILE' || code === 'ENFILE') return 'too many files are open';\n /*\n The one this list could not describe, and the recursive mkdir above is what makes it\n surprising: every parent is created on the way, so \"no such file or directory\" is not a\n parent that was missing. It is a component that exists and leads nowhere — a symbolic\n link with no target is the way to get one — or a directory removed by something else\n between the two calls.\n\n Missing it meant the fallback ran, which is Node's own text, complete with the internal\n call that raised it:\n\n error: Cannot create \"dangle/x\": ENOENT: no such file or directory, mkdir 'dangle'.\n Check the path exists and that you have permission to write there.\n\n Keeping that off the screen is the whole purpose of the six lines above it.\n */\n if (code === 'ENOENT') return 'part of the path does not exist';\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nasync function writeSignalFiles(\n file: EdfFile,\n plan: ConversionPlan,\n outputDir: string | null,\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n onHangUp?: (hungUp: boolean) => void,\n): Promise<WrittenFile[]> {\n // One budget for every table in this conversion, not one per table: see OffsetBudget.\n const offsets = newOffsetBudget();\n // Likewise one for every channel, not one per channel: see SampleCacheBudget.\n const sampleCaches = newSampleCacheBudget();\n // Only the stdout path needs this; --out finds a full disk on its own, because it always\n // has another file to write afterwards. See auditStdout.\n const audit = outputDir === null ? auditStdout() : null;\n /*\n In the long layout every group writes into one table, so they share one stream. Opening\n a stream per group on the same path is what the rate-slug collision fix in groupByRate\n was about: two writers on one file interleave rows under a header naming one of them.\n */\n let shared: {\n stream: Writable;\n settled: Promise<void>;\n release: () => void;\n target: Writable;\n writer: BufferedLineWriter;\n } | null = null;\n\n /*\n One buffer's worth of memory for the conversion, not one per table.\n\n Every group had its own writer at the default 1 MiB threshold, so pending output was\n group count × 1 MiB before anything drained. A 6.5 MB recording with 40 sampling rates\n — the header allows thousands of channels, and a research montage really does mix a\n dozen rates — died with a raw V8 heap out-of-memory and exit 134 under a 96 MB cap,\n while the site advertises 48 MB. The recording is small; it is the fan-out that is not.\n\n Split evenly with a floor, so the single-rate case, which is nearly every recording,\n keeps exactly the buffer it always had, and forty tables cost one megabyte between them\n rather than forty. The floor is what the sum is bounded by rather than the split: past\n 128 groups every table takes the 8 KiB minimum, so the total grows again from there.\n The long layout shares one writer already and is unaffected either way.\n */\n const MIN_FLUSH_THRESHOLD = 8 * 1024;\n const flushThreshold = Math.max(\n MIN_FLUSH_THRESHOLD,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n /*\n The stream's own buffer is the other half of the same sum.\n\n 0.5.6 shared the line-buffer budget and left `createWriteStream` at its 64 KiB default,\n which is per stream: 200 rate groups meant 12.8 MB of stream buffer on top of 12.8 MB of\n line buffer, and an 855 KB recording still died at a 48 MB cap. Shared the same way, with\n a floor that keeps an ordinary conversion writing in useful-sized pieces.\n */\n const streamBuffer = Math.max(\n 16 * 1024,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n\n const open: OpenGroup[] = plan.groups.map((group, groupIndex) => {\n // A null directory means the single table goes to stdout. process.stdout is already a\n // writable stream, so the same buffered writer and backpressure handling apply.\n const target =\n shared?.target ??\n (outputDir === null\n ? process.stdout\n : createWriteStream(path.join(outputDir, group.fileName), {\n highWaterMark: streamBuffer,\n }));\n const { stream, settled, release } = shared ?? compressed(target, options.gzip === true);\n /*\n Under --gzip the writer feeds the compressor, so its byte count is the CSV before\n compression and says nothing about what reached the descriptor. The compressor's own\n output is what stdout is handed, so that is what is counted. `pipe` uses a 'data'\n listener of its own and a second one is delivered the same chunks.\n\n Once per stream, which in the long layout is once for all the groups. Attaching per\n group put N listeners on the one shared compressor, so every chunk was counted N\n times: `--stdout --layout long --gzip` on a 40-rate recording claimed 622,240 of\n 622,240 bytes where 15,556 had been written, failed with a disk-full error over a\n perfectly good file, and printed Node's MaxListenersExceededWarning to stderr on the\n way past ten. 0.5.4 fixed the same arithmetic in the uncompressed branch and left\n this one, because the uncompressed branch is where the count is a sum and this one is\n where it is a subscription.\n */\n if (audit && stream !== target && !shared) {\n stream.on('data', (chunk: Buffer) => audit.count(chunk.length));\n }\n /*\n One writer, not one per group, when the table is shared. Separate writers over one\n stream each hold their own buffer and flush on their own schedule, so the rows would\n reach the file in whatever order the buffers filled — which is not the order they\n were produced in, and the long layout's whole claim is that its rows are in time\n order.\n */\n const writer = shared?.writer ?? new BufferedLineWriter(stream, flushThreshold);\n if (plan.layout === 'long' && !shared) shared = { stream, settled, release, target, writer };\n\n // Only the first group writes the header of a shared table, and the mark before it.\n if (plan.layout !== 'long' || groupIndex === 0) {\n if (options.bom === true) writer.push(UTF8_BOM);\n writer.pushLine(\n plan.layout === 'long'\n ? csvRow([TIME_COLUMN, 'channel', 'value'])\n : csvRow([TIME_COLUMN, ...group.channels.map((c) => c.column)]),\n );\n }\n return {\n group,\n writer,\n formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals, sampleCaches)),\n formatTime: makeTimeFormatter(\n group.samplesPerRecord,\n group.rate,\n group.timeDecimals,\n offsets,\n ),\n rows: 0,\n settled,\n release,\n };\n });\n\n try {\n const written = await streamSignalRows(file, plan, open, recordStarts, options);\n\n /*\n Checked once everything has been flushed and ended, and not when the reader hung up.\n\n `--stdout | head -1` closes the pipe on purpose, which is a shell idiom rather than a\n failure. A pipe is not a regular file, so auditStdout declines it anyway — the second\n guard is here because the cost of getting this one wrong is reporting a failure for a\n command that worked.\n */\n if (audit) {\n // Uncompressed, the writer hands its bytes straight to the descriptor; compressed,\n // they were counted on the compressor's way out.\n if (options.gzip !== true) {\n /*\n Once per writer, not once per group. The long layout gives every group the same\n writer, so counting per group multiplied its byte total by the number of rates:\n `--stdout --layout long` on a three-rate recording handed over 32,043 bytes, was\n credited with 96,129, and failed with a disk-full error for a file that was\n complete on disk. It only showed with stdout redirected to a regular file, since\n that is the one case the audit applies to — which is the command the --layout\n documentation gives.\n */\n for (const writer of new Set(open.map((entry) => entry.writer))) {\n audit.count(writer.bytesOut);\n }\n }\n if (!open.some((entry) => entry.writer.hungUp)) audit.verify();\n }\n onHangUp?.(open.some((entry) => entry.writer.hungUp));\n return written;\n } catch (cause) {\n for (const entry of open) entry.writer.destroy();\n\n /*\n Reading and writing both fail through here, and both were reported as writing.\n\n A recording that shrinks mid-conversion — still being written by the acquisition\n software, say — raises the reader's own error, which names the record and says the\n file changed size while it was being read. That precise diagnosis was then filed under\n `Writing to \"<dir>\" failed` and given the hint about freeing disk space, which sends\n someone to look at the one part of the system that was working.\n\n The reader's message and its advice are kept; only the note about partial output is\n added, since that much is true of either failure.\n */\n // A ConversionError arrived already saying what went wrong — a callback that threw, say.\n // Wrapping it again turned \"the onProgress callback threw\" into `Writing to \"out\"\n // failed: the onProgress callback threw`, under a hint about checking the destination.\n if (cause instanceof ConversionError) throw cause;\n\n if (cause instanceof EdfError) {\n const where = outputDir === null ? 'stdout' : `\"${outputDir}\"`;\n throw new ConversionError(\n 'INPUT_UNREADABLE',\n cause.message,\n `${cause.hint ? `${cause.hint} ` : ''}What was written to ${where} before it failed is ` +\n `incomplete and should not be used.`,\n );\n }\n\n /*\n Through `describeFsError`, like the destination errors above, and not through Node's\n own text.\n\n That function exists to keep an errno and the internal call that raised it off the\n screen — its comment says so in as many words, quoting the leak it was written for:\n `Cannot create \"dangle/x\": ENOENT: no such file or directory, mkdir 'dangle'.` The\n write failures never joined it, so a directory sitting where signals.csv belongs read\n\n error: Writing to \"out\" failed: EISDIR: illegal operation on a directory, open 'out/signals.csv'\n\n with the errno token, the syscall, the path repeated, and no full stop — the one\n diagnostic shape this tool went back and fixed. The hint printed directly under it has\n always read the same errno and said \"A directory is sitting where that file belongs\",\n so the answer was in hand; it was the sentence above it that had not asked.\n */\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to ${outputDir === null ? 'stdout' : `\"${outputDir}\"`} failed: ` +\n `${describeFsError(cause)}.`,\n writeHint(cause, outputDir === null),\n );\n }\n}\n\n/**\n * Whether two sample times are close enough that only rounding could separate them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude. Well below any\n * real sample interval — the finest a recording can declare is bounded by its record\n * duration and samples-per-record fields — and well above the one-ULP disagreement that two\n * exact divisions of the same instant produce.\n */\nfunction nearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * One record's samples in the long layout: `time_s,channel,value`, in time order.\n *\n * The groups are merged rather than written one after another. Every sample in a record\n * falls inside that record's span, so taking the earliest next sample across the groups\n * each time leaves the whole file sorted by `time_s` — which is the only thing that makes a\n * mixed-rate long table useful, since sorting 3 million rows afterwards is the reader's\n * problem and a large one.\n *\n * Ties go to the group with the higher rate, which is the order the groups are already in.\n * Within a sample time the channels come out in the order the file declares them.\n */\nasync function writeLongRecord(\n file: EdfFile,\n open: readonly OpenGroup[],\n batch: RecordBatch,\n recordInBatch: number,\n recordStart: number,\n range: ConversionPlan['range'],\n): Promise<void> {\n const writer = open[0]?.writer;\n if (!writer) return;\n\n const cursors = new Int32Array(open.length);\n /* Reused across sample times so a three-million-row conversion allocates one of these. */\n const due: { entry: OpenGroup; groupIndex: number; sample: number; channel: number }[] = [];\n\n for (;;) {\n let earliest = Infinity;\n let source = -1;\n let sourceSample = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time < earliest) {\n earliest = time;\n source = g;\n sourceSample = sample;\n }\n }\n if (earliest === Infinity) return;\n\n /*\n Everything at this instant, in the order the file declares its channels.\n\n Groups are ordered by rate, largest first, because that is how the wide layout names\n its files. Emitting a tie group by group therefore ordered the channels by descending\n sampling rate — so a recording declaring `slow, medium, fast` wrote `fast, medium,\n slow`, while the documentation promised file order and channels.csv listed file order.\n Signal index is the file's own order, and the only one a reader can predict.\n\n \"At this instant\" is decided on the time as written, not on the double. `s / rate` for\n two channels at one moment need not give the same double: a 0.3 s record holding 12 and\n 4 samples makes 40 Hz and 13.333… Hz, and 9/40 is 0.22500000000000000555 while 3/13.333…\n is 0.22499999999999997780. Equality missed that, so those two rows fell out in numeric\n order — `slow` before `fast`, once, in the middle of a file that was otherwise right.\n\n Two rows are at one time exactly when they carry the same `time_s`, which is the only\n definition a reader of the CSV can apply. The numeric pre-filter keeps the common case\n to one comparison; formatting happens only for candidates already within a hair.\n */\n let earliestText: string | null = null;\n /*\n Formatted on the first near-tie and not before, which on a single-rate table is never.\n\n The group and sample that set `earliest` are captured above rather than searched for\n again, since the loop below advances cursors as it goes and a later search would be\n looking at a table that had already moved on.\n */\n const asWritten = (): string =>\n (earliestText ??= open[source]?.formatTime(recordStart, sourceSample) ?? '');\n due.length = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time !== earliest) {\n // Far away in the ordinary case; only a near-tie is worth formatting.\n if (!nearlyEqual(time, earliest)) continue;\n if (entry.formatTime(recordStart, sample) !== asWritten()) continue;\n }\n cursors[g] = sample + 1;\n // Same window rule as the wide layout, with the same per-rate slack.\n if (\n !sampleTimeIsInRange(earliest, range.startSeconds, range.endSeconds, toleranceFor(entry.group.rate))\n ) {\n continue;\n }\n for (let c = 0; c < entry.group.channels.length; c++) {\n due.push({ entry, groupIndex: g, sample, channel: c });\n }\n }\n if (due.length === 0) continue;\n if (due.length > 1) {\n due.sort(\n (a, b) =>\n (a.entry.group.channels[a.channel]?.signal.index ?? 0) -\n (b.entry.group.channels[b.channel]?.signal.index ?? 0),\n );\n }\n\n for (const item of due) {\n const channel = item.entry.group.channels[item.channel];\n const format = item.entry.formatters[item.channel];\n if (!channel || !format) continue;\n if (writer.hungUp) return;\n writer.pushLine(\n `${item.entry.formatTime(recordStart, item.sample)},${escapeCsvField(channel.column)},` +\n `${format(file.sampleAt(batch, recordInBatch, channel.signal, item.sample))}`,\n );\n item.entry.rows++;\n // Flushed inside the record for the same reason the wide layout is; see there.\n if (writer.full) await writer.flush();\n }\n }\n}\n\nasync function streamSignalRows(\n file: EdfFile,\n plan: ConversionPlan,\n open: OpenGroup[],\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n): Promise<WrittenFile[]> {\n const { startSeconds, endSeconds, startRecord, endRecord } = plan.range;\n const { recordDuration } = file.header;\n let recordsDone = 0;\n\n /*\n Every destination has hung up, so nothing formatted from here on could reach anyone.\n `--stdout | head -1` is the usual way to arrive here: the reader takes one line and\n closes the pipe while the conversion is still near the start of the recording.\n\n Stopping also makes the reported row count mean what it says — rows that reached the\n consumer, rather than rows the loop went on formatting into a discarded buffer.\n */\n const allHungUp = (): boolean => open.length > 0 && open.every((entry) => entry.writer.hungUp);\n\n for await (const batch of file.readRecords({ startRecord, endRecord })) {\n if (allHungUp()) break;\n for (let r = 0; r < batch.recordCount; r++) {\n if (allHungUp()) break;\n const index = batch.firstRecordIndex + r;\n const recordStart = recordStarts ? (recordStarts[index] ?? index * recordDuration) : index * recordDuration;\n\n if (plan.layout === 'long') {\n await writeLongRecord(file, open, batch, r, recordStart, plan.range);\n recordsDone++;\n continue;\n }\n\n for (const entry of open) {\n const { group, writer, formatters, formatTime } = entry;\n const { channels, rate } = group;\n // Slack that never reaches the next sample; see toleranceFor.\n const slack = toleranceFor(rate);\n\n for (let sample = 0; sample < group.samplesPerRecord; sample++) {\n const time = recordStart + sample / rate;\n if (!sampleTimeIsInRange(time, startSeconds, endSeconds, slack)) continue;\n\n let row = formatTime(recordStart, sample);\n for (let c = 0; c < channels.length; c++) {\n const channel = channels[c];\n const format = formatters[c];\n if (!channel || !format) continue;\n row += ',' + format(file.sampleAt(batch, r, channel.signal, sample));\n }\n if (writer.hungUp) break;\n writer.pushLine(row);\n entry.rows++;\n\n /*\n Flushed inside the record, not only at the end of one.\n\n The buffer was drained once per record, so the rows of a single record piled up\n with nothing emptying them — memory followed samples-per-record rather than the\n batch size the writer exists to hold to. One record of 16,000,000 samples died\n with a heap out of memory under a 256 MB cap, while the same 32 MB of samples\n split into 16,000 records converted to the same 283 MB CSV without trouble. The\n format allows either layout and says nothing about which to expect.\n\n `full` is a synchronous read of the pending size, so the twenty million rows that\n are not at a boundary cost a comparison rather than a microtask each.\n */\n if (writer.full) await writer.flush();\n }\n await writer.maybeFlush();\n }\n\n recordsDone++;\n }\n\n if (options.onProgress) {\n /*\n A caller's callback is the caller's, and its failures are not the destination's.\n\n This ran inside the same try that turns a stream failure into WRITE_FAILED, so a\n progress callback that threw came back as `Writing to \"out\" failed: caller bug`,\n advising the reader to check a destination that was working perfectly. It is the\n same misattribution the write hints had until 0.4.36, one layer up.\n\n The original is kept as `cause`, so the stack that actually matters survives, and\n the conversion still stops — the callback threw, and carrying on writing into a\n directory whose owner has just failed is not an improvement.\n */\n try {\n options.onProgress({\n recordsDone,\n recordsTotal: endRecord - startRecord,\n // Once per writer: the long layout's groups share one, so summing per group\n // reported a figure larger than the file being written.\n bytesWritten: [...new Set(open.map((entry) => entry.writer))].reduce(\n (sum, entry) => sum + entry.charsWritten,\n 0,\n ),\n });\n } catch (cause) {\n throw new ConversionError(\n 'CALLBACK_FAILED',\n `The onProgress callback threw: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'This is the caller\\'s callback, not the recording or the destination. Whatever was ' +\n 'written before it threw is incomplete and should not be used.',\n { cause },\n );\n }\n }\n }\n\n const closed = new Set<BufferedLineWriter>();\n for (const entry of open) {\n if (closed.has(entry.writer)) continue;\n closed.add(entry.writer);\n try {\n await entry.writer.end();\n // With --gzip the writer's stream is the compressor, whose end callback fires when the\n // compressor is done rather than when the file behind it is. Awaiting only that would\n // report success with the tail of the file still in flight.\n await entry.settled;\n } finally {\n // In a finally for the reason 0.5.45 gives about the writer's own release: a\n // conversion that fails here still has to leave process.stdout as it found it, and a\n // failure is exactly when a caller goes on to convert something else.\n entry.release();\n }\n }\n\n // A shared table is one file, and its row count is every group's rows, not the first's.\n if (plan.layout === 'long') {\n const first = open[0];\n if (!first) return [];\n return [{ name: first.group.fileName, rows: open.reduce((sum, e) => sum + e.rows, 0) }];\n }\n return open.map((entry) => ({ name: entry.group.fileName, rows: entry.rows }));\n}\n\n/**\n * The stream rows are written to, plus a promise for the data reaching its destination.\n *\n * Compression sits between the writer and the file as a transform. Failures below it — a\n * full disk, an unwritable path — surface on the file stream, where nothing is listening,\n * so they are forwarded onto the compressor: that is the stream the writer watches, and\n * routing them there keeps one error path rather than two.\n */\nfunction compressed(\n target: Writable,\n gzip: boolean,\n): { stream: Writable; settled: Promise<void>; release: () => void } {\n if (!gzip) return { stream: target, settled: Promise.resolve(), release: (): void => {} };\n const compressor = createGzip();\n /*\n Removable, because one of the streams this can be handed outlives the conversion.\n\n 0.5.36 fixed exactly this leak for the writer's own listener: a library caller running\n twelve `toStdout` conversions left twelve 'error' listeners on `process.stdout` and got\n Node's MaxListenersExceededWarning on the eleventh. That fix is `BufferedLineWriter`'s\n `#release()`, and it cannot reach this one — under `gzip` the writer's stream is the\n compressor, and `process.stdout` is only ever the compressor's destination. So the same\n leak survived on the same stream, behind one extra flag, and the regression test written\n to catch it does not pass `gzip: true`.\n\n A file stream is a different matter and needs no release: it is created for this\n conversion and closed with it. The discipline is the writer's — release only from a\n stream this tool does not own.\n */\n const forward = (error: Error): void => {\n compressor.destroy(error);\n };\n target.on('error', forward);\n const release = (): void => {\n if (target === process.stdout || target === process.stderr) target.off('error', forward);\n };\n\n /*\n pipe() ends its destination when the source ends, and stdout must survive the\n conversion: the writer already refuses to close it, because a closed stdout breaks\n every later write in the process. Nothing waits on stdout either — it is not this\n tool's to finish.\n */\n const toStdout = target === process.stdout;\n compressor.pipe(target, { end: !toStdout });\n if (toStdout) {\n /*\n Waited on, even though stdout is not ours to end.\n\n This returned an already-resolved promise, so `await entry.settled` waited for nothing\n and the conversion declared itself finished while the compressor still held the tail of\n the stream. `--stdout --gzip` onto a destination that filled up therefore printed the\n ENOSPC *and then* \"Wrote 102,400 rows to stdout.\" — and exited 0, over a file 11,270\n bytes short whose gzip member has no trailer and will not decompress. Through `--out`,\n on the same volume with the same space, the identical failure is reported and exits 1.\n\n The byte audit could not see it either: it stats the descriptor as soon as the writers\n are done, which on this path is before the compressor has pushed its last chunks.\n\n `finished(compressor)` is the source side, not the destination — it resolves when the\n compressor has flushed everything into stdout, and it is stdout's own write that fails.\n That keeps `end: !toStdout` exactly as it was: nothing here closes stdout.\n */\n const flushed = finished(compressor).catch((error: unknown) => {\n /*\n EPIPE is not a failure here, and turning it into one is the trap this nearly fell\n into: `--stdout --gzip | head` is an ordinary thing to type, and the documented\n answer to it is \"Stopped: the reader closed the pipe after 52,507 of 102,400 rows\n had been written\", exit 0. Waiting on the compressor surfaced the EPIPE that the\n uncompressed path already routes through the writer's hang-up flag — the writer\n sees the same error, forwarded, and records it — so the wait has to let that one\n through and keep everything else.\n */\n if ((error as NodeJS.ErrnoException | null)?.code === 'EPIPE') return;\n throw error;\n });\n flushed.catch(() => {});\n return { stream: compressor, settled: flushed, release };\n }\n\n /*\n A failure under the compressor rejects both this promise and the writer's own end(),\n and end() is the one awaited first. Without a handler attached here that rejection\n belonged to nobody, and Node killed the process over it: `--gzip` into an unwritable\n path printed a raw EISDIR stack trace instead of the ordinary \"Writing to ... failed\"\n message and exit 1, which is what the same path does uncompressed.\n\n Attaching the handler is enough to make it handled. Awaiting `settled` downstream still\n reports the failure in the case where end() happened to succeed.\n */\n const settled = finished(target);\n settled.catch(() => {});\n return { stream: compressor, settled, release };\n}\n\n/**\n * Write one of the sidecar files, reporting a failure the way the signal writer does.\n *\n * These three used to call `writeFile` bare, so a failure escaped as whatever the\n * filesystem said — `EISDIR: illegal operation on a directory, open '...'` with no hint\n * and, more importantly, no mention that the signal files had already been written. The\n * conversion stopped half-done and the message gave no sign of it.\n */\nasync function writeOutputFile(\n outputDir: string,\n name: string,\n contents: string,\n gzip = false,\n bom = false,\n): Promise<void> {\n // metadata.json never gets one: JSON.parse rejects a leading U+FEFF, so a mark there\n // would break every reader of the file to help a spreadsheet that will not open it.\n if (bom) contents = UTF8_BOM + contents;\n try {\n // The sidecars are built in memory before being written, so compressing them in memory\n // costs nothing extra. Only the signal tables are large enough to need a stream.\n await writeFile(path.join(outputDir, name), gzip ? gzipSync(contents) : contents, gzip ? undefined : 'utf8');\n } catch (cause) {\n // The same as the signal tables above; see the note there.\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing \"${name}\" to \"${outputDir}\" failed: ${describeFsError(cause)}.`,\n writeHint(cause),\n );\n }\n}\n\nasync function writeChannelsCsv(\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const includedColumns = new Set(plan.groups.flatMap((g) => g.channels.map((c) => c.signal.index)));\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n /*\n A rate rendered the way the file name beside it in the same row was rendered.\n\n `sampling_rate_hz` and `output_file` are two cells of one row about one rate, and they were\n written from different renderings of it — the cell from `String`, the name from\n `formatRates` over the whole set. They agree on every rate `String` prints plainly and part\n company on the ones it does not:\n\n a,0,a,uV,1e-19,100,...,signals_1_000e-19hz.csv,yes\n\n A reader joining that column against the files on disk finds `1e-19` naming\n `1_000e-19hz`, and `--info` printing a third thing again — except that `--info`'s RATE\n column is `1.000e-19 Hz`, which is the name's own form. channels.csv was the one surface\n rendering the rate for itself.\n\n Rendered together because that is how the names are: two rates a sixth decimal apart round\n to one string, and `formatRates` widens the whole set when they do, so a per-rate rendering\n would disagree with the names again on exactly those files.\n */\n const groupRates = plan.groups.map((group) => group.rate);\n const rateText = new Map<number, string>(\n formatRates(groupRates).map((text, i) => [groupRates[i] as number, text]),\n );\n\n const lines = [\n csvRow([\n 'column',\n 'signal_index',\n 'label',\n 'unit',\n 'sampling_rate_hz',\n 'samples_per_record',\n 'physical_min',\n 'physical_max',\n 'digital_min',\n 'digital_max',\n 'transducer',\n 'prefiltering',\n 'output_file',\n 'converted',\n ]),\n ];\n\n for (const signal of file.header.signals) {\n if (signal.isAnnotations) continue;\n const column = plan.columnNames.get(signal.index) ?? `signal_${signal.index}`;\n lines.push(\n csvRow([\n column,\n String(signal.index),\n signal.label,\n signal.physicalDimension,\n /*\n Plain decimal, at whatever magnitude the header declares.\n\n `String` switches to exponent notation below 1e-6 and above 1e21, which `plain`\n exists to undo — annotations.csv's two numeric columns were given it because \"an\n EDF+ TAL states its onset as ordinary decimal text, so a file saying `+0.0000001`\n came back as `1e-7` in a column whose every other cell is a plain decimal\", and\n `fixed` refuses the same form in signals.csv for the same reason: \"a reader parsing\n the column as decimal text has no reason to expect it.\" channels.csv was the third\n CSV and the one that kept it. A magnetometer calibrated to ±1e-16 T had its\n `physical_min` written `-1e-16` beside a `digital_min` of `-32768`, in the pair of\n columns this documentation points at for recovering the digital codes, over a\n signals.csv writing every one of those values out in full — and a channel calibrated\n to ±100 in the row above it, so the column held both notations at once.\n */\n // Nor the rate, which is rendered above against the names it has to match. Nor\n // samples_per_record, which is a count.\n rateText.get(signal.samplingRate) ?? formatRate(signal.samplingRate),\n String(signal.samplesPerRecord),\n plain(signal.physicalMin),\n plain(signal.physicalMax),\n plain(signal.digitalMin),\n plain(signal.digitalMax),\n signal.transducer,\n signal.prefiltering,\n fileFor.get(signal.index) ?? '',\n includedColumns.has(signal.index) ? 'yes' : 'no',\n ]),\n );\n }\n\n const name = outputCsvName('channels', gzip);\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: lines.length - 1 };\n}\n\n/*\n The bounds an annotation is filtered against: what the caller actually asked for, not the\n window that survived being clamped to the recording.\n\n Filtering by the resolved window meant an unbounded request still lost events. `--end 999h`\n on a three-second file clamps to 3, and `--start 0` gets its end from the recording, so\n both filtered to [0, 3) and dropped the markers at 3.0 and 3.5 that a bare invocation\n keeps — asking for more of a recording returned less of it.\n\n An end the caller did not give is unbounded, not \"the end of the data\": EDF+ lets an\n annotation sit past the last sample, which is exactly where an end-of-recording marker is.\n\n `--duration` is measured from wherever the conversion actually starts. Anchoring it at 0\n instead read the same absent `--start` two ways in two adjacent lines, and on a recording\n that does not begin at zero the two windows did not even overlap: an EDF+D file whose first\n record sits at 30 s converted its samples from [30, 35) while filtering annotations against\n (-inf, 5), so every event inside the converted window was dropped and `annotations.csv` came\n back empty. `resolveRange` has always defaulted the same missing start to the earliest\n record, which is where this now takes it from.\n*/\n/**\n * What the durations in the events that will actually be written look like.\n *\n * These two warnings were raised from the file-wide counts the decoder accumulates, while\n * `annotations.csv` is filtered to the requested window. A conversion of one second of a\n * recording therefore warned that \"1 annotation states a duration that is not a number, so\n * its duration_s cell is empty\" about an event two seconds outside it — naming a cell that is\n * not in the output — and `--strict` failed the run for it. There is no such value, no such\n * cell, and no such row.\n *\n * Taken from the events themselves, after the same filter the writer applies, so the count\n * and the sentence describe the same rows. An unreadable duration is carried on the event\n * because `duration: null` cannot say whether the file gave one; a negative duration needs no\n * flag, since the value is right there.\n */\nexport function durationDiagnostics(\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n): Diagnostic[] {\n const written = annotations.filter((a) => a.onset >= window.from && a.onset < window.to);\n const diagnostics: Diagnostic[] = [];\n\n const negative = written.filter((a) => a.duration !== null && a.duration < 0).length;\n if (negative > 0) {\n const one = negative === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${counted(negative, 'annotation')} state${one ? 's' : ''} a duration below zero, ` +\n `which is not a length of time.`,\n hint:\n 'The value is written to annotations.csv as the file gave it. Adding it to onset_s ' +\n 'ends the event before it starts, so check these rows before using the durations.',\n });\n }\n\n const unreadable = written.filter((a) => a.durationUnreadable === true).length;\n if (unreadable > 0) {\n const one = unreadable === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${counted(unreadable, 'annotation')} state${one ? 's' : ''} a duration that is ` +\n `not a number, so ${one ? 'its' : 'their'} duration_s cell is empty.`,\n hint:\n 'The onset and the description were read normally. An empty duration_s otherwise ' +\n 'means the file stated no duration, so these rows cannot be told apart from those.',\n });\n }\n\n return diagnostics;\n}\n\n/**\n * What the descriptions in the events that will actually be written look like.\n *\n * EDF's four free-text header fields have had two warnings about where they land since they\n * were written: `FORMULA_LABEL` for text a spreadsheet runs instead of reading, and\n * `NONPRINTABLE_LABEL` for bytes that drive a terminal. Both say the same thing about the\n * remedy — the text is written exactly as the file has it, because rewriting it would mean\n * the CSV no longer says what the recording says — and both exist so that the tool is not\n * silent about where it goes.\n *\n * `annotations.csv`'s `description` column is the same kind of text, out of the same file,\n * into the same spreadsheet, and nothing was said about it at all. An event described\n * `=HYPERLINK(\"http://…\",\"Sleep stage W\")` was written verbatim, exit 0, no warning, and\n * opens as a live link nobody in the reading chain wrote; one carrying `\\x1b[31m` turns the\n * terminal red on `cat annotations.csv`. It is the more likely of the two to happen by\n * accident, since a description is typed by a person at a scoring station while a channel\n * label is written once by the recorder.\n *\n * It is also the only free text in the output that can carry a character above U+00FF —\n * header text is decoded latin1, so every byte of it becomes a code point below U+0100, and\n * a bidirectional override cannot reach a label. It can reach a description, which is UTF-8.\n *\n * Counted rather than raised per event, unlike the header's four fields: a night's scoring is\n * thousands of events, and a warning each is not a report. The count is of the rows that\n * reach `annotations.csv`, after the same window filter the writer applies, for the reason\n * `durationDiagnostics` beside it gives.\n */\nexport function descriptionDiagnostics(\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n): Diagnostic[] {\n const written = annotations.filter((a) => a.onset >= window.from && a.onset < window.to);\n const diagnostics: Diagnostic[] = [];\n\n const formulaic = written.filter((a) => startsFormula(a.text));\n if (formulaic.length > 0) {\n const one = formulaic.length === 1;\n const shown = [...new Set(formulaic.map((a) => a.text[0] as string))].join(', ');\n diagnostics.push({\n code: 'FORMULA_LABEL',\n severity: 'warning',\n message:\n `${counted(formulaic.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +\n `starting with ${shown}, which Excel, LibreOffice and Google Sheets read as the start ` +\n `of a formula rather than as text.`,\n hint:\n 'The text is written to annotations.csv exactly as the file has it, so the cell is ' +\n 'what the recording says. Open the CSV with pandas or R, or import it into the ' +\n 'spreadsheet as text, if you do not want it evaluated.',\n });\n }\n\n const marked = written.filter((a) => unprintableIn(a.text).length > 0);\n if (marked.length > 0) {\n const one = marked.length === 1;\n const shown = [...new Set(marked.flatMap((a) => unprintableIn(a.text)))]\n .map(escapeCharacter)\n .join(', ');\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `${counted(marked.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +\n `carrying text a terminal does not print as itself (${shown}), written to ` +\n `annotations.csv exactly as the file has ${one ? 'it' : 'them'}.`,\n hint:\n 'A control byte can drive the terminal and a bidirectional override reverses what ' +\n 'follows it, so read the file with pandas or R rather than with cat. The cell is ' +\n 'what the recording says either way.',\n });\n }\n\n return diagnostics;\n}\n\n/**\n * The window annotations are filtered by — the bounds as asked for, not as snapped to records.\n *\n * Exported so `--info` can count the events a conversion would write using the same predicate\n * that writes them, rather than a second copy of it.\n */\nexport function requestedAnnotationWindow(\n options: ConvertOptions,\n recordingStart: number,\n): { from: number; to: number } {\n const from = options.start ?? -Infinity;\n const to =\n options.end !== undefined\n ? options.end\n : options.duration !== undefined\n ? (options.start ?? recordingStart) + options.duration\n : Infinity;\n return { from, to };\n}\n\nasync function writeAnnotationsCsv(\n outputDir: string,\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const inWindow = annotations\n .filter((a) => a.onset >= window.from && a.onset < window.to)\n .sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n\n const lines = [csvRow(['onset_s', 'duration_s', 'description', 'record_index'])];\n for (const annotation of inWindow) {\n lines.push(\n csvRow([\n plain(annotation.onset),\n annotation.duration === null ? '' : plain(annotation.duration),\n annotation.text,\n String(annotation.recordIndex),\n ]),\n );\n }\n\n const name = outputCsvName('annotations', gzip);\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: inWindow.length };\n}\n\n/**\n * What to try next, chosen from what actually went wrong.\n *\n * Every write failure carried the same advice — \"Free up space or choose another destination\n * with --out\" — which fits exactly one errno. A directory sitting where signals.csv belongs\n * came back telling the reader to free up disk space, and so did a read-only volume, a\n * permission denial and a path too long for the filesystem. Wrong advice is worse than none:\n * it sends someone to check `df` on a disk that is fine, and the thing that is actually\n * wrong stays unexamined.\n *\n * The errno is the one piece of the failure that names the cause, so it is what picks the\n * sentence. Anything unrecognised keeps the general form rather than guessing.\n */\nfunction writeHint(cause: unknown, toStdout = false): string {\n /*\n The stdout path writes no files, and --out is the flag its user chose not to pass.\n\n Both halves of this sentence were wrong there: \"the files written so far\" named files\n that do not exist, and \"choose another destination with --out\" is advice for a different\n command — the destination is whatever the shell redirected the stream to. Same class as\n the disk-space hint this function replaced, one flag over.\n */\n const preamble = toStdout\n ? 'What reached stdout before it failed is incomplete and should not be used. '\n : 'The files written so far are incomplete and should not be used. ';\n return (\n preamble +\n destinationAdvice(\n cause,\n toStdout ? 'redirect it somewhere else' : 'choose another with --out',\n toStdout ? 'redirect it somewhere shorter' : 'choose a shorter destination with --out',\n 'Part of that path no longer exists; make sure nothing is removing it while the ' +\n 'conversion runs.',\n )\n );\n}\n\n/**\n * The same question asked before anything has been written: the output directory could not be\n * created.\n *\n * Both raisings carried one sentence for every errno — \"Check the path exists and that you\n * have permission to write there\" — which is the shape `writeHint` above was written to\n * replace, in the words of its own docstring: \"Wrong advice is worse than none: it sends\n * someone to check `df` on a disk that is fine, and the thing that is actually wrong stays\n * unexamined.\" A full disk, a read-only volume and a path past the filesystem's length limit\n * all got advice about a path that exists and a permission that is not the problem, two lines\n * under a message where `describeFsError` had already named the cause exactly.\n *\n * No preamble, because nothing has been written yet — which is the only thing that differs\n * between the two, so the sentences themselves are shared rather than copied.\n */\nfunction createHint(cause: unknown): string {\n return destinationAdvice(\n cause,\n 'choose another with --out',\n 'choose a shorter destination with --out',\n 'Part of that path cannot be created — a symbolic link with nothing behind it, or a ' +\n 'mount point that is not mounted; choose another with --out.',\n );\n}\n\n/** The sentence an errno earns, with the two phrases that name where \"somewhere else\" is. */\nfunction destinationAdvice(\n cause: unknown,\n elsewhere: string,\n shorter: string,\n missing: string,\n): string {\n const code = (cause as NodeJS.ErrnoException | null)?.code;\n switch (code) {\n case 'ENOSPC':\n return `The destination is out of space; free some up or ${elsewhere}.`;\n case 'EDQUOT':\n return `You are over your disk quota on this filesystem; ${elsewhere}.`;\n case 'EACCES':\n case 'EPERM':\n return `You do not have permission to write there; ${elsewhere}.`;\n case 'EROFS':\n return `That filesystem is mounted read-only; ${elsewhere}.`;\n case 'EISDIR':\n return `A directory is sitting where that file belongs; remove or rename it, or ${elsewhere}.`;\n /*\n The one errno whose sentence is not the same question at both call sites.\n\n Mid-conversion, a missing component is a directory that went away under a run that had\n already written into it, and \"make sure nothing is removing it\" is the thing to check.\n Before anything is written it is the opposite: the destination's parents are created\n recursively, so reaching ENOENT there means a component that cannot be created — a\n symbolic link with nothing behind it, or a mount point that is not mounted.\n\n $ edf2csv rec.edf --out /mnt/archive/out\n error: Cannot create \"/mnt/archive/out\": part of the path does not exist.\n Part of that path no longer exists; make sure nothing is removing it\n while the conversion runs.\n\n Nothing is removing it and nothing was there. `createHint`'s own docstring says the\n preamble \"is the only thing that differs between the two, so the sentences themselves\n are shared rather than copied\" — and this is the sentence where that was not so.\n */\n case 'ENOENT':\n return missing;\n case 'ENAMETOOLONG':\n return `That path is longer than the filesystem allows; ${shorter}.`;\n case 'EMFILE':\n case 'ENFILE':\n return `Too many files are open; a recording with many sampling rates opens one output file per rate, so --channels narrows it.`;\n case 'EPIPE':\n return 'Whatever was reading the output closed it before the conversion finished.';\n default:\n return 'Check the destination and run the conversion again.';\n }\n}\n\n/**\n * Confirms that everything handed to a file-backed stdout actually arrived.\n *\n * `edf2csv rec.edf --stdout > out.csv` onto a volume that the output very nearly fills lost\n * the tail in silence: 94,977 of 102,400 rows on disk, the file ending mid-row, stderr\n * announcing \"Wrote 102,400 rows to stdout.\" and the process exiting 0. The same recording\n * onto the same volume through `--out` fails correctly, which is what gives it away.\n *\n * POSIX `write` returns a short count rather than an error when the disk fills partway\n * through a single call, and only the NEXT write raises ENOSPC. `--out` always has a next\n * write — channels.csv and metadata.json come after — so it always finds out. `--stdout`\n * has nothing after it, and when fd 1 is a regular file Node's stdout is a SyncWriteStream\n * whose `_write` discards the byte count `writeSync` returns, so nothing is raised at all.\n * No error means no `#failure`, so checking that alone would not have caught this.\n *\n * What can be checked is the descriptor: how much it grew against how much it was given.\n * Only for a regular file — a pipe, a terminal or a socket has no size to compare, and on\n * those a short write cannot go unreported this way. Appending (`>>`) is fine, since the\n * starting size is taken before anything is written.\n *\n * Exported so `--info` can use the same audit a `--stdout` conversion does.\n *\n * `--info` wrote its description with `process.stdout.write` and looked at nothing: redirected\n * into a full filesystem it produced a zero-byte file and exited 0, so `edf2csv rec.edf --info\n * > desc.txt` reported success over nothing at all. A 900-channel recording's description is\n * 58 KB, which is not a size a destination is guaranteed to have.\n */\nexport function auditStdout(): { count: (bytes: number) => void; verify: () => void } | null {\n let startSize: number;\n try {\n const info = fstatSync(1);\n if (!info.isFile()) return null;\n startSize = info.size;\n } catch {\n // No usable descriptor to audit; the conversion is not the place to complain about it.\n return null;\n }\n\n let expected = 0;\n return {\n count: (bytes: number): void => {\n expected += bytes;\n },\n verify: (): void => {\n let landed: number;\n try {\n landed = fstatSync(1).size - startSize;\n } catch {\n return;\n }\n if (landed >= expected) return;\n\n /*\n This audit exists for the failure nothing else reports: a write that is accepted, and\n silently truncated. When the stream itself raised an error there is nothing left for\n it to add, and what it added was a second `error:` line denying the first —\n\n error: Writing to stdout failed: ENOSPC: no space left on device, write\n error: Writing to stdout failed: 58900 of 58900 bytes did not reach the destination,\n which stopped accepting them part way through.\n ... nothing after it raised an error because there was nothing after it.\n\n one failure, reported twice, the second of the two wrong about it.\n */\n if (process.stdout.errored) return;\n\n /*\n Nothing landing at all is not a short write, and was described as one.\n\n `--info > desc.txt` onto a full filesystem left a zero-byte file, and this said the\n destination \"stopped accepting them part way through\" and that \"what is there ends\n mid-row\" — of a file with nothing in it and no rows in it.\n */\n if (landed === 0) {\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to stdout failed: none of the ${counted(expected, 'byte')} reached the destination.`,\n 'The destination is almost certainly out of space. Nothing was written, so there ' +\n 'is nothing there to discard.',\n );\n }\n throw new ConversionError(\n 'WRITE_FAILED',\n // The pair a reader subtracts by eye, so both go through the grouping — the rule\n // 0.8.5 set and reached only as far as the messages already built with `counted`.\n `Writing to stdout failed: ${grouped(expected - landed)} of ` +\n `${counted(expected, 'byte')} did not reach the destination, which stopped ` +\n `accepting them part way through.`,\n 'What is there ends mid-row and should not be used. The destination is almost ' +\n 'certainly out of space — a short write is how a filesystem reports filling up ' +\n 'mid-write, and nothing after it raised an error because there was nothing after it.',\n );\n },\n };\n}\n\n/**\n * Why `--stdout` cannot take this recording, or null when it can.\n *\n * Lifted out of the conversion so `--info` can ask the same question. It was not asking:\n * `--info --stdout` on a three-rate recording predicted \"Would write 1,155 rows, roughly\n * 22.2 KB\" and said the channels \"are written to one file per rate\" — for a command that\n * refuses to run, writes nothing, and names no files. `--info` exists to say what a\n * conversion will do, and this is one of the things it does.\n *\n * Reported by `--info` as a warning rather than a refusal, for the reason 0.5.51 gives about\n * the destination guards: `--info` writes nothing, so a rule about what the output would be\n * has no business stopping it from describing the recording — and being told the command\n * will not work is exactly what you asked.\n */\nexport function stdoutRefusal(file: EdfFile, plan: ConversionPlan): ConversionError | null {\n if (!plan.writeSignals) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n '--stdout has no signal data to write because --annotations-only was given.',\n 'Drop one of the two flags.',\n );\n }\n\n /*\n No table at all is its own answer, and neither layout gave it.\n\n A recording with no signal channels — one holding only EDF+ annotations — produced\n zero rate groups. The wide layout then said \"--stdout needs exactly one table, but\n this recording produces 0, one for each sampling rate its channels use ()\", with an\n empty parenthetical, advice to narrow to one of no rates, and advice to use\n `--layout long` — which wrote zero bytes to stdout, not even a header row, and exited\n 0 while warning that \"the signal files hold their headers and no data\". There were no\n files and there was no header. The one path that was right about this is\n `--annotations-only`, which refuses outright, and this is the same situation reached\n by a different route.\n */\n if (plan.groups.length === 0) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n file.dataSignals.length === 0\n ? `--stdout has no signal data to write: this recording has no signal channels, ` +\n `only ${file.header.isBdf ? 'BDF+' : 'EDF+'} annotations.`\n : '--stdout has no signal data to write: nothing was selected that carries samples.',\n file.dataSignals.length === 0\n ? /*\n Named the way that conversion would name it, since `--gzip` is already on the\n command line this is refusing. This hint is nothing but a command to run, and the\n command it names writes `annotations.csv.gz` — so a reader who follows it finds\n nothing under the name they were given. Same fault 0.8.48 fixed for the warnings,\n in the one refusal that names a file.\n */\n `Convert to a directory to get its ${outputCsvName('annotations', plan.gzip)}, or ` +\n `drop --stdout.`\n : 'Check --channels and the requested window, or convert to a directory instead.',\n );\n }\n\n // The long layout is one table whatever the rates are, so it has nothing to refuse.\n if (plan.layout !== 'long' && plan.groups.length !== 1) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n // Naming the rates is the point: the hint says to narrow the selection, and this\n // is what there is to narrow it to. The parenthetical used to repeat the count\n // that had just been given — \"produces 3 (its channels use 3 different sampling\n // rates)\" — which told nobody anything they could act on.\n `--stdout needs exactly one table, but this recording produces ${grouped(plan.groups.length)}, ` +\n `one for each sampling rate its channels use ` +\n `(${listed(formatRates(plan.groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n 'Narrow it to one rate with --channels, write --layout long to get them all in ' +\n 'one table, or convert to a directory instead.',\n );\n }\n return null;\n}\n\n\n\n/**\n * Asked for signal data and given none to put in a file.\n *\n * Every channel selected carries zero samples per record, so there is no table to make —\n * `edf2csv rec.edf --channels unused` writes channels.csv and metadata.json and no signals.csv\n * at all. The NO_SAMPLES warning explains the channel; nothing explained the missing file, and\n * the documentation says signals.csv is written unless --annotations-only was passed. Someone\n * looking for it should be told where it went.\n *\n * There are two ways to arrive with no groups, and one wording is only true of one of them. A\n * recording that holds nothing but EDF+ annotations has no channel that could have been\n * selected, its channels.csv is a header row and nothing else, and no channel of it carries\n * samples — so \"every channel selected\", \"channels.csv still describes them\" and \"which\n * channels do carry samples\" were three false statements in one warning, printed under a\n * warning that had just said the file has no signal channels.\n *\n * Exported, and worded in the present tense, so `--info` can raise the same one. It was built\n * inline here, which meant the one mode whose purpose is to say what a conversion will do said\n * nothing about the file that conversion would not write: `--info --strict` on a recording of\n * nothing but annotations reported one warning where converting it reported two, and\n * `--info --json` carried the shorter list to whatever reads it. Everything the answer depends\n * on is in the file and the plan, both of which `--info` already has.\n */\nexport function noSignalFile(file: EdfFile, plan: ConversionPlan): Diagnostic | null {\n if (!plan.writeSignals || plan.groups.length > 0) return null;\n const noChannelsAtAll = file.dataSignals.length === 0;\n return {\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: noChannelsAtAll\n ? 'No signal file is written: there is no signal data in this recording to put in one.'\n : 'No signal file is written: every channel selected carries zero samples per data ' +\n 'record, so there is nothing to put in one.',\n // Named as they will be written. `plan.gzip` is on the plan because `--info` named\n // `annotations.csv` for a run that wrote `annotations.csv.gz`; these two sentences are\n // about the same pair of files and were still naming the uncompressed ones.\n hint: noChannelsAtAll\n ? `${outputCsvName('annotations', plan.gzip)} holds whatever events it carries. ` +\n `${outputCsvName('channels', plan.gzip)} lists signal channels, so it has none to list.`\n : // `--info` raises this too, since 0.7.84, and was told to run itself to answer it.\n // Naming where the number lives works in both modes; naming a command works in one.\n 'Nothing about them is lost: every channel\\'s samples per record is in the channel ' +\n `table --info prints, and in the ${outputCsvName('channels', plan.gzip)} a conversion writes.`,\n };\n}\n\n/**\n * The signal table an `--annotations-only` run does not write, taken out of the hints about it.\n *\n * That mode writes the event list and nothing else — no signal files at all — and four hints\n * about record timing describe the rows of one. On a recording whose records run backwards it\n * printed both of these over an annotations.csv holding its header and no rows:\n *\n * warning: This is a discontinuous (EDF+D) recording: its data records are not\n * contiguous in time.\n * Each row carries its true recording time, so gaps stay visible instead of\n * being closed.\n * warning: 2 data records start earlier than the record before them.\n * Rows are written in file order, so the time column will not increase\n * monotonically.\n *\n * There are no rows and no time column. The facts above the hints are about the recording and\n * stay; what changes is the sentence describing what the conversion will do with them, which\n * is the same surgery `withTimingPromiseKept` does to the first of these when the record\n * starts cannot be derived.\n *\n * `writesSignals` rather than the option, because the plan is what settles it.\n */\nexport function withSignalTableUnwritten(\n diagnostics: readonly Diagnostic[],\n writesSignals: boolean,\n gzip: boolean,\n): Diagnostic[] {\n if (writesSignals) return [...diagnostics];\n /*\n The sidecars named here are named as the run writes them.\n\n These sentences send the reader to the one file an `--annotations-only` run does write, and\n said `channels.csv` to a `--gzip` run that writes `channels.csv.gz` — five hints pointing at\n a name that is not in the directory, one of them beside the `_ch` collision hint that 0.8.48\n had already taught to say `channels.csv.gz`. So a single run named both spellings, and only\n the second one existed.\n */\n const channelsFile = outputCsvName('channels', gzip);\n const annotationsFile = outputCsvName('annotations', gzip);\n return diagnostics.map((diagnostic) => {\n /*\n And the three calibration warnings, whose hints are about cells.\n\n Each describes what a conversion does with a channel whose header cannot map cleanly —\n leaves the cell empty, fills it with the one value the mapping has, keeps the inversion —\n and `--annotations-only` converts no samples, so none of it happens:\n\n warning: Signal 0 (\"flat\") has digital minimum equal to digital maximum (0), so its\n values cannot be scaled.\n Its cells are left empty rather than filled with a value the header cannot\n justify.\n\n What is wrong with the header is still worth saying, and channels.csv — which that run\n does write — still carries the calibration. One message also ends in the conversion:\n \"so every sample converts to the same value\", over a run converting none.\n */\n /*\n And the two that say where a channel's name lands, which is signals.csv in both layouts\n and neither of them here.\n\n `--annotations-only` writes no signal table at all, so the name reaches exactly one\n place: channels.csv's `column` cell, which that run does write — control bytes, `_ch`\n suffix and all. Both sentences sent the reader to a file that is not there:\n\n warning: Signal 0's label and unit contain 2 control characters (\\x1b), which will\n appear as the channel's name in signals.csv and in channels.csv's unit\n cell in any conversion that writes one ...\n warning: 2 signals share the label \"T8-P8\" (positions #0, #1).\n Their names are suffixed with the signal number so they stay\n distinguishable: a column name each in the wide layout, and a distinct\n value in the channel column under --layout long.\n\n Neither layout happens. The second sentence offers a choice between two files that are\n not written.\n */\n if (diagnostic.code === 'NONPRINTABLE_LABEL') {\n return {\n ...diagnostic,\n message: diagnostic.message\n .replace(\n \"as the channel's name in signals.csv\",\n `as the channel's name in ${channelsFile}'s column cell`,\n )\n // Matched on the name the header diagnostic used, which the line above has not\n // touched: it renames the first mention, and this collapses the second.\n .replace(\"column cell and in channels.csv's\", 'column cell and in its')\n .replace(' in any conversion that writes one', ''),\n };\n }\n if (\n diagnostic.code === 'DUPLICATE_LABEL' &&\n diagnostic.hint?.startsWith('Their names are suffixed')\n ) {\n return {\n ...diagnostic,\n hint:\n 'Their names are suffixed with the signal number so they stay distinguishable. ' +\n '--annotations-only writes no signal table, so the suffixed names appear only in ' +\n `${channelsFile}'s column cells.`,\n };\n }\n if (diagnostic.code === 'DEGENERATE_DIGITAL_RANGE') {\n return {\n ...diagnostic,\n hint:\n 'No samples are converted with --annotations-only, so there are no cells to leave ' +\n `empty. ${channelsFile} still records the digital range the header gives.`,\n };\n }\n if (diagnostic.code === 'DEGENERATE_PHYSICAL_RANGE') {\n return {\n ...diagnostic,\n message: diagnostic.message.replace(\n 'so every sample converts to the same value.',\n 'so every sample would convert to the same value.',\n ),\n hint:\n `No samples are converted with --annotations-only. ${channelsFile} still records ` +\n 'the calibration, one point wide.',\n };\n }\n if (diagnostic.code === 'INVERTED_PHYSICAL_RANGE') {\n return {\n ...diagnostic,\n hint:\n `No samples are converted with --annotations-only. ${channelsFile} records the ` +\n 'physical minimum and maximum in the order the header gives them, inversion included.',\n };\n }\n if (diagnostic.code !== 'DISCONTINUOUS' || diagnostic.hint === undefined) return diagnostic;\n if (diagnostic.hint.startsWith('Each row carries its true recording time')) {\n return {\n ...diagnostic,\n hint:\n '--annotations-only writes no signal rows, so nothing here is timed from the ' +\n `records. ${annotationsFile} carries each event's own onset, and the record it ` +\n 'came from in record_index.',\n };\n }\n if (diagnostic.hint.startsWith('Rows are written in file order')) {\n return {\n ...diagnostic,\n hint:\n '--annotations-only writes no signal rows, so no time column is affected. ' +\n `${annotationsFile}'s record_index still names the record each event came from.`,\n };\n }\n if (diagnostic.hint.startsWith('Sample times are written from zero')) {\n return {\n ...diagnostic,\n hint: '--annotations-only writes no signal rows, so no sample times are written at all.',\n };\n }\n return diagnostic;\n });\n}\n\n/**\n * The sidecar files a `--stdout` run does not write, taken out of the sentences about them.\n *\n * `--stdout` puts one table on the stream and writes nothing else — \"No sidecar files are\n * written\", as `ConvertOptions` puts it. Two diagnostics raised before the destination is\n * known end by pointing at one of those files:\n *\n * warning: The header's start date and time (\"XX.XX.XX\" and \"YY.YY.YY\") are not a date\n * and a time, so the recording has no start instant.\n * ... and metadata.json records start_datetime_local as null.\n *\n * warning: This recording's timekeeping annotations place it 1e17s from its own start\n * date ...\n * ... Add the onsets in annotations.csv to recover absolute times.\n *\n * Neither file exists after such a run. The second is advice a reader can follow into an\n * empty directory — there is no directory.\n *\n * Amended where the answer is, the same way `withTimingPromiseKept` rewrites a `DISCONTINUOUS`\n * hint the parser could not have known was false, and `withoutFileRateWarning` drops a header\n * diagnostic the plan supersedes. A conversion to a directory keeps every word.\n */\nexport function withSidecarsNamed(\n diagnostics: readonly Diagnostic[],\n { toStdout, gzip }: { toStdout: boolean; gzip: boolean },\n): Diagnostic[] {\n if (!toStdout && !gzip) return [...diagnostics];\n const channels = outputCsvName('channels', gzip);\n const annotations = outputCsvName('annotations', gzip);\n return diagnostics.map((diagnostic) => {\n /*\n The last message of this kind, and the one that names its files mid-sentence.\n\n `NONPRINTABLE_LABEL` says where an invisible byte lands — the channel's name in\n signals.csv, a cell of channels.csv — and `--gzip` writes neither of those names. It was\n left out of 0.8.48 because the file is named inside a sentence assembled from which of\n the four header fields carry bytes, and out of 0.8.54 for the same reason. Renaming is\n all it needs, once the sentence is settled: this runs after `withSignalTableUnwritten`\n has had its say about the layout, so whichever file the sentence ended up naming is the\n one that gets the suffix.\n */\n /*\n And the same sentence under `--stdout`, where the signal table has no name at all.\n\n The `channels.csv` half of it has been hedged since 0.8.52 — \"in the channels.csv of any\n conversion that writes one\", which is true of a run that writes none. The half naming\n `signals.csv` was not, and `--stdout` is the mode this warning matters most in: it is\n the one that really does print the CSV to a terminal, which is what the hint two lines\n under it warns about.\n\n $ edf2csv control-labels.edf --stdout | less\n warning: Signal 1's label contains 1 control character (\\x07), which will appear as\n the channel's name in signals.csv, exactly as the header has it.\n\n There is no signals.csv. The byte is in the header row going past on the stream, which\n is where the reader is looking.\n */\n if ((gzip || toStdout) && diagnostic.code === 'NONPRINTABLE_LABEL') {\n return {\n ...diagnostic,\n message: diagnostic.message\n // Past a name the pass above already suffixed: `channels.csv.gz` contains\n // `channels.csv`, and a second rename made it `channels.csv.gz.gz`.\n .replace(\n /\\bsignals\\.csv\\b(?!\\.gz)/gu,\n toStdout ? 'the CSV on stdout' : outputCsvName('signals', true),\n )\n .replace(/\\bchannels\\.csv\\b(?!\\.gz)/gu, channels),\n };\n }\n /*\n The header's own `NO_SAMPLES`, which names the file that describes the channel it is\n about — and under `--stdout` there is no such file, so the reassurance is empty:\n\n $ edf2csv one-empty-channel.edf --stdout > rows.csv\n warning: Signal 1 (\"unused\") carries no samples at all (0 per data record).\n It is described in channels.csv but left out of the converted data.\n\n Under `--gzip` the file is there under another name. Both are settled here.\n */\n if (diagnostic.code === 'NO_SAMPLES' && diagnostic.hint?.startsWith('It is described in')) {\n return {\n ...diagnostic,\n hint: toStdout\n ? 'It is left out of the converted data, and --stdout writes no channels.csv to ' +\n 'describe it in — convert to a directory for that.'\n : `It is described in ${channels} but left out of the converted data.`,\n };\n }\n /*\n The collision hint, whose whole advice is a file to look the channel up in.\n\n A duplicate label, or a label that collides with another channel's `_ch` suffix, renames\n the column — and the sentence saying so ends by sending the reader to channels.csv for\n the channel's `signal_index`, since the renamed name is the only one in the table:\n\n $ edf2csv montage.edf --stdout > rows.csv\n warning: Signal 2 is labelled \"T8_ch0\", which is also the column name another\n channel's \"_ch\" suffix produces, so its column is \"T8_ch0_ch2\".\n Column names are unique; look this channel up in channels.csv by its\n signal_index.\n\n There is no channels.csv, and the renamed column is sitting in the header row on stdout\n with nothing to map it back. Under `--gzip` it was worse: `channels.csv.gz`, a\n compressed file no `--stdout` run writes either.\n\n Answered the way the two hints above answer it — the file is named, so that the reader\n knows what they are missing, and the command that produces it is named too.\n */\n if (toStdout && diagnostic.code === 'DUPLICATE_LABEL' && diagnostic.hint?.includes('look this channel up in')) {\n // \"Channel names\" under --layout long, where the name lands in the channel column;\n // \"Column names\" in the wide layout. Both are true of the stream, so the half of the\n // sentence that is about the rename is kept as the plan wrote it.\n const unique = diagnostic.hint.startsWith('Channel names') ? 'Channel names' : 'Column names';\n return {\n ...diagnostic,\n hint:\n `${unique} are unique, and --stdout writes no channels.csv to look this channel ` +\n `up in by its signal_index — convert to a directory for that.`,\n };\n }\n if (\n toStdout &&\n diagnostic.code === 'START_TIME_UNREADABLE' &&\n diagnostic.hint?.includes('metadata.json records')\n ) {\n return {\n ...diagnostic,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and --stdout writes no ' +\n 'metadata.json to record start_datetime_local as null in.',\n };\n }\n if (\n diagnostic.code === 'DISCONTINUOUS' &&\n diagnostic.hint?.includes('onsets in annotations.csv')\n ) {\n return {\n ...diagnostic,\n hint: toStdout\n ? 'Sample times are written from zero instead, so every row is present and the ' +\n 'column increases. The onsets that recover absolute times are in the annotation ' +\n 'channel; --stdout writes no annotations.csv, so convert to a directory for them.'\n : 'Sample times are written from zero instead, so every row is present and the ' +\n `column increases. Add the onsets in ${annotations} to recover absolute times if ` +\n 'you need them.',\n };\n }\n return diagnostic;\n });\n}\n\n/**\n * `--annotations-only` on a recording that has no annotations.\n *\n * Exported and present-tense for the same reason `noSignalFile` above is. `--info` prints an\n * accurate line about it in the report body — \"and no annotations.csv either, since this\n * recording has no annotation channel\" — and raised nothing, so the one mode whose purpose is\n * to say what a conversion will do carried a shorter warning list than the conversion did:\n * `--info --json --annotations-only` on a plain EDF file listed no warning where converting\n * lists one, and `--info --strict`, which cli-reference.md recommends for screening a folder\n * before converting it, exited 0 where the conversion exits 1. Same defect as the one 0.7.84\n * closed for `NO_SAMPLES`, one flag over; both halves of the answer are in the file and the\n * options, which `--info` already has.\n */\nexport function noAnnotations(file: EdfFile, options: ConvertOptions): Diagnostic | null {\n if (options.annotationsOnly !== true || file.annotationSignals.length > 0) return null;\n return {\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message:\n '--annotations-only was requested but this recording has no annotation channel, ' +\n 'so there are no events to export.',\n /*\n The format the file is in, rather than the one most files are in.\n\n \"Plain EDF files carry no annotations\" was said to every recording that reaches here,\n and two kinds of file reach it that are not plain EDF. A BioSemi `.bdf` was told about a\n format it is not in — this tool tells EDF and BDF apart everywhere else, down to the\n `BDF Annotations` label the specification gives the channel.\n\n And a file whose header says `EDF+C` while carrying no annotation channel at all is not\n a file for which having no events is ordinary: EDF+ keeps its events in that channel and\n is required to have one. The sentence read as reassurance about a file that is malformed.\n */\n hint: file.header.isEdfPlus\n ? `The header marks this ${describeFormat(file.header)}, and that format keeps its ` +\n `events in an annotation channel this file does not have. Convert without ` +\n `--annotations-only to get the signals.`\n : `Plain ${file.header.isBdf ? 'BDF' : 'EDF'} files carry no annotations. Convert ` +\n `without --annotations-only to get the signals.`,\n };\n}\n\n/**\n * `--annotations-only` that wrote no events, and why.\n *\n * The two causes are told apart because the answers are different. A channel holding nothing\n * but timekeeping entries has nothing to export and never will; a window that excluded every\n * event is a thing the caller can change, and the commonest reason is reading the window off\n * a clock the recording does not use — `--start` and `--end` are on the recording's own,\n * which `--info` prints as \"Timed from\".\n *\n * `NO_ANNOTATIONS` rather than a new code, since it is the same statement its other raising\n * makes — there are no events to export — about the same flag, and a code is matched on by\n * scripts that should not have to learn a second one for the same fact.\n */\nexport function emptyAnnotations(\n total: number,\n window: { from: number; to: number },\n gzip = false,\n): Diagnostic {\n const windowed = (window.from !== -Infinity || window.to !== Infinity) && total > 0;\n return {\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message: windowed\n ? `None of this recording's ${counted(total, 'event')} fall inside the requested ` +\n `window, so ${outputCsvName('annotations', gzip)} holds its header and no rows.`\n : `This recording's annotation channel carries no events, so ` +\n `${outputCsvName('annotations', gzip)} holds its header and no rows.`,\n hint: windowed\n ? '--start and --end are read on the recording\\'s own clock, which --info prints as ' +\n '\"Timed from\", and an event is kept when its onset falls inside the window.'\n : 'The channel holds only the timekeeping entries that say where each data record ' +\n 'sits, and those are never exported.',\n };\n}\n\n/** Raised when the input moved while it was being read. See where it is pushed. */\nfunction inputChanged(hadChecksum: boolean): Diagnostic {\n return {\n code: 'INPUT_CHANGED',\n severity: 'warning',\n message:\n 'The input changed while it was being converted, so this output covers the file as ' +\n 'it was when the conversion started, not as it is now.',\n hint: hadChecksum\n ? 'No checksum was recorded: the bytes that were converted are no longer there to ' +\n 'hash. Convert again once the recording is finished.'\n : 'Convert again once the recording is finished to pick up the rest.',\n };\n}\n\nasync function writeMetadata(\n outputDir: string,\n inputPath: string,\n file: EdfFile,\n plan: ConversionPlan,\n written: readonly WrittenFile[],\n annotationCount: number,\n /** Hash of the bytes that were converted, or null when it could not be vouched for. */\n checksum: string | null,\n /** Whether the record start times could be read; see withTimingPromiseKept. */\n timedFromRecords: boolean,\n /** Whether each CSV starts with a byte order mark; see the `bom` field below. */\n bom: boolean,\n): Promise<void> {\n const { header } = file;\n\n /*\n The file as it was when it was opened, not as it is now.\n\n Both of these used to come from re-opening the path once the CSVs were written, which\n describes whatever answers to that name by then rather than what was converted. A\n recording still being written grew from 2,000 records to 3,000 mid-conversion and\n metadata.json recorded `data_records: 2000` — correct, the CSV holds 2,000 — beside the\n byte count and SHA-256 of the 3,000-record file. The two halves of one provenance record\n described two different files, and the checksum covered bytes nobody had converted.\n Replacing the file at that path did the same thing more thoroughly.\n\n `file.fileSize` is the number every record count and window in this output was derived\n from, and the hash is taken over exactly those bytes through the descriptor already open\n on them, so the record describes one file throughout.\n */\n const metadata = {\n tool: { name: 'edf2csv', version: TOOL_VERSION },\n source: {\n path: path.resolve(inputPath),\n bytes: file.fileSize,\n modified: new Date(file.modifiedAtOpenMs).toISOString(),\n sha256: checksum,\n },\n recording: {\n format: describeFormat(header),\n version: header.version,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n // Zone-less on purpose: EDF records local wall-clock digits and no timezone.\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n signal_count: header.signalCount,\n annotation_channels: file.annotationSignals.length,\n },\n conversion: {\n converted_at: new Date().toISOString(),\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n records_converted: [plan.range.startRecord, plan.range.endRecord],\n annotations_written: annotationCount,\n /*\n Which shape the signal table is in, which nothing recorded.\n\n A wide `signals.csv` and a long one are different files with different columns, and\n metadata.json described them identically — so a pipeline handed an output directory\n could not tell from the archive which it had. It matters most for `rate_groups` right\n below: in the wide layout those entries are one per file and their `channels` are that\n file's columns, and in the long layout every entry names the one shared table and its\n `channels` are values in that table's `channel` column. Same array, two readings, and\n no way to know which applied.\n */\n layout: plan.layout,\n /*\n The one option whose effect nothing else in this archive shows.\n\n `layout` is recorded on the stated grounds that \"the two have different columns and\n nothing else in the archive distinguishes them\". `--gzip` distinguishes itself: the\n names in `files` end `.csv.gz`. `--bom` puts three bytes in front of every CSV and\n leaves no other trace — and it is the option that decides whether reading the table\n back works. `csv.ts` sets out why: pandas strips the mark either engine, and Python's\n own `csv.reader` over a plain `open()` does not, nor does `readFileSync(path,\n 'utf8')`, so the first column name comes back as `\\ufefftime_s` and a lookup of\n `time_s` misses. A pipeline reading this document to find out how to open the files\n could not find out the one thing that changes the answer.\n */\n bom,\n files: written.map((f) => ({ name: f.name, rows: f.rows })),\n rate_groups: plan.groups.map((g) => ({\n file: g.fileName,\n sampling_rate_hz: g.rate,\n channels: g.channels.map((c) => c.column),\n decimals: g.channels.map((c) => c.decimals),\n })),\n },\n notes: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timedFromRecords),\n ...plan.diagnostics,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n // metadata.json is only written into a directory, so this is never the stdout case.\n { toStdout: false, gzip: plan.gzip },\n ).map((d) => ({\n code: d.code,\n severity: d.severity,\n message: d.message,\n })),\n };\n\n // Escaped like the two --json documents: metadata.json records the input's path, and a\n // `cat` of it should not be able to reorder the terminal. See escapeJsonText.\n const document = escapeJsonText(JSON.stringify(metadata, null, 2));\n await writeOutputFile(outputDir, 'metadata.json', `${document}\\n`);\n}\n\n\n"]}
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/convert/run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executing a conversion.\n *\n * Everything is written in a single pass over the data records. All rate groups are\n * open at once and fed from the same batch of bytes, so a file is read once no\n * matter how many output tables it produces, and memory stays flat.\n */\n\nimport { createWriteStream, fstatSync } from 'node:fs';\nimport { lstat, mkdir, readdir, stat, writeFile } from 'node:fs/promises';\nimport { finished } from 'node:stream/promises';\nimport type { Writable } from 'node:stream';\nimport { createGzip, gzipSync } from 'node:zlib';\nimport path from 'node:path';\n\nimport type { RecordBatch } from '../edf/reader.js';\nimport { EdfFile } from '../edf/reader.js';\nimport { describeFormat, formatRate, formatRates, formatWallClock, startsFormula } from '../edf/header.js';\nimport type { Diagnostic } from '../edf/errors.js';\nimport { EdfError } from '../edf/errors.js';\nimport type { Annotation } from '../edf/annotations.js';\nimport {\n BufferedLineWriter,\n DEFAULT_FLUSH_THRESHOLD,\n UTF8_BOM,\n csvRow,\n escapeCsvField,\n} from '../format/csv.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { escapeCharacter, escapeJsonText, unprintableIn } from '../format/unprintable.js';\nimport {\n makeSampleFormatter,\n makeTimeFormatter,\n newOffsetBudget,\n newSampleCacheBudget,\n plain,\n} from '../format/number.js';\nimport type { SampleFormatter } from '../format/number.js';\nimport { TIME_COLUMN } from './channels.js';\nimport { assertInputPath } from './options.js';\nimport { buildPlan, outputCsvName, withoutFileRateWarning } from './plan.js';\nimport type { ConversionPlan, PlanOptions, RateGroup } from './plan.js';\nimport { deriveRecordStarts, withTimingPromiseKept } from './timing.js';\nimport { sampleTimeIsInRange, toleranceFor } from './time-range.js';\nimport { VERSION as TOOL_VERSION } from '../version.js';\n\nexport { TOOL_VERSION };\n\nexport type ConversionErrorCode =\n | 'OUTPUT_EXISTS'\n | 'OUTPUT_UNWRITABLE'\n | 'INPUT_OUTPUT_COLLISION'\n | 'INPUT_UNREADABLE'\n | 'UNSUPPORTED_REQUEST'\n | 'CALLBACK_FAILED'\n | 'WRITE_FAILED';\n\n/**\n * Codes that mean the command cannot be carried out as written, rather than that something\n * about the file or the destination went wrong.\n *\n * The distinction is the one the exit codes draw: 1 is \"the file or the destination is the\n * problem\", 2 is \"the command line is the problem\". A caller with a `--stdout` conflict is\n * being told to change the flags — the hints say exactly that — so filing it under 1 sent\n * scripts looking at the disk. Exit 2 already covers checks that need the header first,\n * such as a `--channels` term matching nothing.\n */\nexport const USAGE_ERROR_CODES: ReadonlySet<ConversionErrorCode> = new Set(['UNSUPPORTED_REQUEST']);\n\nexport class ConversionError extends Error {\n readonly code: ConversionErrorCode;\n readonly hint: string | undefined;\n constructor(code: ConversionErrorCode, message: string, hint?: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ConversionError';\n this.code = code;\n this.hint = hint;\n }\n}\n\nexport interface ConvertOptions extends PlanOptions {\n /**\n * Destination directory. Defaults to `defaultOutputDir(inputPath)`: the input's name with\n * its extension replaced by `_csv`, beside the input — `sleep-study.edf` gives\n * `sleep-study_csv`, not `sleep-study`.\n */\n outputDir?: string | undefined;\n /** Overwrite an existing output directory. */\n force?: boolean | undefined;\n /** Record a SHA-256 of the input in metadata.json. Costs one extra read of the file. */\n checksum?: boolean | undefined;\n /**\n * Write the signal CSV to stdout instead of to a directory.\n *\n * Only valid when the conversion produces exactly one signal file. In the default wide\n * layout a mixed-rate recording becomes several tables, and merging them into one stream\n * would mean inventing the samples this tool exists not to invent; `layout: 'long'` gives\n * one table for any recording, so it lifts the restriction. No sidecar files are written.\n */\n toStdout?: boolean | undefined;\n onProgress?: ((progress: ConversionProgress) => void) | undefined;\n}\n\nexport interface ConversionProgress {\n recordsDone: number;\n recordsTotal: number;\n bytesWritten: number;\n}\n\nexport interface WrittenFile {\n name: string;\n rows: number;\n}\n\nexport interface ConvertResult {\n outputDir: string;\n files: WrittenFile[];\n /**\n * True when a `--stdout` reader closed the pipe before the conversion finished.\n *\n * `edf2csv rec.edf --stdout | head -1` is an ordinary thing to type and not a failure, but\n * it is also not a conversion: the row count is rows formatted before the close was\n * noticed, which is neither the recording's total nor what the reader received.\n */\n readerHungUp: boolean;\n annotationCount: number;\n diagnostics: Diagnostic[];\n plan: ConversionPlan;\n file: EdfFile;\n elapsedMs: number;\n}\n\ninterface OpenGroup {\n group: RateGroup;\n writer: BufferedLineWriter;\n formatters: SampleFormatter[];\n formatTime: (recordStart: number, sample: number) => string;\n rows: number;\n /** Resolves once a compressed stream's bytes have reached the file behind it. */\n settled: Promise<void>;\n /** Detaches the compressor's error forwarding from a stream this tool does not own. */\n release: () => void;\n}\n\nexport async function convert(inputPath: string, options: ConvertOptions = {}): Promise<ConvertResult> {\n assertInputPath(inputPath);\n const startedAt = Date.now();\n const file = await EdfFile.open(inputPath);\n\n try {\n /*\n Hashed before a record is read, and only published if the file held still.\n\n A checksum taken afterwards cannot be trusted whichever descriptor it goes through. A\n file overwritten in place keeps its inode, so the open handle sees the new bytes too,\n and the old ones are simply gone — there is nowhere left to read what was converted.\n Taking it first at least means the hash describes the file the header was read from.\n\n What makes it a guarantee rather than a hope is the check at the end: if size or\n modification time moved at any point, the hash is dropped and the run says why. So\n `sha256` present means the file demonstrably did not change while it was read, and a\n recording still being written gets a null and a warning instead of a plausible hash of\n the wrong bytes. (A change reverted within the same modification timestamp would slip\n through; nothing short of copying the input first can close that.)\n */\n const checksumAtOpen = options.checksum === true ? await file.sha256() : null;\n\n // One pass over the annotation channel supplies everything annotation-related:\n // where each record sits in time, and the full event list. It reads the whole\n // file even when a window was requested, because an annotation inside the window\n // may be stored in a record outside it.\n const annotationData =\n file.annotationSignals.length > 0\n ? await file.readAnnotations()\n : { annotations: [] as Annotation[], recordStarts: [] as (number | null)[], malformed: 0 };\n\n const timing = deriveRecordStarts(file, annotationData);\n\n const plan = buildPlan(\n {\n signals: file.header.signals,\n recordDuration: file.header.recordDuration,\n recordCount: file.recordCount,\n hasAnnotationChannel: file.annotationSignals.length > 0,\n recordStarts: timing.starts,\n },\n options,\n );\n plan.diagnostics.push(...timing.diagnostics);\n\n if (options.toStdout === true) {\n const refusal = stdoutRefusal(file, plan);\n if (refusal) throw refusal;\n let readerHungUp = false;\n const written = await writeSignalFiles(file, plan, null, timing.starts, options, (hungUp) => {\n readerHungUp = hungUp;\n });\n if (await file.changedSinceOpen()) plan.diagnostics.push(inputChanged(false));\n return {\n outputDir: '-',\n files: written,\n readerHungUp,\n annotationCount: 0,\n // Over both lists, because the timing warnings are pushed onto the plan's.\n diagnostics: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timing.starts !== null),\n ...plan.diagnostics,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n { toStdout: true, gzip: plan.gzip },\n ),\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n /*\n A destination whose last component is `.` or `..` does not name a directory of its own.\n\n `prepareOutputDir` claims the final component with a single non-recursive mkdir, having\n created its parents recursively — which is what makes two conversions racing for one\n directory safe. `path.dirname(\"out/.\")` is `\"out\"`, so for these the parent step creates\n the destination itself and the claim then asks the filesystem to make `.` inside it,\n which always exists. The result was a refusal naming a directory this same run had just\n made, one line after making it:\n\n edf2csv rec.edf --out ./fresh/.\n error: \"./fresh/.\" already exists.\n Pass --force to overwrite it, or --out to choose a different directory.\n\n Exit 1, nothing converted, and an empty `fresh/` left on disk. `--force` does not help:\n the claim fails the same way whatever it is told, so the path was unusable rather than\n occupied.\n\n Normalised only in that case, so `--out ./converted` keeps the spelling it was given —\n that one is already how the directory is found on disk, which is what `output_dir`\n promises, and rewriting it to `converted` would churn every example for no gain.\n */\n const asked = options.outputDir ?? defaultOutputDir(inputPath);\n const outputDir = /(?:^|[\\\\/])\\.\\.?$/u.test(asked) ? path.normalize(asked) : asked;\n await assertInputDoesNotOverlapOutputs(inputPath, outputDir, file, plan, options);\n await prepareOutputDir(outputDir, options.force === true);\n\n const written: WrittenFile[] = [];\n\n if (plan.writeSignals && plan.groups.length > 0) {\n const signals = await writeSignalFiles(file, plan, outputDir, timing.starts, options);\n written.push(...signals);\n } else {\n const missing = noSignalFile(file, plan);\n if (missing) plan.diagnostics.push(missing);\n }\n\n const noEvents = noAnnotations(file, options);\n if (noEvents) plan.diagnostics.push(noEvents);\n\n let annotationsWritten = 0;\n if (file.annotationSignals.length > 0) {\n const window = requestedAnnotationWindow(options, plan.range.recordingStartSeconds);\n // Reported against the rows that will be written, not against the file; see\n // durationDiagnostics.\n plan.diagnostics.push(...durationDiagnostics(annotationData.annotations, window));\n plan.diagnostics.push(...descriptionDiagnostics(annotationData.annotations, window));\n const result = await writeAnnotationsCsv(\n outputDir,\n annotationData.annotations,\n window,\n options.gzip === true,\n options.bom === true,\n );\n written.push(result);\n annotationsWritten = result.rows;\n /*\n A run whose only table came out empty, which nothing said.\n\n `emptyWindow`'s own docstring states the rule this was breaking: \"everywhere else that\n a request produces nothing this tool says so: a --channels term matching nothing is an\n error, and --annotations-only on a file with no events raises NO_ANNOTATIONS.\" The\n second half is true only of a file with no annotation *channel*. Give the flag a\n recording whose channel carries nothing but timekeeping, or a window its events fall\n outside, and the run writes an annotations.csv holding one header line, prints \"Wrote\n … annotations.csv 0 rows\", exits 0 and passes --strict — which is exactly the shape\n EMPTY_WINDOW exists to refuse on the signal side, arriving by the other route.\n\n Only when there is no signal table either, so this is the whole of what the run\n produced. A windowed conversion of a recording whose events sit elsewhere is\n ordinary and its signals are the point; warning there would fire on most windows of\n most annotated recordings, which is how a warning stops being read.\n */\n if (!plan.writeSignals && annotationsWritten === 0) {\n plan.diagnostics.push(emptyAnnotations(annotationData.annotations.length, window, plan.gzip));\n }\n }\n\n written.push(\n await writeChannelsCsv(outputDir, file, plan, options.gzip === true, options.bom === true),\n );\n\n /*\n The file moved under the conversion. Said out loud, because nothing else shows it.\n\n The CSVs are still correct for the records that were read, and metadata.json still\n describes the file as it was opened — so the record is consistent with the output\n whatever happens here. What stops being true is that the checksum describes the bytes\n that were converted, since an in-place overwrite leaves nowhere to read them from. It\n is dropped rather than guessed at.\n */\n const changed = await file.changedSinceOpen();\n if (changed) plan.diagnostics.push(inputChanged(options.checksum === true));\n\n await writeMetadata(\n outputDir,\n inputPath,\n file,\n plan,\n written,\n annotationsWritten,\n changed ? null : checksumAtOpen,\n timing.starts !== null,\n options.bom === true,\n );\n\n const stale = await findStaleOutput(outputDir, written);\n\n return {\n outputDir,\n files: written,\n readerHungUp: false,\n annotationCount: annotationsWritten,\n // The directory path needs it too: `--gzip` changes the names, and two of these\n // sentences are a file name and nothing else. See withSidecarsNamed.\n diagnostics: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timing.starts !== null),\n ...plan.diagnostics,\n ...stale,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n { toStdout: false, gzip: plan.gzip },\n ),\n plan,\n file,\n elapsedMs: Date.now() - startedAt,\n };\n } finally {\n await file.close();\n }\n}\n\n/*\n Files this tool produces, used to spot leftovers from an earlier conversion.\n\n The rate part has to allow every shape a filename can now take, not just the plain ones:\n\n signals_256hz.csv an integer rate\n signals_12_5hz.csv a fractional rate, decimal point written as an underscore\n signals_1_000e-7hz.csv a rate small enough to need exponent form (0.2.2)\n signals_256hz.csv.gz any of the above, compressed (0.3.0)\n signals_0hz_2.csv a second group whose rate slug collided (0.2.1)\n\n The previous `[\\w.]+hz` matched neither of the last two — `-` and `+` are not word\n characters, and the collision suffix falls after the `hz`. Both were introduced by\n recent changes and both silently stopped being recognised as this tool's own output, so\n leftovers of exactly those kinds went unreported: the one situation the warning exists\n for. Requiring a digit after the underscore keeps a user's own `signals_notes.csv` out.\n\n And `signals_Infinityhz.csv`, which is a name this tool writes and the digit rule then\n excluded. A record duration too small to divide into overflows the rate — `rateSlug` says\n `Infinityhz` and the writer opens that file, with the TIME_RESOLUTION warning beside it\n saying no rows will go in it — so a mixed-rate run of such a file leaves two `signals_*`\n files behind and the next conversion into that directory named one of them:\n\n warning: signals_1e+308hz.csv is left over from an earlier conversion into this\n directory and was not rewritten.\n\n with `signals_Infinityhz.csv` sitting beside it, from the same run, unmentioned. Spelled\n out rather than loosened, so a user's own `signals_notes.csv` stays out for the reason\n above; the check below holds the pattern against every name `rateSlug` can produce.\n*/\nconst OUTPUT_PATTERN =\n /^(signals(_(\\d[\\w.+-]*|Infinity)hz(_\\d+)?)?\\.csv(\\.gz)?|annotations\\.csv(\\.gz)?|channels\\.csv(\\.gz)?|metadata\\.json)$/u;\n\n/**\n * Detect output from a previous run that this one did not replace.\n *\n * `--force` overwrites files but does not empty the directory, so converting a\n * mixed-rate recording and then a single-rate one into the same place leaves\n * `signals_256hz.csv` sitting next to a fresh `signals.csv`. Both look current.\n * Nothing is deleted here — the user is told, and decides.\n */\nasync function findStaleOutput(\n outputDir: string,\n written: readonly WrittenFile[],\n): Promise<Diagnostic[]> {\n const entries = await readdir(outputDir).catch(() => null);\n if (!entries) return [];\n\n // metadata.json is rewritten on every run but is not part of the reported file list.\n const fresh = new Set([...written.map((f) => f.name), 'metadata.json']);\n const stale = entries.filter((name) => OUTPUT_PATTERN.test(name) && !fresh.has(name)).sort();\n if (stale.length === 0) return [];\n\n return [\n {\n code: 'STALE_OUTPUT',\n severity: 'warning',\n /*\n Through `listed`, like every other message that enumerates something this run does not\n control. How many stale files a directory holds is up to the directory, and a\n mixed-rate recording converted into a reused one is exactly how it fills up: 120 old\n `signals_<rate>hz.csv` files produced a single 2,373-character warning line. That is\n the failure `listed` was written for — its own comment quotes the 1,600-character\n version of it — and this was the one message still joining its own list.\n\n The hint said \"Delete them\" whatever the count, so one stale file read \"signals_999hz\n .csv is left over ... Delete them.\"\n */\n message:\n `${listed(stale)} ${stale.length === 1 ? 'is' : 'are'} left over from an earlier ` +\n `conversion into this directory and ${stale.length === 1 ? 'was' : 'were'} not rewritten.`,\n hint:\n `Delete ${stale.length === 1 ? 'it' : 'them'}, or convert into a fresh directory, so ` +\n 'the two runs do not get mixed up.',\n },\n ];\n}\n\nexport function defaultOutputDir(inputPath: string): string {\n /*\n The third export whose only argument is a path, and the one left handing a wrong value on.\n\n `path.basename` refuses a non-string with Node's own text — `The \"path\" argument must be\n of type string. Received type number (5)` — which names a parameter of `path`, not one of\n this function, and reaches a caller who never called `path`. `convert` got this check in\n 0.7.x and `EdfFile.open` in 0.8.9; this is what the api page tells you to call to find out\n where those two would write, so it is reached by exactly the same caller.\n */\n assertInputPath(inputPath);\n const base = path.basename(inputPath, path.extname(inputPath));\n return path.join(path.dirname(inputPath), `${base}_csv`);\n}\n\n/** Never let an output target resolve to the recording being read, even with --force. */\nasync function assertInputDoesNotOverlapOutputs(\n inputPath: string,\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n options: ConvertOptions,\n): Promise<void> {\n /*\n The names this run will actually write, compressed ones included.\n\n The rate files come from the plan and already carry `.csv.gz` under --gzip; the sidecars\n were spelled out here and did not. So a compressed run checked two names it would never\n write and missed the two it would: a recording sitting at <outdir>/channels.csv.gz was\n overwritten by its own conversion, with --force, reported as a success. The same file\n named signals.csv.gz was refused, which is what gives the oversight away.\n */\n const suffix = options.gzip === true ? '.csv.gz' : '.csv';\n const names = new Set(plan.groups.map((group) => group.fileName));\n if (file.annotationSignals.length > 0) names.add(`annotations${suffix}`);\n names.add(`channels${suffix}`);\n names.add('metadata.json');\n\n const inputResolved = path.resolve(inputPath);\n /*\n Tolerant of the input going away, like every other stat in this function.\n\n This one was bare, so a recording renamed or unlinked between `EdfFile.open` and here threw\n Node's own error straight out of `convert()`:\n\n Error: ENOENT: no such file or directory, stat './link.edf'\n\n with no `code` this tool defines and no hint — a sixth error type from a function the API\n page says throws five, on the one path where a file moving under a conversion is the\n documented ordinary case. The descriptor is already open, so the conversion itself is fine;\n what is lost is the inode comparison, and `samePath` below still answers the case that\n matters when the two names are the same name.\n */\n const inputInfo = await stat(inputPath).catch(() => null);\n\n for (const name of names) {\n const target = path.join(outputDir, name);\n const targetResolved = path.resolve(target);\n const targetInfo = await stat(target).catch(() => null);\n const samePath = targetResolved === inputResolved;\n const sameFile =\n targetInfo !== null &&\n inputInfo !== null &&\n targetInfo.dev === inputInfo.dev &&\n targetInfo.ino === inputInfo.ino;\n if (!samePath && !sameFile) continue;\n\n throw new ConversionError(\n 'INPUT_OUTPUT_COLLISION',\n `Output file \"${target}\" is the same file as the input recording.`,\n 'Choose a separate directory with --out. The input was not modified.',\n );\n }\n}\n\nasync function prepareOutputDir(dir: string, force: boolean): Promise<void> {\n /*\n Claim the directory with a single atomic mkdir rather than asking whether it exists\n and then creating it.\n\n Checking first left a window between the two: two conversions started together both\n saw \"not there\", both proceeded, and both opened write streams on the same signals.csv.\n Neither reported anything — both exited 0, having half-written one file between them.\n A non-recursive mkdir cannot do that. Exactly one caller creates the directory; every\n other one gets EEXIST from the filesystem and takes the already-exists path below.\n\n Parents are still created recursively, since --out ./a/b/c should work. Only the final\n component is the claim.\n */\n const parent = path.dirname(dir);\n if (parent && parent !== dir) {\n try {\n await mkdir(parent, { recursive: true });\n } catch (cause: unknown) {\n // A parent that is a regular file surfaces as EEXIST naming the parent, which reads\n // as though the destination already exists rather than as \"you cannot put a\n // directory inside a file\". Name the real obstacle instead of the errno.\n const info = await stat(parent).catch(() => null);\n if (info && !info.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": \"${parent}\" is a file, not a directory.`,\n 'Choose a destination whose parent directories are directories, with --out.',\n );\n }\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n createHint(cause),\n );\n }\n }\n\n let claimed = true;\n try {\n await mkdir(dir);\n } catch (cause: unknown) {\n if ((cause as NodeJS.ErrnoException).code !== 'EEXIST') {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `Cannot create \"${dir}\": ${describeFsError(cause)}.`,\n createHint(cause),\n );\n }\n claimed = false;\n }\n\n if (!claimed) {\n const existing = await stat(dir).catch(() => null);\n\n // --force means \"replace my previous output\", not \"write output files into\n // whatever this happens to be\". Pointing it at a regular file needs saying so.\n if (existing && !existing.isDirectory()) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a file, but the converted data needs a directory.`,\n 'Choose a directory with --out.',\n );\n }\n /*\n A link to nothing, which `stat` cannot see and `mkdir` will not write through.\n\n `stat` follows symbolic links, so a dangling one leaves `existing` null and fell through\n to the sentence below — which is the one claim that is not true of it:\n\n $ edf2csv rec.edf --out link-to-nowhere\n error: \"link-to-nowhere\" already exists.\n Pass --force to overwrite it, or --out to choose a different directory.\n\n Nothing is there. And following that advice made it worse: `--force` reached the writer\n and came back as `Writing to \"link-to-nowhere\" failed: part of the path does not exist.\n The files written so far are incomplete and should not be used` — a conversion failure,\n about files that were never written, advising the reader to make sure nothing is\n removing a directory that never existed.\n\n A broken link in a batch's destination is how this arrives: the run before it wrote into\n a mount that has since gone.\n */\n if (!existing && (await lstat(dir).catch(() => null))) {\n throw new ConversionError(\n 'OUTPUT_UNWRITABLE',\n `\"${dir}\" is a symbolic link to something that does not exist, so nothing can be ` +\n `written there.`,\n 'Remove the link, or choose a directory with --out. --force writes into a directory ' +\n 'that is already there, and a link to nowhere is not one.',\n );\n }\n if (!force) {\n /*\n What `--force` does, rather than what it sounds like it does.\n\n \"Overwrite it\" reads as a claim about the directory, and the neighbouring hint said so\n outright — \"--force replaces a previous output directory\". It does neither. It writes\n into the directory: files of the same name are replaced and everything else is left\n exactly where it was, which is why `STALE_OUTPUT` exists to report what stayed. The\n flag list has described it accurately since it was written — \"write into the output\n directory if it already exists\" — so the one sentence a reader meets at the moment\n they decide whether to pass it was the one contradicting the other two.\n\n The difference is a directory that ends up holding two conversions. Convert a\n mixed-rate recording and then a single-rate one into the same place with `--force`,\n and `signals_256hz.csv` sits beside a fresh `signals.csv` under a channels.csv and a\n metadata.json describing only the second — which is exactly the outcome a reader who\n was told the directory would be replaced does not expect, and does not check for.\n */\n throw new ConversionError(\n 'OUTPUT_EXISTS',\n `\"${dir}\" already exists.`,\n 'Pass --force to write into it, leaving whatever else it holds, or --out to choose ' +\n 'a different directory.',\n );\n }\n }\n}\n\n/**\n * Turn a Node filesystem error into something a person can act on: an errno as a sentence,\n * so Node's own text never reaches the screen.\n *\n * Exported for the CLI's last-resort stdout listener, which is the third and last place a\n * write failure becomes a message and the one that was still printing `ENOSPC: no space left\n * on device, write`.\n */\nexport function describeFsError(cause: unknown): string {\n const code = (cause as NodeJS.ErrnoException | undefined)?.code;\n if (code === 'EACCES' || code === 'EPERM') return 'permission denied';\n if (code === 'ENOSPC') return 'the disk is full';\n // The one that actually happens on a shared filesystem, and the one this list left out.\n if (code === 'EDQUOT') return 'you are over your disk quota on this filesystem';\n if (code === 'ENOTDIR') return 'part of the path is a file, not a directory';\n if (code === 'EROFS') return 'the filesystem is read-only';\n if (code === 'ENAMETOOLONG') return 'the path is too long';\n /*\n The two `writeHint` knew and this did not, which showed once the write failures started\n coming through here: `writeHint` reads the same errno off the same error and has a\n sentence for eleven of them, this had seven, and the gap was the two that only a write\n reaches. `EISDIR` is a directory sitting where an output file belongs, which is the\n commonest way to make one of these happen on purpose.\n\n `EPIPE` is deliberately not here. The writer treats a reader hanging up as the ordinary\n end of a pipeline rather than as a failure, so it never becomes a message.\n */\n if (code === 'EISDIR') return 'a directory is sitting there already';\n if (code === 'EMFILE' || code === 'ENFILE') return 'too many files are open';\n /*\n The one this list could not describe, and the recursive mkdir above is what makes it\n surprising: every parent is created on the way, so \"no such file or directory\" is not a\n parent that was missing. It is a component that exists and leads nowhere — a symbolic\n link with no target is the way to get one — or a directory removed by something else\n between the two calls.\n\n Missing it meant the fallback ran, which is Node's own text, complete with the internal\n call that raised it:\n\n error: Cannot create \"dangle/x\": ENOENT: no such file or directory, mkdir 'dangle'.\n Check the path exists and that you have permission to write there.\n\n Keeping that off the screen is the whole purpose of the six lines above it.\n */\n if (code === 'ENOENT') return 'part of the path does not exist';\n return cause instanceof Error ? cause.message : String(cause);\n}\n\nasync function writeSignalFiles(\n file: EdfFile,\n plan: ConversionPlan,\n outputDir: string | null,\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n onHangUp?: (hungUp: boolean) => void,\n): Promise<WrittenFile[]> {\n // One budget for every table in this conversion, not one per table: see OffsetBudget.\n const offsets = newOffsetBudget();\n // Likewise one for every channel, not one per channel: see SampleCacheBudget.\n const sampleCaches = newSampleCacheBudget();\n // Only the stdout path needs this; --out finds a full disk on its own, because it always\n // has another file to write afterwards. See auditStdout.\n const audit = outputDir === null ? auditStdout() : null;\n /*\n In the long layout every group writes into one table, so they share one stream. Opening\n a stream per group on the same path is what the rate-slug collision fix in groupByRate\n was about: two writers on one file interleave rows under a header naming one of them.\n */\n let shared: {\n stream: Writable;\n settled: Promise<void>;\n release: () => void;\n target: Writable;\n writer: BufferedLineWriter;\n } | null = null;\n\n /*\n One buffer's worth of memory for the conversion, not one per table.\n\n Every group had its own writer at the default 1 MiB threshold, so pending output was\n group count × 1 MiB before anything drained. A 6.5 MB recording with 40 sampling rates\n — the header allows thousands of channels, and a research montage really does mix a\n dozen rates — died with a raw V8 heap out-of-memory and exit 134 under a 96 MB cap,\n while the site advertises 48 MB. The recording is small; it is the fan-out that is not.\n\n Split evenly with a floor, so the single-rate case, which is nearly every recording,\n keeps exactly the buffer it always had, and forty tables cost one megabyte between them\n rather than forty. The floor is what the sum is bounded by rather than the split: past\n 128 groups every table takes the 8 KiB minimum, so the total grows again from there.\n The long layout shares one writer already and is unaffected either way.\n */\n const MIN_FLUSH_THRESHOLD = 8 * 1024;\n const flushThreshold = Math.max(\n MIN_FLUSH_THRESHOLD,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n /*\n The stream's own buffer is the other half of the same sum.\n\n 0.5.6 shared the line-buffer budget and left `createWriteStream` at its 64 KiB default,\n which is per stream: 200 rate groups meant 12.8 MB of stream buffer on top of 12.8 MB of\n line buffer, and an 855 KB recording still died at a 48 MB cap. Shared the same way, with\n a floor that keeps an ordinary conversion writing in useful-sized pieces.\n */\n const streamBuffer = Math.max(\n 16 * 1024,\n Math.floor(DEFAULT_FLUSH_THRESHOLD / Math.max(1, plan.groups.length)),\n );\n\n const open: OpenGroup[] = plan.groups.map((group, groupIndex) => {\n // A null directory means the single table goes to stdout. process.stdout is already a\n // writable stream, so the same buffered writer and backpressure handling apply.\n const target =\n shared?.target ??\n (outputDir === null\n ? process.stdout\n : createWriteStream(path.join(outputDir, group.fileName), {\n highWaterMark: streamBuffer,\n }));\n const { stream, settled, release } = shared ?? compressed(target, options.gzip === true);\n /*\n Under --gzip the writer feeds the compressor, so its byte count is the CSV before\n compression and says nothing about what reached the descriptor. The compressor's own\n output is what stdout is handed, so that is what is counted. `pipe` uses a 'data'\n listener of its own and a second one is delivered the same chunks.\n\n Once per stream, which in the long layout is once for all the groups. Attaching per\n group put N listeners on the one shared compressor, so every chunk was counted N\n times: `--stdout --layout long --gzip` on a 40-rate recording claimed 622,240 of\n 622,240 bytes where 15,556 had been written, failed with a disk-full error over a\n perfectly good file, and printed Node's MaxListenersExceededWarning to stderr on the\n way past ten. 0.5.4 fixed the same arithmetic in the uncompressed branch and left\n this one, because the uncompressed branch is where the count is a sum and this one is\n where it is a subscription.\n */\n if (audit && stream !== target && !shared) {\n stream.on('data', (chunk: Buffer) => audit.count(chunk.length));\n }\n /*\n One writer, not one per group, when the table is shared. Separate writers over one\n stream each hold their own buffer and flush on their own schedule, so the rows would\n reach the file in whatever order the buffers filled — which is not the order they\n were produced in, and the long layout's whole claim is that its rows are in time\n order.\n */\n const writer = shared?.writer ?? new BufferedLineWriter(stream, flushThreshold);\n if (plan.layout === 'long' && !shared) shared = { stream, settled, release, target, writer };\n\n // Only the first group writes the header of a shared table, and the mark before it.\n if (plan.layout !== 'long' || groupIndex === 0) {\n if (options.bom === true) writer.push(UTF8_BOM);\n writer.pushLine(\n plan.layout === 'long'\n ? csvRow([TIME_COLUMN, 'channel', 'value'])\n : csvRow([TIME_COLUMN, ...group.channels.map((c) => c.column)]),\n );\n }\n return {\n group,\n writer,\n formatters: group.channels.map((c) => makeSampleFormatter(c.signal, c.decimals, sampleCaches)),\n formatTime: makeTimeFormatter(\n group.samplesPerRecord,\n group.rate,\n group.timeDecimals,\n offsets,\n ),\n rows: 0,\n settled,\n release,\n };\n });\n\n try {\n const written = await streamSignalRows(file, plan, open, recordStarts, options);\n\n /*\n Checked once everything has been flushed and ended, and not when the reader hung up.\n\n `--stdout | head -1` closes the pipe on purpose, which is a shell idiom rather than a\n failure. A pipe is not a regular file, so auditStdout declines it anyway — the second\n guard is here because the cost of getting this one wrong is reporting a failure for a\n command that worked.\n */\n if (audit) {\n // Uncompressed, the writer hands its bytes straight to the descriptor; compressed,\n // they were counted on the compressor's way out.\n if (options.gzip !== true) {\n /*\n Once per writer, not once per group. The long layout gives every group the same\n writer, so counting per group multiplied its byte total by the number of rates:\n `--stdout --layout long` on a three-rate recording handed over 32,043 bytes, was\n credited with 96,129, and failed with a disk-full error for a file that was\n complete on disk. It only showed with stdout redirected to a regular file, since\n that is the one case the audit applies to — which is the command the --layout\n documentation gives.\n */\n for (const writer of new Set(open.map((entry) => entry.writer))) {\n audit.count(writer.bytesOut);\n }\n }\n if (!open.some((entry) => entry.writer.hungUp)) audit.verify();\n }\n onHangUp?.(open.some((entry) => entry.writer.hungUp));\n return written;\n } catch (cause) {\n for (const entry of open) entry.writer.destroy();\n\n /*\n Reading and writing both fail through here, and both were reported as writing.\n\n A recording that shrinks mid-conversion — still being written by the acquisition\n software, say — raises the reader's own error, which names the record and says the\n file changed size while it was being read. That precise diagnosis was then filed under\n `Writing to \"<dir>\" failed` and given the hint about freeing disk space, which sends\n someone to look at the one part of the system that was working.\n\n The reader's message and its advice are kept; only the note about partial output is\n added, since that much is true of either failure.\n */\n // A ConversionError arrived already saying what went wrong — a callback that threw, say.\n // Wrapping it again turned \"the onProgress callback threw\" into `Writing to \"out\"\n // failed: the onProgress callback threw`, under a hint about checking the destination.\n if (cause instanceof ConversionError) throw cause;\n\n if (cause instanceof EdfError) {\n const where = outputDir === null ? 'stdout' : `\"${outputDir}\"`;\n throw new ConversionError(\n 'INPUT_UNREADABLE',\n cause.message,\n `${cause.hint ? `${cause.hint} ` : ''}What was written to ${where} before it failed is ` +\n `incomplete and should not be used.`,\n );\n }\n\n /*\n Through `describeFsError`, like the destination errors above, and not through Node's\n own text.\n\n That function exists to keep an errno and the internal call that raised it off the\n screen — its comment says so in as many words, quoting the leak it was written for:\n `Cannot create \"dangle/x\": ENOENT: no such file or directory, mkdir 'dangle'.` The\n write failures never joined it, so a directory sitting where signals.csv belongs read\n\n error: Writing to \"out\" failed: EISDIR: illegal operation on a directory, open 'out/signals.csv'\n\n with the errno token, the syscall, the path repeated, and no full stop — the one\n diagnostic shape this tool went back and fixed. The hint printed directly under it has\n always read the same errno and said \"A directory is sitting where that file belongs\",\n so the answer was in hand; it was the sentence above it that had not asked.\n */\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to ${outputDir === null ? 'stdout' : `\"${outputDir}\"`} failed: ` +\n `${describeFsError(cause)}.`,\n writeHint(cause, outputDir === null),\n );\n }\n}\n\n/**\n * Whether two sample times are close enough that only rounding could separate them.\n *\n * A relative epsilon, because the gap between doubles grows with magnitude. Well below any\n * real sample interval — the finest a recording can declare is bounded by its record\n * duration and samples-per-record fields — and well above the one-ULP disagreement that two\n * exact divisions of the same instant produce.\n */\nfunction nearlyEqual(a: number, b: number): boolean {\n return Math.abs(a - b) <= Math.max(Math.abs(a), Math.abs(b)) * 1e-12;\n}\n\n/**\n * One record's samples in the long layout: `time_s,channel,value`, in time order.\n *\n * The groups are merged rather than written one after another. Every sample in a record\n * falls inside that record's span, so taking the earliest next sample across the groups\n * each time leaves the whole file sorted by `time_s` — which is the only thing that makes a\n * mixed-rate long table useful, since sorting 3 million rows afterwards is the reader's\n * problem and a large one.\n *\n * Ties go to the group with the higher rate, which is the order the groups are already in.\n * Within a sample time the channels come out in the order the file declares them.\n */\nasync function writeLongRecord(\n file: EdfFile,\n open: readonly OpenGroup[],\n batch: RecordBatch,\n recordInBatch: number,\n recordStart: number,\n range: ConversionPlan['range'],\n): Promise<void> {\n const writer = open[0]?.writer;\n if (!writer) return;\n\n const cursors = new Int32Array(open.length);\n /* Reused across sample times so a three-million-row conversion allocates one of these. */\n const due: { entry: OpenGroup; groupIndex: number; sample: number; channel: number }[] = [];\n\n for (;;) {\n let earliest = Infinity;\n let source = -1;\n let sourceSample = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time < earliest) {\n earliest = time;\n source = g;\n sourceSample = sample;\n }\n }\n if (earliest === Infinity) return;\n\n /*\n Everything at this instant, in the order the file declares its channels.\n\n Groups are ordered by rate, largest first, because that is how the wide layout names\n its files. Emitting a tie group by group therefore ordered the channels by descending\n sampling rate — so a recording declaring `slow, medium, fast` wrote `fast, medium,\n slow`, while the documentation promised file order and channels.csv listed file order.\n Signal index is the file's own order, and the only one a reader can predict.\n\n \"At this instant\" is decided on the time as written, not on the double. `s / rate` for\n two channels at one moment need not give the same double: a 0.3 s record holding 12 and\n 4 samples makes 40 Hz and 13.333… Hz, and 9/40 is 0.22500000000000000555 while 3/13.333…\n is 0.22499999999999997780. Equality missed that, so those two rows fell out in numeric\n order — `slow` before `fast`, once, in the middle of a file that was otherwise right.\n\n Two rows are at one time exactly when they carry the same `time_s`, which is the only\n definition a reader of the CSV can apply. The numeric pre-filter keeps the common case\n to one comparison; formatting happens only for candidates already within a hair.\n */\n let earliestText: string | null = null;\n /*\n Formatted on the first near-tie and not before, which on a single-rate table is never.\n\n The group and sample that set `earliest` are captured above rather than searched for\n again, since the loop below advances cursors as it goes and a later search would be\n looking at a table that had already moved on.\n */\n const asWritten = (): string =>\n (earliestText ??= open[source]?.formatTime(recordStart, sourceSample) ?? '');\n due.length = 0;\n for (let g = 0; g < open.length; g++) {\n const entry = open[g];\n if (!entry) continue;\n const sample = cursors[g] ?? 0;\n if (sample >= entry.group.samplesPerRecord) continue;\n const time = recordStart + sample / entry.group.rate;\n if (time !== earliest) {\n // Far away in the ordinary case; only a near-tie is worth formatting.\n if (!nearlyEqual(time, earliest)) continue;\n if (entry.formatTime(recordStart, sample) !== asWritten()) continue;\n }\n cursors[g] = sample + 1;\n // Same window rule as the wide layout, with the same per-rate slack.\n if (\n !sampleTimeIsInRange(earliest, range.startSeconds, range.endSeconds, toleranceFor(entry.group.rate))\n ) {\n continue;\n }\n for (let c = 0; c < entry.group.channels.length; c++) {\n due.push({ entry, groupIndex: g, sample, channel: c });\n }\n }\n if (due.length === 0) continue;\n if (due.length > 1) {\n due.sort(\n (a, b) =>\n (a.entry.group.channels[a.channel]?.signal.index ?? 0) -\n (b.entry.group.channels[b.channel]?.signal.index ?? 0),\n );\n }\n\n for (const item of due) {\n const channel = item.entry.group.channels[item.channel];\n const format = item.entry.formatters[item.channel];\n if (!channel || !format) continue;\n if (writer.hungUp) return;\n writer.pushLine(\n `${item.entry.formatTime(recordStart, item.sample)},${escapeCsvField(channel.column)},` +\n `${format(file.sampleAt(batch, recordInBatch, channel.signal, item.sample))}`,\n );\n item.entry.rows++;\n // Flushed inside the record for the same reason the wide layout is; see there.\n if (writer.full) await writer.flush();\n }\n }\n}\n\nasync function streamSignalRows(\n file: EdfFile,\n plan: ConversionPlan,\n open: OpenGroup[],\n recordStarts: Float64Array | null,\n options: ConvertOptions,\n): Promise<WrittenFile[]> {\n const { startSeconds, endSeconds, startRecord, endRecord } = plan.range;\n const { recordDuration } = file.header;\n let recordsDone = 0;\n\n /*\n Every destination has hung up, so nothing formatted from here on could reach anyone.\n `--stdout | head -1` is the usual way to arrive here: the reader takes one line and\n closes the pipe while the conversion is still near the start of the recording.\n\n Stopping also makes the reported row count mean what it says — rows that reached the\n consumer, rather than rows the loop went on formatting into a discarded buffer.\n */\n const allHungUp = (): boolean => open.length > 0 && open.every((entry) => entry.writer.hungUp);\n\n for await (const batch of file.readRecords({ startRecord, endRecord })) {\n if (allHungUp()) break;\n for (let r = 0; r < batch.recordCount; r++) {\n if (allHungUp()) break;\n const index = batch.firstRecordIndex + r;\n const recordStart = recordStarts ? (recordStarts[index] ?? index * recordDuration) : index * recordDuration;\n\n if (plan.layout === 'long') {\n await writeLongRecord(file, open, batch, r, recordStart, plan.range);\n recordsDone++;\n continue;\n }\n\n for (const entry of open) {\n const { group, writer, formatters, formatTime } = entry;\n const { channels, rate } = group;\n // Slack that never reaches the next sample; see toleranceFor.\n const slack = toleranceFor(rate);\n\n for (let sample = 0; sample < group.samplesPerRecord; sample++) {\n const time = recordStart + sample / rate;\n if (!sampleTimeIsInRange(time, startSeconds, endSeconds, slack)) continue;\n\n let row = formatTime(recordStart, sample);\n for (let c = 0; c < channels.length; c++) {\n const channel = channels[c];\n const format = formatters[c];\n if (!channel || !format) continue;\n row += ',' + format(file.sampleAt(batch, r, channel.signal, sample));\n }\n if (writer.hungUp) break;\n writer.pushLine(row);\n entry.rows++;\n\n /*\n Flushed inside the record, not only at the end of one.\n\n The buffer was drained once per record, so the rows of a single record piled up\n with nothing emptying them — memory followed samples-per-record rather than the\n batch size the writer exists to hold to. One record of 16,000,000 samples died\n with a heap out of memory under a 256 MB cap, while the same 32 MB of samples\n split into 16,000 records converted to the same 283 MB CSV without trouble. The\n format allows either layout and says nothing about which to expect.\n\n `full` is a synchronous read of the pending size, so the twenty million rows that\n are not at a boundary cost a comparison rather than a microtask each.\n */\n if (writer.full) await writer.flush();\n }\n await writer.maybeFlush();\n }\n\n recordsDone++;\n }\n\n if (options.onProgress) {\n /*\n A caller's callback is the caller's, and its failures are not the destination's.\n\n This ran inside the same try that turns a stream failure into WRITE_FAILED, so a\n progress callback that threw came back as `Writing to \"out\" failed: caller bug`,\n advising the reader to check a destination that was working perfectly. It is the\n same misattribution the write hints had until 0.4.36, one layer up.\n\n The original is kept as `cause`, so the stack that actually matters survives, and\n the conversion still stops — the callback threw, and carrying on writing into a\n directory whose owner has just failed is not an improvement.\n */\n try {\n options.onProgress({\n recordsDone,\n recordsTotal: endRecord - startRecord,\n // Once per writer: the long layout's groups share one, so summing per group\n // reported a figure larger than the file being written.\n bytesWritten: [...new Set(open.map((entry) => entry.writer))].reduce(\n (sum, entry) => sum + entry.charsWritten,\n 0,\n ),\n });\n } catch (cause) {\n throw new ConversionError(\n 'CALLBACK_FAILED',\n `The onProgress callback threw: ${cause instanceof Error ? cause.message : String(cause)}`,\n 'This is the caller\\'s callback, not the recording or the destination. Whatever was ' +\n 'written before it threw is incomplete and should not be used.',\n { cause },\n );\n }\n }\n }\n\n const closed = new Set<BufferedLineWriter>();\n for (const entry of open) {\n if (closed.has(entry.writer)) continue;\n closed.add(entry.writer);\n try {\n await entry.writer.end();\n // With --gzip the writer's stream is the compressor, whose end callback fires when the\n // compressor is done rather than when the file behind it is. Awaiting only that would\n // report success with the tail of the file still in flight.\n await entry.settled;\n } finally {\n // In a finally for the reason 0.5.45 gives about the writer's own release: a\n // conversion that fails here still has to leave process.stdout as it found it, and a\n // failure is exactly when a caller goes on to convert something else.\n entry.release();\n }\n }\n\n // A shared table is one file, and its row count is every group's rows, not the first's.\n if (plan.layout === 'long') {\n const first = open[0];\n if (!first) return [];\n return [{ name: first.group.fileName, rows: open.reduce((sum, e) => sum + e.rows, 0) }];\n }\n return open.map((entry) => ({ name: entry.group.fileName, rows: entry.rows }));\n}\n\n/**\n * The stream rows are written to, plus a promise for the data reaching its destination.\n *\n * Compression sits between the writer and the file as a transform. Failures below it — a\n * full disk, an unwritable path — surface on the file stream, where nothing is listening,\n * so they are forwarded onto the compressor: that is the stream the writer watches, and\n * routing them there keeps one error path rather than two.\n */\nfunction compressed(\n target: Writable,\n gzip: boolean,\n): { stream: Writable; settled: Promise<void>; release: () => void } {\n if (!gzip) return { stream: target, settled: Promise.resolve(), release: (): void => {} };\n const compressor = createGzip();\n /*\n Removable, because one of the streams this can be handed outlives the conversion.\n\n 0.5.36 fixed exactly this leak for the writer's own listener: a library caller running\n twelve `toStdout` conversions left twelve 'error' listeners on `process.stdout` and got\n Node's MaxListenersExceededWarning on the eleventh. That fix is `BufferedLineWriter`'s\n `#release()`, and it cannot reach this one — under `gzip` the writer's stream is the\n compressor, and `process.stdout` is only ever the compressor's destination. So the same\n leak survived on the same stream, behind one extra flag, and the regression test written\n to catch it does not pass `gzip: true`.\n\n A file stream is a different matter and needs no release: it is created for this\n conversion and closed with it. The discipline is the writer's — release only from a\n stream this tool does not own.\n */\n const forward = (error: Error): void => {\n compressor.destroy(error);\n };\n target.on('error', forward);\n const release = (): void => {\n if (target === process.stdout || target === process.stderr) target.off('error', forward);\n };\n\n /*\n pipe() ends its destination when the source ends, and stdout must survive the\n conversion: the writer already refuses to close it, because a closed stdout breaks\n every later write in the process. Nothing waits on stdout either — it is not this\n tool's to finish.\n */\n const toStdout = target === process.stdout;\n compressor.pipe(target, { end: !toStdout });\n if (toStdout) {\n /*\n Waited on, even though stdout is not ours to end.\n\n This returned an already-resolved promise, so `await entry.settled` waited for nothing\n and the conversion declared itself finished while the compressor still held the tail of\n the stream. `--stdout --gzip` onto a destination that filled up therefore printed the\n ENOSPC *and then* \"Wrote 102,400 rows to stdout.\" — and exited 0, over a file 11,270\n bytes short whose gzip member has no trailer and will not decompress. Through `--out`,\n on the same volume with the same space, the identical failure is reported and exits 1.\n\n The byte audit could not see it either: it stats the descriptor as soon as the writers\n are done, which on this path is before the compressor has pushed its last chunks.\n\n `finished(compressor)` is the source side, not the destination — it resolves when the\n compressor has flushed everything into stdout, and it is stdout's own write that fails.\n That keeps `end: !toStdout` exactly as it was: nothing here closes stdout.\n */\n const flushed = finished(compressor).catch((error: unknown) => {\n /*\n EPIPE is not a failure here, and turning it into one is the trap this nearly fell\n into: `--stdout --gzip | head` is an ordinary thing to type, and the documented\n answer to it is \"Stopped: the reader closed the pipe after 52,507 of 102,400 rows\n had been written\", exit 0. Waiting on the compressor surfaced the EPIPE that the\n uncompressed path already routes through the writer's hang-up flag — the writer\n sees the same error, forwarded, and records it — so the wait has to let that one\n through and keep everything else.\n */\n if ((error as NodeJS.ErrnoException | null)?.code === 'EPIPE') return;\n throw error;\n });\n flushed.catch(() => {});\n return { stream: compressor, settled: flushed, release };\n }\n\n /*\n A failure under the compressor rejects both this promise and the writer's own end(),\n and end() is the one awaited first. Without a handler attached here that rejection\n belonged to nobody, and Node killed the process over it: `--gzip` into an unwritable\n path printed a raw EISDIR stack trace instead of the ordinary \"Writing to ... failed\"\n message and exit 1, which is what the same path does uncompressed.\n\n Attaching the handler is enough to make it handled. Awaiting `settled` downstream still\n reports the failure in the case where end() happened to succeed.\n */\n const settled = finished(target);\n settled.catch(() => {});\n return { stream: compressor, settled, release };\n}\n\n/**\n * Write one of the sidecar files, reporting a failure the way the signal writer does.\n *\n * These three used to call `writeFile` bare, so a failure escaped as whatever the\n * filesystem said — `EISDIR: illegal operation on a directory, open '...'` with no hint\n * and, more importantly, no mention that the signal files had already been written. The\n * conversion stopped half-done and the message gave no sign of it.\n */\nasync function writeOutputFile(\n outputDir: string,\n name: string,\n contents: string,\n gzip = false,\n bom = false,\n): Promise<void> {\n // metadata.json never gets one: JSON.parse rejects a leading U+FEFF, so a mark there\n // would break every reader of the file to help a spreadsheet that will not open it.\n if (bom) contents = UTF8_BOM + contents;\n try {\n // The sidecars are built in memory before being written, so compressing them in memory\n // costs nothing extra. Only the signal tables are large enough to need a stream.\n await writeFile(path.join(outputDir, name), gzip ? gzipSync(contents) : contents, gzip ? undefined : 'utf8');\n } catch (cause) {\n // The same as the signal tables above; see the note there.\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing \"${name}\" to \"${outputDir}\" failed: ${describeFsError(cause)}.`,\n writeHint(cause),\n );\n }\n}\n\nasync function writeChannelsCsv(\n outputDir: string,\n file: EdfFile,\n plan: ConversionPlan,\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const includedColumns = new Set(plan.groups.flatMap((g) => g.channels.map((c) => c.signal.index)));\n const fileFor = new Map<number, string>();\n for (const group of plan.groups) {\n for (const channel of group.channels) fileFor.set(channel.signal.index, group.fileName);\n }\n\n /*\n A rate rendered the way the file name beside it in the same row was rendered.\n\n `sampling_rate_hz` and `output_file` are two cells of one row about one rate, and they were\n written from different renderings of it — the cell from `String`, the name from\n `formatRates` over the whole set. They agree on every rate `String` prints plainly and part\n company on the ones it does not:\n\n a,0,a,uV,1e-19,100,...,signals_1_000e-19hz.csv,yes\n\n A reader joining that column against the files on disk finds `1e-19` naming\n `1_000e-19hz`, and `--info` printing a third thing again — except that `--info`'s RATE\n column is `1.000e-19 Hz`, which is the name's own form. channels.csv was the one surface\n rendering the rate for itself.\n\n Rendered together because that is how the names are: two rates a sixth decimal apart round\n to one string, and `formatRates` widens the whole set when they do, so a per-rate rendering\n would disagree with the names again on exactly those files.\n */\n const groupRates = plan.groups.map((group) => group.rate);\n const rateText = new Map<number, string>(\n formatRates(groupRates).map((text, i) => [groupRates[i] as number, text]),\n );\n\n const lines = [\n csvRow([\n 'column',\n 'signal_index',\n 'label',\n 'unit',\n 'sampling_rate_hz',\n 'samples_per_record',\n 'physical_min',\n 'physical_max',\n 'digital_min',\n 'digital_max',\n 'transducer',\n 'prefiltering',\n 'output_file',\n 'converted',\n ]),\n ];\n\n for (const signal of file.header.signals) {\n if (signal.isAnnotations) continue;\n const column = plan.columnNames.get(signal.index) ?? `signal_${signal.index}`;\n lines.push(\n csvRow([\n column,\n String(signal.index),\n signal.label,\n signal.physicalDimension,\n /*\n Plain decimal, at whatever magnitude the header declares.\n\n `String` switches to exponent notation below 1e-6 and above 1e21, which `plain`\n exists to undo — annotations.csv's two numeric columns were given it because \"an\n EDF+ TAL states its onset as ordinary decimal text, so a file saying `+0.0000001`\n came back as `1e-7` in a column whose every other cell is a plain decimal\", and\n `fixed` refuses the same form in signals.csv for the same reason: \"a reader parsing\n the column as decimal text has no reason to expect it.\" channels.csv was the third\n CSV and the one that kept it. A magnetometer calibrated to ±1e-16 T had its\n `physical_min` written `-1e-16` beside a `digital_min` of `-32768`, in the pair of\n columns this documentation points at for recovering the digital codes, over a\n signals.csv writing every one of those values out in full — and a channel calibrated\n to ±100 in the row above it, so the column held both notations at once.\n */\n // Nor the rate, which is rendered above against the names it has to match. Nor\n // samples_per_record, which is a count.\n rateText.get(signal.samplingRate) ?? formatRate(signal.samplingRate),\n String(signal.samplesPerRecord),\n plain(signal.physicalMin),\n plain(signal.physicalMax),\n plain(signal.digitalMin),\n plain(signal.digitalMax),\n signal.transducer,\n signal.prefiltering,\n fileFor.get(signal.index) ?? '',\n includedColumns.has(signal.index) ? 'yes' : 'no',\n ]),\n );\n }\n\n const name = outputCsvName('channels', gzip);\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: lines.length - 1 };\n}\n\n/*\n The bounds an annotation is filtered against: what the caller actually asked for, not the\n window that survived being clamped to the recording.\n\n Filtering by the resolved window meant an unbounded request still lost events. `--end 999h`\n on a three-second file clamps to 3, and `--start 0` gets its end from the recording, so\n both filtered to [0, 3) and dropped the markers at 3.0 and 3.5 that a bare invocation\n keeps — asking for more of a recording returned less of it.\n\n An end the caller did not give is unbounded, not \"the end of the data\": EDF+ lets an\n annotation sit past the last sample, which is exactly where an end-of-recording marker is.\n\n `--duration` is measured from wherever the conversion actually starts. Anchoring it at 0\n instead read the same absent `--start` two ways in two adjacent lines, and on a recording\n that does not begin at zero the two windows did not even overlap: an EDF+D file whose first\n record sits at 30 s converted its samples from [30, 35) while filtering annotations against\n (-inf, 5), so every event inside the converted window was dropped and `annotations.csv` came\n back empty. `resolveRange` has always defaulted the same missing start to the earliest\n record, which is where this now takes it from.\n*/\n/**\n * What the durations in the events that will actually be written look like.\n *\n * These two warnings were raised from the file-wide counts the decoder accumulates, while\n * `annotations.csv` is filtered to the requested window. A conversion of one second of a\n * recording therefore warned that \"1 annotation states a duration that is not a number, so\n * its duration_s cell is empty\" about an event two seconds outside it — naming a cell that is\n * not in the output — and `--strict` failed the run for it. There is no such value, no such\n * cell, and no such row.\n *\n * Taken from the events themselves, after the same filter the writer applies, so the count\n * and the sentence describe the same rows. An unreadable duration is carried on the event\n * because `duration: null` cannot say whether the file gave one; a negative duration needs no\n * flag, since the value is right there.\n */\nexport function durationDiagnostics(\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n): Diagnostic[] {\n const written = annotations.filter((a) => a.onset >= window.from && a.onset < window.to);\n const diagnostics: Diagnostic[] = [];\n\n const negative = written.filter((a) => a.duration !== null && a.duration < 0).length;\n if (negative > 0) {\n const one = negative === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${counted(negative, 'annotation')} state${one ? 's' : ''} a duration below zero, ` +\n `which is not a length of time.`,\n hint:\n 'The value is written to annotations.csv as the file gave it. Adding it to onset_s ' +\n 'ends the event before it starts, so check these rows before using the durations.',\n });\n }\n\n const unreadable = written.filter((a) => a.durationUnreadable === true).length;\n if (unreadable > 0) {\n const one = unreadable === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${counted(unreadable, 'annotation')} state${one ? 's' : ''} a duration that is ` +\n `not a number, so ${one ? 'its' : 'their'} duration_s cell is empty.`,\n hint:\n 'The onset and the description were read normally. An empty duration_s otherwise ' +\n 'means the file stated no duration, so these rows cannot be told apart from those.',\n });\n }\n\n return diagnostics;\n}\n\n/**\n * What the descriptions in the events that will actually be written look like.\n *\n * EDF's four free-text header fields have had two warnings about where they land since they\n * were written: `FORMULA_LABEL` for text a spreadsheet runs instead of reading, and\n * `NONPRINTABLE_LABEL` for bytes that drive a terminal. Both say the same thing about the\n * remedy — the text is written exactly as the file has it, because rewriting it would mean\n * the CSV no longer says what the recording says — and both exist so that the tool is not\n * silent about where it goes.\n *\n * `annotations.csv`'s `description` column is the same kind of text, out of the same file,\n * into the same spreadsheet, and nothing was said about it at all. An event described\n * `=HYPERLINK(\"http://…\",\"Sleep stage W\")` was written verbatim, exit 0, no warning, and\n * opens as a live link nobody in the reading chain wrote; one carrying `\\x1b[31m` turns the\n * terminal red on `cat annotations.csv`. It is the more likely of the two to happen by\n * accident, since a description is typed by a person at a scoring station while a channel\n * label is written once by the recorder.\n *\n * It is also the only free text in the output that can carry a character above U+00FF —\n * header text is decoded latin1, so every byte of it becomes a code point below U+0100, and\n * a bidirectional override cannot reach a label. It can reach a description, which is UTF-8.\n *\n * Counted rather than raised per event, unlike the header's four fields: a night's scoring is\n * thousands of events, and a warning each is not a report. The count is of the rows that\n * reach `annotations.csv`, after the same window filter the writer applies, for the reason\n * `durationDiagnostics` beside it gives.\n */\nexport function descriptionDiagnostics(\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n): Diagnostic[] {\n const written = annotations.filter((a) => a.onset >= window.from && a.onset < window.to);\n const diagnostics: Diagnostic[] = [];\n\n const formulaic = written.filter((a) => startsFormula(a.text));\n if (formulaic.length > 0) {\n const one = formulaic.length === 1;\n const shown = [...new Set(formulaic.map((a) => a.text[0] as string))].join(', ');\n diagnostics.push({\n code: 'FORMULA_LABEL',\n severity: 'warning',\n message:\n `${counted(formulaic.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +\n `starting with ${shown}, which Excel, LibreOffice and Google Sheets read as the start ` +\n `of a formula rather than as text.`,\n hint:\n 'The text is written to annotations.csv exactly as the file has it, so the cell is ' +\n 'what the recording says. Open the CSV with pandas or R, or import it into the ' +\n 'spreadsheet as text, if you do not want it evaluated.',\n });\n }\n\n const marked = written.filter((a) => unprintableIn(a.text).length > 0);\n if (marked.length > 0) {\n const one = marked.length === 1;\n const shown = [...new Set(marked.flatMap((a) => unprintableIn(a.text)))]\n .map(escapeCharacter)\n .join(', ');\n diagnostics.push({\n code: 'NONPRINTABLE_LABEL',\n severity: 'warning',\n message:\n `${counted(marked.length, 'annotation')} ${one ? 'has a description' : 'have descriptions'} ` +\n `carrying text a terminal does not print as itself (${shown}), written to ` +\n `annotations.csv exactly as the file has ${one ? 'it' : 'them'}.`,\n hint:\n 'A control byte can drive the terminal and a bidirectional override reverses what ' +\n 'follows it, so read the file with pandas or R rather than with cat. The cell is ' +\n 'what the recording says either way.',\n });\n }\n\n return diagnostics;\n}\n\n/**\n * The window annotations are filtered by — the bounds as asked for, not as snapped to records.\n *\n * Exported so `--info` can count the events a conversion would write using the same predicate\n * that writes them, rather than a second copy of it.\n */\nexport function requestedAnnotationWindow(\n options: ConvertOptions,\n recordingStart: number,\n): { from: number; to: number } {\n const from = options.start ?? -Infinity;\n const to =\n options.end !== undefined\n ? options.end\n : options.duration !== undefined\n ? (options.start ?? recordingStart) + options.duration\n : Infinity;\n return { from, to };\n}\n\nasync function writeAnnotationsCsv(\n outputDir: string,\n annotations: readonly Annotation[],\n window: { from: number; to: number },\n gzip: boolean,\n bom: boolean,\n): Promise<WrittenFile> {\n const inWindow = annotations\n .filter((a) => a.onset >= window.from && a.onset < window.to)\n .sort((a, b) => a.onset - b.onset || a.recordIndex - b.recordIndex);\n\n const lines = [csvRow(['onset_s', 'duration_s', 'description', 'record_index'])];\n for (const annotation of inWindow) {\n lines.push(\n csvRow([\n plain(annotation.onset),\n annotation.duration === null ? '' : plain(annotation.duration),\n annotation.text,\n String(annotation.recordIndex),\n ]),\n );\n }\n\n const name = outputCsvName('annotations', gzip);\n await writeOutputFile(outputDir, name, lines.join('\\n') + '\\n', gzip, bom);\n return { name, rows: inWindow.length };\n}\n\n/**\n * What to try next, chosen from what actually went wrong.\n *\n * Every write failure carried the same advice — \"Free up space or choose another destination\n * with --out\" — which fits exactly one errno. A directory sitting where signals.csv belongs\n * came back telling the reader to free up disk space, and so did a read-only volume, a\n * permission denial and a path too long for the filesystem. Wrong advice is worse than none:\n * it sends someone to check `df` on a disk that is fine, and the thing that is actually\n * wrong stays unexamined.\n *\n * The errno is the one piece of the failure that names the cause, so it is what picks the\n * sentence. Anything unrecognised keeps the general form rather than guessing.\n */\nfunction writeHint(cause: unknown, toStdout = false): string {\n /*\n The stdout path writes no files, and --out is the flag its user chose not to pass.\n\n Both halves of this sentence were wrong there: \"the files written so far\" named files\n that do not exist, and \"choose another destination with --out\" is advice for a different\n command — the destination is whatever the shell redirected the stream to. Same class as\n the disk-space hint this function replaced, one flag over.\n */\n const preamble = toStdout\n ? 'What reached stdout before it failed is incomplete and should not be used. '\n : 'The files written so far are incomplete and should not be used. ';\n return (\n preamble +\n destinationAdvice(\n cause,\n toStdout ? 'redirect it somewhere else' : 'choose another with --out',\n toStdout ? 'redirect it somewhere shorter' : 'choose a shorter destination with --out',\n 'Part of that path no longer exists; make sure nothing is removing it while the ' +\n 'conversion runs.',\n )\n );\n}\n\n/**\n * The same question asked before anything has been written: the output directory could not be\n * created.\n *\n * Both raisings carried one sentence for every errno — \"Check the path exists and that you\n * have permission to write there\" — which is the shape `writeHint` above was written to\n * replace, in the words of its own docstring: \"Wrong advice is worse than none: it sends\n * someone to check `df` on a disk that is fine, and the thing that is actually wrong stays\n * unexamined.\" A full disk, a read-only volume and a path past the filesystem's length limit\n * all got advice about a path that exists and a permission that is not the problem, two lines\n * under a message where `describeFsError` had already named the cause exactly.\n *\n * No preamble, because nothing has been written yet — which is the only thing that differs\n * between the two, so the sentences themselves are shared rather than copied.\n */\nfunction createHint(cause: unknown): string {\n return destinationAdvice(\n cause,\n 'choose another with --out',\n 'choose a shorter destination with --out',\n 'Part of that path cannot be created — a symbolic link with nothing behind it, or a ' +\n 'mount point that is not mounted; choose another with --out.',\n );\n}\n\n/** The sentence an errno earns, with the two phrases that name where \"somewhere else\" is. */\nfunction destinationAdvice(\n cause: unknown,\n elsewhere: string,\n shorter: string,\n missing: string,\n): string {\n const code = (cause as NodeJS.ErrnoException | null)?.code;\n switch (code) {\n case 'ENOSPC':\n return `The destination is out of space; free some up or ${elsewhere}.`;\n case 'EDQUOT':\n return `You are over your disk quota on this filesystem; ${elsewhere}.`;\n case 'EACCES':\n case 'EPERM':\n return `You do not have permission to write there; ${elsewhere}.`;\n case 'EROFS':\n return `That filesystem is mounted read-only; ${elsewhere}.`;\n case 'EISDIR':\n return `A directory is sitting where that file belongs; remove or rename it, or ${elsewhere}.`;\n /*\n The one errno whose sentence is not the same question at both call sites.\n\n Mid-conversion, a missing component is a directory that went away under a run that had\n already written into it, and \"make sure nothing is removing it\" is the thing to check.\n Before anything is written it is the opposite: the destination's parents are created\n recursively, so reaching ENOENT there means a component that cannot be created — a\n symbolic link with nothing behind it, or a mount point that is not mounted.\n\n $ edf2csv rec.edf --out /mnt/archive/out\n error: Cannot create \"/mnt/archive/out\": part of the path does not exist.\n Part of that path no longer exists; make sure nothing is removing it\n while the conversion runs.\n\n Nothing is removing it and nothing was there. `createHint`'s own docstring says the\n preamble \"is the only thing that differs between the two, so the sentences themselves\n are shared rather than copied\" — and this is the sentence where that was not so.\n */\n case 'ENOENT':\n return missing;\n case 'ENAMETOOLONG':\n return `That path is longer than the filesystem allows; ${shorter}.`;\n case 'EMFILE':\n case 'ENFILE':\n return `Too many files are open; a recording with many sampling rates opens one output file per rate, so --channels narrows it.`;\n case 'EPIPE':\n return 'Whatever was reading the output closed it before the conversion finished.';\n default:\n return 'Check the destination and run the conversion again.';\n }\n}\n\n/**\n * Confirms that everything handed to a file-backed stdout actually arrived.\n *\n * `edf2csv rec.edf --stdout > out.csv` onto a volume that the output very nearly fills lost\n * the tail in silence: 94,977 of 102,400 rows on disk, the file ending mid-row, stderr\n * announcing \"Wrote 102,400 rows to stdout.\" and the process exiting 0. The same recording\n * onto the same volume through `--out` fails correctly, which is what gives it away.\n *\n * POSIX `write` returns a short count rather than an error when the disk fills partway\n * through a single call, and only the NEXT write raises ENOSPC. `--out` always has a next\n * write — channels.csv and metadata.json come after — so it always finds out. `--stdout`\n * has nothing after it, and when fd 1 is a regular file Node's stdout is a SyncWriteStream\n * whose `_write` discards the byte count `writeSync` returns, so nothing is raised at all.\n * No error means no `#failure`, so checking that alone would not have caught this.\n *\n * What can be checked is the descriptor: how much it grew against how much it was given.\n * Only for a regular file — a pipe, a terminal or a socket has no size to compare, and on\n * those a short write cannot go unreported this way. Appending (`>>`) is fine, since the\n * starting size is taken before anything is written.\n *\n * Exported so `--info` can use the same audit a `--stdout` conversion does.\n *\n * `--info` wrote its description with `process.stdout.write` and looked at nothing: redirected\n * into a full filesystem it produced a zero-byte file and exited 0, so `edf2csv rec.edf --info\n * > desc.txt` reported success over nothing at all. A 900-channel recording's description is\n * 58 KB, which is not a size a destination is guaranteed to have.\n */\nexport function auditStdout(): { count: (bytes: number) => void; verify: () => void } | null {\n let startSize: number;\n try {\n const info = fstatSync(1);\n if (!info.isFile()) return null;\n startSize = info.size;\n } catch {\n // No usable descriptor to audit; the conversion is not the place to complain about it.\n return null;\n }\n\n let expected = 0;\n return {\n count: (bytes: number): void => {\n expected += bytes;\n },\n verify: (): void => {\n let landed: number;\n try {\n landed = fstatSync(1).size - startSize;\n } catch {\n return;\n }\n if (landed >= expected) return;\n\n /*\n This audit exists for the failure nothing else reports: a write that is accepted, and\n silently truncated. When the stream itself raised an error there is nothing left for\n it to add, and what it added was a second `error:` line denying the first —\n\n error: Writing to stdout failed: ENOSPC: no space left on device, write\n error: Writing to stdout failed: 58900 of 58900 bytes did not reach the destination,\n which stopped accepting them part way through.\n ... nothing after it raised an error because there was nothing after it.\n\n one failure, reported twice, the second of the two wrong about it.\n */\n if (process.stdout.errored) return;\n\n /*\n Nothing landing at all is not a short write, and was described as one.\n\n `--info > desc.txt` onto a full filesystem left a zero-byte file, and this said the\n destination \"stopped accepting them part way through\" and that \"what is there ends\n mid-row\" — of a file with nothing in it and no rows in it.\n */\n if (landed === 0) {\n throw new ConversionError(\n 'WRITE_FAILED',\n `Writing to stdout failed: none of the ${counted(expected, 'byte')} reached the destination.`,\n 'The destination is almost certainly out of space. Nothing was written, so there ' +\n 'is nothing there to discard.',\n );\n }\n throw new ConversionError(\n 'WRITE_FAILED',\n // The pair a reader subtracts by eye, so both go through the grouping — the rule\n // 0.8.5 set and reached only as far as the messages already built with `counted`.\n `Writing to stdout failed: ${grouped(expected - landed)} of ` +\n `${counted(expected, 'byte')} did not reach the destination, which stopped ` +\n `accepting them part way through.`,\n 'What is there ends mid-row and should not be used. The destination is almost ' +\n 'certainly out of space — a short write is how a filesystem reports filling up ' +\n 'mid-write, and nothing after it raised an error because there was nothing after it.',\n );\n },\n };\n}\n\n/**\n * Why `--stdout` cannot take this recording, or null when it can.\n *\n * Lifted out of the conversion so `--info` can ask the same question. It was not asking:\n * `--info --stdout` on a three-rate recording predicted \"Would write 1,155 rows, roughly\n * 22.2 KB\" and said the channels \"are written to one file per rate\" — for a command that\n * refuses to run, writes nothing, and names no files. `--info` exists to say what a\n * conversion will do, and this is one of the things it does.\n *\n * Reported by `--info` as a warning rather than a refusal, for the reason 0.5.51 gives about\n * the destination guards: `--info` writes nothing, so a rule about what the output would be\n * has no business stopping it from describing the recording — and being told the command\n * will not work is exactly what you asked.\n */\nexport function stdoutRefusal(file: EdfFile, plan: ConversionPlan): ConversionError | null {\n if (!plan.writeSignals) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n '--stdout has no signal data to write because --annotations-only was given.',\n 'Drop one of the two flags.',\n );\n }\n\n /*\n No table at all is its own answer, and neither layout gave it.\n\n A recording with no signal channels — one holding only EDF+ annotations — produced\n zero rate groups. The wide layout then said \"--stdout needs exactly one table, but\n this recording produces 0, one for each sampling rate its channels use ()\", with an\n empty parenthetical, advice to narrow to one of no rates, and advice to use\n `--layout long` — which wrote zero bytes to stdout, not even a header row, and exited\n 0 while warning that \"the signal files hold their headers and no data\". There were no\n files and there was no header. The one path that was right about this is\n `--annotations-only`, which refuses outright, and this is the same situation reached\n by a different route.\n */\n if (plan.groups.length === 0) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n file.dataSignals.length === 0\n ? `--stdout has no signal data to write: this recording has no signal channels, ` +\n `only ${file.header.isBdf ? 'BDF+' : 'EDF+'} annotations.`\n : '--stdout has no signal data to write: nothing was selected that carries samples.',\n file.dataSignals.length === 0\n ? /*\n Named the way that conversion would name it, since `--gzip` is already on the\n command line this is refusing. This hint is nothing but a command to run, and the\n command it names writes `annotations.csv.gz` — so a reader who follows it finds\n nothing under the name they were given. Same fault 0.8.48 fixed for the warnings,\n in the one refusal that names a file.\n */\n `Convert to a directory to get its ${outputCsvName('annotations', plan.gzip)}, or ` +\n `drop --stdout.`\n : 'Check --channels and the requested window, or convert to a directory instead.',\n );\n }\n\n // The long layout is one table whatever the rates are, so it has nothing to refuse.\n if (plan.layout !== 'long' && plan.groups.length !== 1) {\n return new ConversionError(\n 'UNSUPPORTED_REQUEST',\n // Naming the rates is the point: the hint says to narrow the selection, and this\n // is what there is to narrow it to. The parenthetical used to repeat the count\n // that had just been given — \"produces 3 (its channels use 3 different sampling\n // rates)\" — which told nobody anything they could act on.\n `--stdout needs exactly one table, but this recording produces ${grouped(plan.groups.length)}, ` +\n `one for each sampling rate its channels use ` +\n `(${listed(formatRates(plan.groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n 'Narrow it to one rate with --channels, write --layout long to get them all in ' +\n 'one table, or convert to a directory instead.',\n );\n }\n return null;\n}\n\n\n\n/**\n * Asked for signal data and given none to put in a file.\n *\n * Every channel selected carries zero samples per record, so there is no table to make —\n * `edf2csv rec.edf --channels unused` writes channels.csv and metadata.json and no signals.csv\n * at all. The NO_SAMPLES warning explains the channel; nothing explained the missing file, and\n * the documentation says signals.csv is written unless --annotations-only was passed. Someone\n * looking for it should be told where it went.\n *\n * There are two ways to arrive with no groups, and one wording is only true of one of them. A\n * recording that holds nothing but EDF+ annotations has no channel that could have been\n * selected, its channels.csv is a header row and nothing else, and no channel of it carries\n * samples — so \"every channel selected\", \"channels.csv still describes them\" and \"which\n * channels do carry samples\" were three false statements in one warning, printed under a\n * warning that had just said the file has no signal channels.\n *\n * Exported, and worded in the present tense, so `--info` can raise the same one. It was built\n * inline here, which meant the one mode whose purpose is to say what a conversion will do said\n * nothing about the file that conversion would not write: `--info --strict` on a recording of\n * nothing but annotations reported one warning where converting it reported two, and\n * `--info --json` carried the shorter list to whatever reads it. Everything the answer depends\n * on is in the file and the plan, both of which `--info` already has.\n */\nexport function noSignalFile(file: EdfFile, plan: ConversionPlan): Diagnostic | null {\n if (!plan.writeSignals || plan.groups.length > 0) return null;\n const noChannelsAtAll = file.dataSignals.length === 0;\n return {\n code: 'NO_SAMPLES',\n severity: 'warning',\n message: noChannelsAtAll\n ? 'No signal file is written: there is no signal data in this recording to put in one.'\n : 'No signal file is written: every channel selected carries zero samples per data ' +\n 'record, so there is nothing to put in one.',\n // Named as they will be written. `plan.gzip` is on the plan because `--info` named\n // `annotations.csv` for a run that wrote `annotations.csv.gz`; these two sentences are\n // about the same pair of files and were still naming the uncompressed ones.\n hint: noChannelsAtAll\n ? `${outputCsvName('annotations', plan.gzip)} holds whatever events it carries. ` +\n `${outputCsvName('channels', plan.gzip)} lists signal channels, so it has none to list.`\n : // `--info` raises this too, since 0.7.84, and was told to run itself to answer it.\n // Naming where the number lives works in both modes; naming a command works in one.\n 'Nothing about them is lost: every channel\\'s samples per record is in the channel ' +\n `table --info prints, and in the ${outputCsvName('channels', plan.gzip)} a conversion writes.`,\n };\n}\n\n/**\n * The signal table an `--annotations-only` run does not write, taken out of the hints about it.\n *\n * That mode writes the event list and nothing else — no signal files at all — and four hints\n * about record timing describe the rows of one. On a recording whose records run backwards it\n * printed both of these over an annotations.csv holding its header and no rows:\n *\n * warning: This is a discontinuous (EDF+D) recording: its data records are not\n * contiguous in time.\n * Each row carries its true recording time, so gaps stay visible instead of\n * being closed.\n * warning: 2 data records start earlier than the record before them.\n * Rows are written in file order, so the time column will not increase\n * monotonically.\n *\n * There are no rows and no time column. The facts above the hints are about the recording and\n * stay; what changes is the sentence describing what the conversion will do with them, which\n * is the same surgery `withTimingPromiseKept` does to the first of these when the record\n * starts cannot be derived.\n *\n * `writesSignals` rather than the option, because the plan is what settles it.\n */\nexport function withSignalTableUnwritten(\n diagnostics: readonly Diagnostic[],\n writesSignals: boolean,\n gzip: boolean,\n): Diagnostic[] {\n if (writesSignals) return [...diagnostics];\n /*\n The sidecars named here are named as the run writes them.\n\n These sentences send the reader to the one file an `--annotations-only` run does write, and\n said `channels.csv` to a `--gzip` run that writes `channels.csv.gz` — five hints pointing at\n a name that is not in the directory, one of them beside the `_ch` collision hint that 0.8.48\n had already taught to say `channels.csv.gz`. So a single run named both spellings, and only\n the second one existed.\n */\n const channelsFile = outputCsvName('channels', gzip);\n const annotationsFile = outputCsvName('annotations', gzip);\n return diagnostics.map((diagnostic) => {\n /*\n And the three calibration warnings, whose hints are about cells.\n\n Each describes what a conversion does with a channel whose header cannot map cleanly —\n leaves the cell empty, fills it with the one value the mapping has, keeps the inversion —\n and `--annotations-only` converts no samples, so none of it happens:\n\n warning: Signal 0 (\"flat\") has digital minimum equal to digital maximum (0), so its\n values cannot be scaled.\n Its cells are left empty rather than filled with a value the header cannot\n justify.\n\n What is wrong with the header is still worth saying, and channels.csv — which that run\n does write — still carries the calibration. One message also ends in the conversion:\n \"so every sample converts to the same value\", over a run converting none.\n */\n /*\n And the two that say where a channel's name lands, which is signals.csv in both layouts\n and neither of them here.\n\n `--annotations-only` writes no signal table at all, so the name reaches exactly one\n place: channels.csv's `column` cell, which that run does write — control bytes, `_ch`\n suffix and all. Both sentences sent the reader to a file that is not there:\n\n warning: Signal 0's label and unit contain 2 control characters (\\x1b), which will\n appear as the channel's name in signals.csv and in channels.csv's unit\n cell in any conversion that writes one ...\n warning: 2 signals share the label \"T8-P8\" (positions #0, #1).\n Their names are suffixed with the signal number so they stay\n distinguishable: a column name each in the wide layout, and a distinct\n value in the channel column under --layout long.\n\n Neither layout happens. The second sentence offers a choice between two files that are\n not written.\n */\n if (diagnostic.code === 'NONPRINTABLE_LABEL') {\n return {\n ...diagnostic,\n message: diagnostic.message\n .replace(\n \"as the channel's name in signals.csv\",\n `as the channel's name in ${channelsFile}'s column cell`,\n )\n // Matched on the name the header diagnostic used, which the line above has not\n // touched: it renames the first mention, and this collapses the second.\n .replace(\"column cell and in channels.csv's\", 'column cell and in its')\n .replace(' in any conversion that writes one', ''),\n };\n }\n if (\n diagnostic.code === 'DUPLICATE_LABEL' &&\n diagnostic.hint?.startsWith('Their names are suffixed')\n ) {\n return {\n ...diagnostic,\n hint:\n 'Their names are suffixed with the signal number so they stay distinguishable. ' +\n '--annotations-only writes no signal table, so the suffixed names appear only in ' +\n `${channelsFile}'s column cells.`,\n };\n }\n if (diagnostic.code === 'DEGENERATE_DIGITAL_RANGE') {\n return {\n ...diagnostic,\n hint:\n 'No samples are converted with --annotations-only, so there are no cells to leave ' +\n `empty. ${channelsFile} still records the digital range the header gives.`,\n };\n }\n if (diagnostic.code === 'DEGENERATE_PHYSICAL_RANGE') {\n return {\n ...diagnostic,\n message: diagnostic.message.replace(\n 'so every sample converts to the same value.',\n 'so every sample would convert to the same value.',\n ),\n hint:\n `No samples are converted with --annotations-only. ${channelsFile} still records ` +\n 'the calibration, one point wide.',\n };\n }\n if (diagnostic.code === 'INVERTED_PHYSICAL_RANGE') {\n return {\n ...diagnostic,\n hint:\n `No samples are converted with --annotations-only. ${channelsFile} records the ` +\n 'physical minimum and maximum in the order the header gives them, inversion included.',\n };\n }\n if (diagnostic.code !== 'DISCONTINUOUS' || diagnostic.hint === undefined) return diagnostic;\n if (diagnostic.hint.startsWith('Each row carries its true recording time')) {\n return {\n ...diagnostic,\n hint:\n '--annotations-only writes no signal rows, so nothing here is timed from the ' +\n `records. ${annotationsFile} carries each event's own onset, and the record it ` +\n 'came from in record_index.',\n };\n }\n if (diagnostic.hint.startsWith('Rows are written in file order')) {\n return {\n ...diagnostic,\n hint:\n '--annotations-only writes no signal rows, so no time column is affected. ' +\n `${annotationsFile}'s record_index still names the record each event came from.`,\n };\n }\n if (diagnostic.hint.startsWith('Sample times are written from zero')) {\n return {\n ...diagnostic,\n hint: '--annotations-only writes no signal rows, so no sample times are written at all.',\n };\n }\n return diagnostic;\n });\n}\n\n/**\n * The sidecar files a `--stdout` run does not write, taken out of the sentences about them.\n *\n * `--stdout` puts one table on the stream and writes nothing else — \"No sidecar files are\n * written\", as `ConvertOptions` puts it. Two diagnostics raised before the destination is\n * known end by pointing at one of those files:\n *\n * warning: The header's start date and time (\"XX.XX.XX\" and \"YY.YY.YY\") are not a date\n * and a time, so the recording has no start instant.\n * ... and metadata.json records start_datetime_local as null.\n *\n * warning: This recording's timekeeping annotations place it 1e17s from its own start\n * date ...\n * ... Add the onsets in annotations.csv to recover absolute times.\n *\n * Neither file exists after such a run. The second is advice a reader can follow into an\n * empty directory — there is no directory.\n *\n * Amended where the answer is, the same way `withTimingPromiseKept` rewrites a `DISCONTINUOUS`\n * hint the parser could not have known was false, and `withoutFileRateWarning` drops a header\n * diagnostic the plan supersedes. A conversion to a directory keeps every word.\n */\nexport function withSidecarsNamed(\n diagnostics: readonly Diagnostic[],\n { toStdout, gzip }: { toStdout: boolean; gzip: boolean },\n): Diagnostic[] {\n if (!toStdout && !gzip) return [...diagnostics];\n const channels = outputCsvName('channels', gzip);\n const annotations = outputCsvName('annotations', gzip);\n return diagnostics.map((diagnostic) => {\n /*\n The last message of this kind, and the one that names its files mid-sentence.\n\n `NONPRINTABLE_LABEL` says where an invisible byte lands — the channel's name in\n signals.csv, a cell of channels.csv — and `--gzip` writes neither of those names. It was\n left out of 0.8.48 because the file is named inside a sentence assembled from which of\n the four header fields carry bytes, and out of 0.8.54 for the same reason. Renaming is\n all it needs, once the sentence is settled: this runs after `withSignalTableUnwritten`\n has had its say about the layout, so whichever file the sentence ended up naming is the\n one that gets the suffix.\n */\n /*\n And the same sentence under `--stdout`, where the signal table has no name at all.\n\n The `channels.csv` half of it has been hedged since 0.8.52 — \"in the channels.csv of any\n conversion that writes one\", which is true of a run that writes none. The half naming\n `signals.csv` was not, and `--stdout` is the mode this warning matters most in: it is\n the one that really does print the CSV to a terminal, which is what the hint two lines\n under it warns about.\n\n $ edf2csv control-labels.edf --stdout | less\n warning: Signal 1's label contains 1 control character (\\x07), which will appear as\n the channel's name in signals.csv, exactly as the header has it.\n\n There is no signals.csv. The byte is in the header row going past on the stream, which\n is where the reader is looking.\n */\n if ((gzip || toStdout) && diagnostic.code === 'NONPRINTABLE_LABEL') {\n return {\n ...diagnostic,\n message: diagnostic.message\n // Past a name the pass above already suffixed: `channels.csv.gz` contains\n // `channels.csv`, and a second rename made it `channels.csv.gz.gz`.\n .replace(\n /\\bsignals\\.csv\\b(?!\\.gz)/gu,\n toStdout ? 'the CSV on stdout' : outputCsvName('signals', true),\n )\n .replace(/\\bchannels\\.csv\\b(?!\\.gz)/gu, channels),\n };\n }\n /*\n The header's own `NO_SAMPLES`, which names the file that describes the channel it is\n about — and under `--stdout` there is no such file, so the reassurance is empty:\n\n $ edf2csv one-empty-channel.edf --stdout > rows.csv\n warning: Signal 1 (\"unused\") carries no samples at all (0 per data record).\n It is described in channels.csv but left out of the converted data.\n\n Under `--gzip` the file is there under another name. Both are settled here.\n */\n if (diagnostic.code === 'NO_SAMPLES' && diagnostic.hint?.startsWith('It is described in')) {\n return {\n ...diagnostic,\n hint: toStdout\n ? 'It is left out of the converted data, and --stdout writes no channels.csv to ' +\n 'describe it in — convert to a directory for that.'\n : `It is described in ${channels} but left out of the converted data.`,\n };\n }\n /*\n The collision hint, whose whole advice is a file to look the channel up in.\n\n A duplicate label, or a label that collides with another channel's `_ch` suffix, renames\n the column — and the sentence saying so ends by sending the reader to channels.csv for\n the channel's `signal_index`, since the renamed name is the only one in the table:\n\n $ edf2csv montage.edf --stdout > rows.csv\n warning: Signal 2 is labelled \"T8_ch0\", which is also the column name another\n channel's \"_ch\" suffix produces, so its column is \"T8_ch0_ch2\".\n Column names are unique; look this channel up in channels.csv by its\n signal_index.\n\n There is no channels.csv, and the renamed column is sitting in the header row on stdout\n with nothing to map it back. Under `--gzip` it was worse: `channels.csv.gz`, a\n compressed file no `--stdout` run writes either.\n\n Answered the way the two hints above answer it — the file is named, so that the reader\n knows what they are missing, and the command that produces it is named too.\n */\n if (toStdout && diagnostic.code === 'DUPLICATE_LABEL' && diagnostic.hint?.includes('look this channel up in')) {\n // \"Channel names\" under --layout long, where the name lands in the channel column;\n // \"Column names\" in the wide layout. Both are true of the stream, so the half of the\n // sentence that is about the rename is kept as the plan wrote it.\n const unique = diagnostic.hint.startsWith('Channel names') ? 'Channel names' : 'Column names';\n return {\n ...diagnostic,\n hint:\n `${unique} are unique, and --stdout writes no channels.csv to look this channel ` +\n `up in by its signal_index — convert to a directory for that.`,\n };\n }\n if (\n toStdout &&\n diagnostic.code === 'START_TIME_UNREADABLE' &&\n diagnostic.hint?.includes('metadata.json records')\n ) {\n return {\n ...diagnostic,\n hint:\n 'time_s is unaffected — it counts from the start of the recording either way. What ' +\n 'cannot be done is turning it into a wall-clock instant, and --stdout writes no ' +\n 'metadata.json to record start_datetime_local as null in.',\n };\n }\n if (\n diagnostic.code === 'DISCONTINUOUS' &&\n diagnostic.hint?.includes('onsets in annotations.csv')\n ) {\n return {\n ...diagnostic,\n hint: toStdout\n ? 'Sample times are written from zero instead, so every row is present and the ' +\n 'column increases. The onsets that recover absolute times are in the annotation ' +\n 'channel; --stdout writes no annotations.csv, so convert to a directory for them.'\n : 'Sample times are written from zero instead, so every row is present and the ' +\n `column increases. Add the onsets in ${annotations} to recover absolute times if ` +\n 'you need them.',\n };\n }\n return diagnostic;\n });\n}\n\n/**\n * `--annotations-only` on a recording that has no annotations.\n *\n * Exported and present-tense for the same reason `noSignalFile` above is. `--info` prints an\n * accurate line about it in the report body — \"and no annotations.csv either, since this\n * recording has no annotation channel\" — and raised nothing, so the one mode whose purpose is\n * to say what a conversion will do carried a shorter warning list than the conversion did:\n * `--info --json --annotations-only` on a plain EDF file listed no warning where converting\n * lists one, and `--info --strict`, which cli-reference.md recommends for screening a folder\n * before converting it, exited 0 where the conversion exits 1. Same defect as the one 0.7.84\n * closed for `NO_SAMPLES`, one flag over; both halves of the answer are in the file and the\n * options, which `--info` already has.\n */\nexport function noAnnotations(file: EdfFile, options: ConvertOptions): Diagnostic | null {\n if (options.annotationsOnly !== true || file.annotationSignals.length > 0) return null;\n return {\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message:\n '--annotations-only was requested but this recording has no annotation channel, ' +\n 'so there are no events to export.',\n /*\n The format the file is in, rather than the one most files are in.\n\n \"Plain EDF files carry no annotations\" was said to every recording that reaches here,\n and two kinds of file reach it that are not plain EDF. A BioSemi `.bdf` was told about a\n format it is not in — this tool tells EDF and BDF apart everywhere else, down to the\n `BDF Annotations` label the specification gives the channel.\n\n And a file whose header says `EDF+C` while carrying no annotation channel at all is not\n a file for which having no events is ordinary: EDF+ keeps its events in that channel and\n is required to have one. The sentence read as reassurance about a file that is malformed.\n */\n hint: file.header.isEdfPlus\n ? `The header marks this ${describeFormat(file.header)}, and that format keeps its ` +\n `events in an annotation channel this file does not have. Convert without ` +\n `--annotations-only to get the signals.`\n : `Plain ${file.header.isBdf ? 'BDF' : 'EDF'} files carry no annotations. Convert ` +\n `without --annotations-only to get the signals.`,\n };\n}\n\n/**\n * `--annotations-only` that wrote no events, and why.\n *\n * The two causes are told apart because the answers are different. A channel holding nothing\n * but timekeeping entries has nothing to export and never will; a window that excluded every\n * event is a thing the caller can change, and the commonest reason is reading the window off\n * a clock the recording does not use — `--start` and `--end` are on the recording's own,\n * which `--info` prints as \"Timed from\".\n *\n * `NO_ANNOTATIONS` rather than a new code, since it is the same statement its other raising\n * makes — there are no events to export — about the same flag, and a code is matched on by\n * scripts that should not have to learn a second one for the same fact.\n */\nexport function emptyAnnotations(\n total: number,\n window: { from: number; to: number },\n gzip = false,\n): Diagnostic {\n const windowed = (window.from !== -Infinity || window.to !== Infinity) && total > 0;\n return {\n code: 'NO_ANNOTATIONS',\n severity: 'warning',\n message: windowed\n ? `None of this recording's ${counted(total, 'event')} fall inside the requested ` +\n `window, so ${outputCsvName('annotations', gzip)} holds its header and no rows.`\n : `This recording's annotation channel carries no events, so ` +\n `${outputCsvName('annotations', gzip)} holds its header and no rows.`,\n hint: windowed\n ? '--start and --end are read on the recording\\'s own clock, which --info prints as ' +\n '\"Timed from\", and an event is kept when its onset falls inside the window.'\n : 'The channel holds only the timekeeping entries that say where each data record ' +\n 'sits, and those are never exported.',\n };\n}\n\n/** Raised when the input moved while it was being read. See where it is pushed. */\nfunction inputChanged(hadChecksum: boolean): Diagnostic {\n return {\n code: 'INPUT_CHANGED',\n severity: 'warning',\n message:\n 'The input changed while it was being converted, so this output covers the file as ' +\n 'it was when the conversion started, not as it is now.',\n hint: hadChecksum\n ? 'No checksum was recorded: the bytes that were converted are no longer there to ' +\n 'hash. Convert again once the recording is finished.'\n : 'Convert again once the recording is finished to pick up the rest.',\n };\n}\n\nasync function writeMetadata(\n outputDir: string,\n inputPath: string,\n file: EdfFile,\n plan: ConversionPlan,\n written: readonly WrittenFile[],\n annotationCount: number,\n /** Hash of the bytes that were converted, or null when it could not be vouched for. */\n checksum: string | null,\n /** Whether the record start times could be read; see withTimingPromiseKept. */\n timedFromRecords: boolean,\n /** Whether each CSV starts with a byte order mark; see the `bom` field below. */\n bom: boolean,\n): Promise<void> {\n const { header } = file;\n\n /*\n The file as it was when it was opened, not as it is now.\n\n Both of these used to come from re-opening the path once the CSVs were written, which\n describes whatever answers to that name by then rather than what was converted. A\n recording still being written grew from 2,000 records to 3,000 mid-conversion and\n metadata.json recorded `data_records: 2000` — correct, the CSV holds 2,000 — beside the\n byte count and SHA-256 of the 3,000-record file. The two halves of one provenance record\n described two different files, and the checksum covered bytes nobody had converted.\n Replacing the file at that path did the same thing more thoroughly.\n\n `file.fileSize` is the number every record count and window in this output was derived\n from, and the hash is taken over exactly those bytes through the descriptor already open\n on them, so the record describes one file throughout.\n */\n const metadata = {\n tool: { name: 'edf2csv', version: TOOL_VERSION },\n source: {\n path: path.resolve(inputPath),\n bytes: file.fileSize,\n modified: new Date(file.modifiedAtOpenMs).toISOString(),\n sha256: checksum,\n },\n recording: {\n format: describeFormat(header),\n version: header.version,\n patient_id: header.patientId,\n recording_id: header.recordingId,\n // Zone-less on purpose: EDF records local wall-clock digits and no timezone.\n start_datetime_local: formatWallClock(header.startDateTime),\n start_date_raw: header.startDateRaw,\n start_time_raw: header.startTimeRaw,\n data_records: file.recordCount,\n data_records_declared: header.declaredRecordCount,\n record_duration_seconds: header.recordDuration,\n duration_seconds: file.durationSeconds,\n signal_count: header.signalCount,\n annotation_channels: file.annotationSignals.length,\n },\n conversion: {\n converted_at: new Date().toISOString(),\n start_seconds: plan.range.startSeconds,\n end_seconds: plan.range.endSeconds,\n whole_recording: plan.range.isWholeRecording,\n records_converted: [plan.range.startRecord, plan.range.endRecord],\n annotations_written: annotationCount,\n /*\n Which shape the signal table is in, which nothing recorded.\n\n A wide `signals.csv` and a long one are different files with different columns, and\n metadata.json described them identically — so a pipeline handed an output directory\n could not tell from the archive which it had. It matters most for `rate_groups` right\n below: in the wide layout those entries are one per file and their `channels` are that\n file's columns, and in the long layout every entry names the one shared table and its\n `channels` are values in that table's `channel` column. Same array, two readings, and\n no way to know which applied.\n */\n layout: plan.layout,\n /*\n The one option whose effect nothing else in this archive shows.\n\n `layout` is recorded on the stated grounds that \"the two have different columns and\n nothing else in the archive distinguishes them\". `--gzip` distinguishes itself: the\n names in `files` end `.csv.gz`. `--bom` puts three bytes in front of every CSV and\n leaves no other trace — and it is the option that decides whether reading the table\n back works. `csv.ts` sets out why: pandas strips the mark either engine, and Python's\n own `csv.reader` over a plain `open()` does not, nor does `readFileSync(path,\n 'utf8')`, so the first column name comes back as `\\ufefftime_s` and a lookup of\n `time_s` misses. A pipeline reading this document to find out how to open the files\n could not find out the one thing that changes the answer.\n */\n bom,\n files: written.map((f) => ({ name: f.name, rows: f.rows })),\n rate_groups: plan.groups.map((g) => ({\n file: g.fileName,\n sampling_rate_hz: g.rate,\n channels: g.channels.map((c) => c.column),\n decimals: g.channels.map((c) => c.decimals),\n })),\n },\n notes: withSidecarsNamed(\n withSignalTableUnwritten(\n [\n ...withTimingPromiseKept(withoutFileRateWarning(file.diagnostics), timedFromRecords),\n ...plan.diagnostics,\n ],\n plan.writeSignals,\n plan.gzip,\n ),\n // metadata.json is only written into a directory, so this is never the stdout case.\n { toStdout: false, gzip: plan.gzip },\n ).map((d) => ({\n code: d.code,\n severity: d.severity,\n message: d.message,\n })),\n };\n\n // Escaped like the two --json documents: metadata.json records the input's path, and a\n // `cat` of it should not be able to reorder the terminal. See escapeJsonText.\n const document = escapeJsonText(JSON.stringify(metadata, null, 2));\n await writeOutputFile(outputDir, 'metadata.json', `${document}\\n`);\n}\n\n\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edf2csv",
3
- "version": "0.8.81",
3
+ "version": "0.8.83",
4
4
  "description": "Convert EDF, EDF+ and BDF biosignal recordings (European Data Format) to CSV from the command line. Local, streaming, and never resamples or alters units.",
5
5
  "keywords": [
6
6
  "edf",