edfcore 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -0
  3. package/dist/bytes/latin1.d.ts +43 -0
  4. package/dist/bytes/latin1.d.ts.map +1 -0
  5. package/dist/bytes/latin1.js +84 -0
  6. package/dist/bytes/latin1.js.map +1 -0
  7. package/dist/bytes/numbers.d.ts +45 -0
  8. package/dist/bytes/numbers.d.ts.map +1 -0
  9. package/dist/bytes/numbers.js +87 -0
  10. package/dist/bytes/numbers.js.map +1 -0
  11. package/dist/bytes/view.d.ts +27 -0
  12. package/dist/bytes/view.d.ts.map +1 -0
  13. package/dist/bytes/view.js +53 -0
  14. package/dist/bytes/view.js.map +1 -0
  15. package/dist/constants.d.ts +113 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +83 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/decode/digital.d.ts +49 -0
  20. package/dist/decode/digital.d.ts.map +1 -0
  21. package/dist/decode/digital.js +180 -0
  22. package/dist/decode/digital.js.map +1 -0
  23. package/dist/decode/physical.d.ts +37 -0
  24. package/dist/decode/physical.d.ts.map +1 -0
  25. package/dist/decode/physical.js +178 -0
  26. package/dist/decode/physical.js.map +1 -0
  27. package/dist/diagnostics/codes.d.ts +38 -0
  28. package/dist/diagnostics/codes.d.ts.map +1 -0
  29. package/dist/diagnostics/codes.js +134 -0
  30. package/dist/diagnostics/codes.js.map +1 -0
  31. package/dist/diagnostics/collector.d.ts +76 -0
  32. package/dist/diagnostics/collector.d.ts.map +1 -0
  33. package/dist/diagnostics/collector.js +95 -0
  34. package/dist/diagnostics/collector.js.map +1 -0
  35. package/dist/diagnostics/format.d.ts +21 -0
  36. package/dist/diagnostics/format.d.ts.map +1 -0
  37. package/dist/diagnostics/format.js +125 -0
  38. package/dist/diagnostics/format.js.map +1 -0
  39. package/dist/errors.d.ts +143 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +135 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/header/dates.d.ts +101 -0
  44. package/dist/header/dates.d.ts.map +1 -0
  45. package/dist/header/dates.js +378 -0
  46. package/dist/header/dates.js.map +1 -0
  47. package/dist/header/fields.d.ts +105 -0
  48. package/dist/header/fields.d.ts.map +1 -0
  49. package/dist/header/fields.js +363 -0
  50. package/dist/header/fields.js.map +1 -0
  51. package/dist/header/identification.d.ts +47 -0
  52. package/dist/header/identification.d.ts.map +1 -0
  53. package/dist/header/identification.js +156 -0
  54. package/dist/header/identification.js.map +1 -0
  55. package/dist/header/lookup.d.ts +33 -0
  56. package/dist/header/lookup.d.ts.map +1 -0
  57. package/dist/header/lookup.js +67 -0
  58. package/dist/header/lookup.js.map +1 -0
  59. package/dist/header/parse.d.ts +32 -0
  60. package/dist/header/parse.d.ts.map +1 -0
  61. package/dist/header/parse.js +319 -0
  62. package/dist/header/parse.js.map +1 -0
  63. package/dist/header/scale.d.ts +68 -0
  64. package/dist/header/scale.d.ts.map +1 -0
  65. package/dist/header/scale.js +154 -0
  66. package/dist/header/scale.js.map +1 -0
  67. package/dist/header/signals.d.ts +64 -0
  68. package/dist/header/signals.d.ts.map +1 -0
  69. package/dist/header/signals.js +374 -0
  70. package/dist/header/signals.js.map +1 -0
  71. package/dist/header/variant.d.ts +67 -0
  72. package/dist/header/variant.d.ts.map +1 -0
  73. package/dist/header/variant.js +180 -0
  74. package/dist/header/variant.js.map +1 -0
  75. package/dist/index.d.ts +46 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +59 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/inspect.d.ts +31 -0
  80. package/dist/inspect.d.ts.map +1 -0
  81. package/dist/inspect.js +165 -0
  82. package/dist/inspect.js.map +1 -0
  83. package/dist/io/blob.d.ts +14 -0
  84. package/dist/io/blob.d.ts.map +1 -0
  85. package/dist/io/blob.js +29 -0
  86. package/dist/io/blob.js.map +1 -0
  87. package/dist/io/bytes.d.ts +11 -0
  88. package/dist/io/bytes.d.ts.map +1 -0
  89. package/dist/io/bytes.js +26 -0
  90. package/dist/io/bytes.js.map +1 -0
  91. package/dist/io/cached.d.ts +19 -0
  92. package/dist/io/cached.d.ts.map +1 -0
  93. package/dist/io/cached.js +129 -0
  94. package/dist/io/cached.js.map +1 -0
  95. package/dist/io/http.d.ts +20 -0
  96. package/dist/io/http.d.ts.map +1 -0
  97. package/dist/io/http.js +225 -0
  98. package/dist/io/http.js.map +1 -0
  99. package/dist/io/read.d.ts +42 -0
  100. package/dist/io/read.d.ts.map +1 -0
  101. package/dist/io/read.js +119 -0
  102. package/dist/io/read.js.map +1 -0
  103. package/dist/io/source.d.ts +33 -0
  104. package/dist/io/source.d.ts.map +1 -0
  105. package/dist/io/source.js +72 -0
  106. package/dist/io/source.js.map +1 -0
  107. package/dist/node.d.ts +70 -0
  108. package/dist/node.d.ts.map +1 -0
  109. package/dist/node.js +109 -0
  110. package/dist/node.js.map +1 -0
  111. package/dist/record-index.d.ts +58 -0
  112. package/dist/record-index.d.ts.map +1 -0
  113. package/dist/record-index.js +290 -0
  114. package/dist/record-index.js.map +1 -0
  115. package/dist/recording.d.ts +57 -0
  116. package/dist/recording.d.ts.map +1 -0
  117. package/dist/recording.js +178 -0
  118. package/dist/recording.js.map +1 -0
  119. package/dist/tal/annotations.d.ts +36 -0
  120. package/dist/tal/annotations.d.ts.map +1 -0
  121. package/dist/tal/annotations.js +437 -0
  122. package/dist/tal/annotations.js.map +1 -0
  123. package/dist/tal/grammar.d.ts +111 -0
  124. package/dist/tal/grammar.d.ts.map +1 -0
  125. package/dist/tal/grammar.js +324 -0
  126. package/dist/tal/grammar.js.map +1 -0
  127. package/dist/tal/ticks.d.ts +69 -0
  128. package/dist/tal/ticks.d.ts.map +1 -0
  129. package/dist/tal/ticks.js +137 -0
  130. package/dist/tal/ticks.js.map +1 -0
  131. package/dist/time/segments.d.ts +33 -0
  132. package/dist/time/segments.d.ts.map +1 -0
  133. package/dist/time/segments.js +87 -0
  134. package/dist/time/segments.js.map +1 -0
  135. package/dist/time/timeline.d.ts +62 -0
  136. package/dist/time/timeline.d.ts.map +1 -0
  137. package/dist/time/timeline.js +227 -0
  138. package/dist/time/timeline.js.map +1 -0
  139. package/dist/time/window.d.ts +54 -0
  140. package/dist/time/window.d.ts.map +1 -0
  141. package/dist/time/window.js +232 -0
  142. package/dist/time/window.js.map +1 -0
  143. package/dist/types.d.ts +409 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +16 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/validate.d.ts +53 -0
  148. package/dist/validate.d.ts.map +1 -0
  149. package/dist/validate.js +483 -0
  150. package/dist/validate.js.map +1 -0
  151. package/package.json +77 -0
  152. package/src/bytes/latin1.ts +87 -0
  153. package/src/bytes/numbers.ts +115 -0
  154. package/src/bytes/view.ts +59 -0
  155. package/src/constants.ts +96 -0
  156. package/src/decode/digital.ts +258 -0
  157. package/src/decode/physical.ts +235 -0
  158. package/src/diagnostics/codes.ts +209 -0
  159. package/src/diagnostics/collector.ts +129 -0
  160. package/src/diagnostics/format.ts +143 -0
  161. package/src/errors.ts +208 -0
  162. package/src/header/dates.ts +453 -0
  163. package/src/header/fields.ts +455 -0
  164. package/src/header/identification.ts +196 -0
  165. package/src/header/lookup.ts +81 -0
  166. package/src/header/parse.ts +398 -0
  167. package/src/header/scale.ts +268 -0
  168. package/src/header/signals.ts +550 -0
  169. package/src/header/variant.ts +239 -0
  170. package/src/index.ts +151 -0
  171. package/src/inspect.ts +181 -0
  172. package/src/io/blob.ts +31 -0
  173. package/src/io/bytes.ts +29 -0
  174. package/src/io/cached.ts +136 -0
  175. package/src/io/http.ts +299 -0
  176. package/src/io/read.ts +152 -0
  177. package/src/io/source.ts +87 -0
  178. package/src/node.ts +138 -0
  179. package/src/record-index.ts +389 -0
  180. package/src/recording.ts +234 -0
  181. package/src/tal/annotations.ts +574 -0
  182. package/src/tal/grammar.ts +540 -0
  183. package/src/tal/ticks.ts +172 -0
  184. package/src/time/segments.ts +109 -0
  185. package/src/time/timeline.ts +290 -0
  186. package/src/time/window.ts +304 -0
  187. package/src/types.ts +490 -0
  188. package/src/validate.ts +610 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Digital counts to physical units.
3
+ *
4
+ * Layer 3. Sole owner of the scaling expression. The expression is PINNED — see `toPhysical`
5
+ * before changing anything in this file.
6
+ *
7
+ * Also home to `clampToDigitalRange`, which is a cross-validation tool and never part of a
8
+ * read: EDFlib clamps silently when it loads samples, edfcore does not, and reproducing that
9
+ * behaviour has to be something a caller asks for explicitly.
10
+ */
11
+ import { DEFAULT_MAX_MATERIALIZE_BYTES } from '../constants.js';
12
+ import { EdfBudgetError, EdfScalingError } from '../errors.js';
13
+ const BYTES_PER_FLOAT64 = 8;
14
+ const BYTES_PER_INT32 = 4;
15
+ /** The physical dimension that marks a log-compressed channel (EDFlib `edffloat.html`). */
16
+ const LOG_TRANSFORMED_DIMENSION = 'Filtered';
17
+ function assertWithinBudget(requiredBytes, what, options) {
18
+ const budgetBytes = options?.maxMaterializeBytes ?? DEFAULT_MAX_MATERIALIZE_BYTES;
19
+ if (requiredBytes <= budgetBytes)
20
+ return;
21
+ throw new EdfBudgetError(`Producing ${what} needs a ${requiredBytes}-byte array, above the ${budgetBytes}-byte ` +
22
+ 'maxMaterializeBytes budget, so the allocation was refused before it was attempted. ' +
23
+ 'Next: convert fewer samples per call, reuse an `out` array, or raise ' +
24
+ 'options.maxMaterializeBytes.', { requiredBytes, budgetBytes });
25
+ }
26
+ /**
27
+ * Why `signal.scale` is `undefined`, re-derived from the signal itself.
28
+ *
29
+ * The header records the matching diagnostic at parse time, but a bare `EdfSignal` does not
30
+ * carry it, and `toPhysical` takes a signal. These four tests are the same ones `header/scale.ts`
31
+ * applies, IN THE SAME ORDER — a signal can fail two of them at once, and the order is what
32
+ * decides which cause is named, so the code reported here is the code the header reported.
33
+ * `buildScale` owns that order; this function follows it. A signal that fails none
34
+ * of them yet still has no scale gets `SCALE_UNAVAILABLE` rather than the nearest-looking code:
35
+ * naming the wrong cause is worse than admitting the cause is not on hand.
36
+ */
37
+ function describeScalingFailure(signal) {
38
+ const digitalSpec = 'EDF+ additional specification 5: "Digital maximum must be larger than Digital minimum"';
39
+ if (signal.digitalMinimum === signal.digitalMaximum) {
40
+ return {
41
+ code: 'DEGENERATE_DIGITAL_RANGE',
42
+ reason: `declares digitalMinimum == digitalMaximum == ${signal.digitalMinimum}, which makes ` +
43
+ 'the gain a division by zero',
44
+ specReference: digitalSpec,
45
+ };
46
+ }
47
+ if (signal.physicalMinimum === signal.physicalMaximum) {
48
+ return {
49
+ code: 'DEGENERATE_PHYSICAL_RANGE',
50
+ reason: `declares physicalMinimum == physicalMaximum == ${signal.physicalMinimum}, so every ` +
51
+ 'sample would map to that one value',
52
+ specReference: undefined,
53
+ };
54
+ }
55
+ if (signal.digitalMinimum > signal.digitalMaximum) {
56
+ return {
57
+ code: 'INVERTED_DIGITAL_RANGE',
58
+ reason: `declares digitalMinimum ${signal.digitalMinimum} above digitalMaximum ` +
59
+ `${signal.digitalMaximum}, and edfcore will not guess which one the writer meant`,
60
+ specReference: digitalSpec,
61
+ };
62
+ }
63
+ if (signal.physicalDimension.trim() === LOG_TRANSFORMED_DIMENSION) {
64
+ return {
65
+ code: 'LOG_TRANSFORMED_CHANNEL',
66
+ reason: `has physical dimension "${LOG_TRANSFORMED_DIMENSION}", so its values are ` +
67
+ 'log-compressed and the linear formula would be wrong by orders of magnitude',
68
+ specReference: 'EDFlib edffloat.html',
69
+ };
70
+ }
71
+ return {
72
+ code: 'SCALE_UNAVAILABLE',
73
+ reason: 'has no usable scale, and the header recorded the reason rather than the signal',
74
+ specReference: undefined,
75
+ };
76
+ }
77
+ function scalingError(signal) {
78
+ const failure = describeScalingFailure(signal);
79
+ const message = `[${failure.code}] signal ${signal.index} "${signal.label}" ${failure.reason}, so ` +
80
+ 'physical units are undefined for it. Raw fields: digital minimum ' +
81
+ `"${signal.raw.digitalMinimum}", digital maximum "${signal.raw.digitalMaximum}", ` +
82
+ `physical minimum "${signal.raw.physicalMinimum}", physical maximum ` +
83
+ `"${signal.raw.physicalMaximum}", physical dimension "${signal.raw.physicalDimension}". ` +
84
+ (failure.specReference === undefined ? '' : `${failure.specReference}. `) +
85
+ 'Next: decodeDigital() still works on this signal; edfcore will not invent a gain.';
86
+ return new EdfScalingError(message, {
87
+ code: failure.code,
88
+ signalIndex: signal.index,
89
+ label: signal.label,
90
+ });
91
+ }
92
+ /**
93
+ * Float64 by contract, never Float32.
94
+ *
95
+ * Float32 carries 24 significand bits, so a 24-bit BDF sample scaled into it loses about a
96
+ * quarter of a quantisation step — a rounding error a quarter the size of the smallest real
97
+ * difference the hardware can express.
98
+ */
99
+ function resolveFloat64Out(out, length, options) {
100
+ if (out === undefined) {
101
+ assertWithinBudget(length * BYTES_PER_FLOAT64, `${length} physical samples`, options);
102
+ return new Float64Array(length);
103
+ }
104
+ if (out.length < length) {
105
+ throw new RangeError(`out holds ${out.length} samples but this conversion produces ${length}. Next: size the ` +
106
+ 'reused array to digital.length, or omit it and let toPhysical allocate.');
107
+ }
108
+ // A longer `out` is narrowed to a view over its own memory, so reuse still allocates no
109
+ // samples while `result.length` stays equal to the real sample count.
110
+ return out.length === length ? out : out.subarray(0, length);
111
+ }
112
+ function resolveInt32Out(out, length, options) {
113
+ if (out === undefined) {
114
+ assertWithinBudget(length * BYTES_PER_INT32, `${length} clamped samples`, options);
115
+ return new Int32Array(length);
116
+ }
117
+ if (out.length < length) {
118
+ throw new RangeError(`out holds ${out.length} samples but this clamp produces ${length}. Next: size the ` +
119
+ 'reused array to digital.length, or omit it and let clampToDigitalRange allocate.');
120
+ }
121
+ return out.length === length ? out : out.subarray(0, length);
122
+ }
123
+ /**
124
+ * `physical = bitValue * (offset + digital)`, in float64 throughout.
125
+ *
126
+ * THIS EXPRESSION IS PINNED AND MUST NOT BE "SIMPLIFIED". It is numerically worse than
127
+ * `physicalMinimum + (digital - digitalMinimum) * gain`, and that is not an accident: it is
128
+ * EDFlib's exact form, kept verbatim so edfcore reproduces pyEDFlib/EDFlib float64 output
129
+ * bit for bit. The two forms disagree by up to ~9.3e-10 LSB — ten orders of magnitude below
130
+ * the quantisation floor, and on asymmetric ranges they differ on nearly half the samples by
131
+ * one ULP. Rewriting this line breaks the golden-value tests, and rightly so.
132
+ *
133
+ * Throws `EdfScalingError` when `signal.scale` is `undefined`. edfcore never fabricates a gain,
134
+ * and `decodeDigital` keeps working on such a signal.
135
+ */
136
+ export function toPhysical(signal, digital, out, options) {
137
+ const scale = signal.scale;
138
+ if (scale === undefined)
139
+ throw scalingError(signal);
140
+ const length = digital.length;
141
+ const physical = resolveFloat64Out(out, length, options);
142
+ const bitValue = scale.bitValue;
143
+ const offset = scale.offset;
144
+ for (let i = 0; i < length; i++) {
145
+ // biome-ignore lint/style/noNonNullAssertion: i < digital.length, per the ArrayLike contract.
146
+ physical[i] = bitValue * (offset + digital[i]);
147
+ }
148
+ return physical;
149
+ }
150
+ /**
151
+ * Clamp to the declared digital range. POST-HOC ONLY — nothing on the read path calls this.
152
+ *
153
+ * It exists to reproduce a clamping consumer (EDFlib clamps silently on read; edfcore does not)
154
+ * when cross-validating against one. Clamping to `[min(digMin, digMax), max(digMin, digMax)]`
155
+ * rather than to `[digMin, digMax]` matters for an inverted declaration, where the naive bounds
156
+ * are empty and collapse every sample onto a single value.
157
+ */
158
+ export function clampToDigitalRange(signal, digital, out, options) {
159
+ const low = Math.min(signal.digitalMinimum, signal.digitalMaximum);
160
+ const high = Math.max(signal.digitalMinimum, signal.digitalMaximum);
161
+ if (!Number.isFinite(low) || !Number.isFinite(high)) {
162
+ // Every comparison against a NaN bound is false, so proceeding would return the input
163
+ // unchanged while claiming to have clamped it.
164
+ throw new RangeError(`signal ${signal.index} "${signal.label}" declares digital minimum ` +
165
+ `"${signal.raw.digitalMinimum}" and digital maximum "${signal.raw.digitalMaximum}", ` +
166
+ 'which do not both parse as numbers, so there is no range to clamp to. Next: read ' +
167
+ 'header.diagnostics for this signal.');
168
+ }
169
+ const length = digital.length;
170
+ const clamped = resolveInt32Out(out, length, options);
171
+ for (let i = 0; i < length; i++) {
172
+ // biome-ignore lint/style/noNonNullAssertion: i < digital.length.
173
+ const value = digital[i];
174
+ clamped[i] = value < low ? low : value > high ? high : value;
175
+ }
176
+ return clamped;
177
+ }
178
+ //# sourceMappingURL=physical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physical.js","sourceRoot":"","sources":["../../src/decode/physical.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The diagnostic vocabulary.
3
+ *
4
+ * Layer 1. Imports nothing, so every other module may depend on it.
5
+ *
6
+ * A diagnostic is a *value*, never a log line. Each code has exactly one severity and one
7
+ * disposition, both fixed here:
8
+ *
9
+ * - `fatal` — edfcore cannot proceed without inventing something. Always throws, even when
10
+ * `strict` is false.
11
+ * - `deferred` — the header parses, but one signal cannot be scaled. `signal.scale` becomes
12
+ * `undefined`, `decodeDigital` keeps working, and `toPhysical` throws.
13
+ * - `warning` — the file is impolite but readable, and what we return is true.
14
+ * - `info` — the file is correct; the note exists because the situation surprises people.
15
+ */
16
+ export type EdfSeverity = 'error' | 'warning' | 'info';
17
+ /** How a code behaves when it fires. See the module comment. */
18
+ export type EdfDiagnosticDisposition = 'fatal' | 'deferred' | 'warning' | 'info';
19
+ /**
20
+ * Every code edfcore itself can emit, grouped by disposition.
21
+ *
22
+ * This union and the `DISPOSITIONS` map below are checked against each other by the compiler:
23
+ * the map is typed as a total `Record` over this union, so a code added to one and not the
24
+ * other is a build error rather than a silent gap.
25
+ */
26
+ export type EdfKnownDiagnosticCode = 'NOT_AN_EDF_FILE' | 'SOURCE_TOO_SMALL' | 'SIGNAL_COUNT_INVALID' | 'NUMERIC_FIELD_INVALID' | 'COMMA_DECIMAL_SEPARATOR' | 'RECORD_SIZE_ZERO' | 'EDFPLUS_WITHOUT_ANNOTATION_SIGNAL' | 'TIMELINE_NOT_MONOTONIC' | 'DEGENERATE_DIGITAL_RANGE' | 'DEGENERATE_PHYSICAL_RANGE' | 'INVERTED_DIGITAL_RANGE' | 'LOG_TRANSFORMED_CHANNEL' | 'SCALE_UNAVAILABLE' | 'HEADER_SIZE_MISMATCH' | 'RECORD_COUNT_RECOVERED' | 'TRUNCATED_FILE' | 'PARTIAL_FINAL_RECORD' | 'TRAILING_BYTES' | 'RECORD_SIZE_ABOVE_RECOMMENDED' | 'NONSTANDARD_RESERVED_FIELD' | 'NON_ASCII_HEADER_FIELD' | 'NUMERIC_FIELD_NOT_LEFT_JUSTIFIED' | 'DATE_FIELDS_DISAGREE' | 'DATE_UNPARSEABLE' | 'PATIENT_ID_NONCONFORMANT' | 'RECORDING_ID_NONCONFORMANT' | 'DUPLICATE_SIGNAL_LABEL' | 'DIGITAL_RANGE_EXCEEDS_FORMAT' | 'ZERO_SAMPLES_PER_RECORD' | 'ZERO_RECORD_DURATION' | 'ANNOTATION_SIGNAL_HEADER_NONCONFORMANT' | 'MISSING_EDFPLUS_MARKER' | 'TIMEKEEPING_TAL_MISSING' | 'TIMEKEEPING_TAL_NONCONFORMANT' | 'START_OFFSET_OUT_OF_RANGE' | 'TAL_MALFORMED' | 'TAL_TRUNCATED_AT_REGION_END' | 'TAL_REGION_NOT_NUL_TERMINATED' | 'ANNOTATION_TEXT_NOT_UTF8' | 'RECORD_ONSET_SPACING_VIOLATION' | 'DISCONTINUITY_IN_CONTINUOUS_FILE' | 'DATE_CLIPPED_TO_1985_2084' | 'INVERTED_PHYSICAL_RANGE' | 'NEGATIVE_ANNOTATION_ONSET';
27
+ /**
28
+ * Open union: known codes autocomplete, and a `default` branch stays mandatory so adding a
29
+ * code in a minor release cannot break a consumer's exhaustive switch.
30
+ */
31
+ export type EdfDiagnosticCode = EdfKnownDiagnosticCode | (string & {});
32
+ export declare const DIAGNOSTIC_DISPOSITIONS: Readonly<Record<EdfKnownDiagnosticCode, EdfDiagnosticDisposition>>;
33
+ /** Unknown codes are treated as warnings — an unrecognised note must never escalate. */
34
+ export declare function dispositionOf(code: EdfDiagnosticCode): EdfDiagnosticDisposition;
35
+ export declare function severityOf(code: EdfDiagnosticCode): EdfSeverity;
36
+ /** Fires regardless of `strict`: proceeding would require inventing data. */
37
+ export declare function isAlwaysFatal(code: EdfDiagnosticCode): boolean;
38
+ //# sourceMappingURL=codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.d.ts","sourceRoot":"","sources":["../../src/diagnostics/codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEvD,gEAAgE;AAChE,MAAM,MAAM,wBAAwB,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,MAAM,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAE9B,iBAAiB,GACjB,kBAAkB,GAClB,sBAAsB,GACtB,uBAAuB,GACvB,yBAAyB,GACzB,kBAAkB,GAClB,mCAAmC,GACnC,wBAAwB,GAExB,0BAA0B,GAC1B,2BAA2B,GAC3B,wBAAwB,GACxB,yBAAyB,GACzB,mBAAmB,GAEnB,sBAAsB,GACtB,wBAAwB,GACxB,gBAAgB,GAChB,sBAAsB,GACtB,gBAAgB,GAChB,+BAA+B,GAC/B,4BAA4B,GAC5B,wBAAwB,GACxB,kCAAkC,GAClC,sBAAsB,GACtB,kBAAkB,GAClB,0BAA0B,GAC1B,4BAA4B,GAC5B,wBAAwB,GACxB,8BAA8B,GAC9B,yBAAyB,GACzB,sBAAsB,GACtB,wCAAwC,GACxC,wBAAwB,GACxB,yBAAyB,GACzB,+BAA+B,GAC/B,2BAA2B,GAC3B,eAAe,GACf,6BAA6B,GAC7B,+BAA+B,GAC/B,0BAA0B,GAC1B,gCAAgC,GAChC,kCAAkC,GAElC,2BAA2B,GAC3B,yBAAyB,GACzB,2BAA2B,CAAC;AA2GhC;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAEvE,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAC5C,MAAM,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAC1C,CAAC;AAEjB,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CAE/E;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,WAAW,CAK/D;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAE9D"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The diagnostic vocabulary.
3
+ *
4
+ * Layer 1. Imports nothing, so every other module may depend on it.
5
+ *
6
+ * A diagnostic is a *value*, never a log line. Each code has exactly one severity and one
7
+ * disposition, both fixed here:
8
+ *
9
+ * - `fatal` — edfcore cannot proceed without inventing something. Always throws, even when
10
+ * `strict` is false.
11
+ * - `deferred` — the header parses, but one signal cannot be scaled. `signal.scale` becomes
12
+ * `undefined`, `decodeDigital` keeps working, and `toPhysical` throws.
13
+ * - `warning` — the file is impolite but readable, and what we return is true.
14
+ * - `info` — the file is correct; the note exists because the situation surprises people.
15
+ */
16
+ const DISPOSITIONS = {
17
+ // ---- Always fatal --------------------------------------------------------
18
+ /** No recognisable EDF or BDF version block at offset 0. */
19
+ NOT_AN_EDF_FILE: 'fatal',
20
+ /** Fewer than 256 bytes, or fewer than 256*(ns+1). */
21
+ SOURCE_TOO_SMALL: 'fatal',
22
+ /** ns outside 1..9999, blank, or non-numeric. Every later offset is a multiple of it. */
23
+ SIGNAL_COUNT_INVALID: 'fatal',
24
+ /** A field the file geometry depends on failed its grammar end-to-end. */
25
+ NUMERIC_FIELD_INVALID: 'fatal',
26
+ /** '0,5' and '1,024' are indistinguishable; guessing turns 1024 into 1.024. */
27
+ COMMA_DECIMAL_SEPARATOR: 'fatal',
28
+ /** Every signal declares 0 samples per record, so records have no size to step by. */
29
+ RECORD_SIZE_ZERO: 'fatal',
30
+ /** EDF+ marker present but no annotation signal: no per-record timing exists, so any
31
+ * time we reported would be invented. */
32
+ EDFPLUS_WITHOUT_ANNOTATION_SIGNAL: 'fatal',
33
+ /** Record onsets went backwards. Every time-based answer would be wrong. */
34
+ TIMELINE_NOT_MONOTONIC: 'fatal',
35
+ // ---- Deferred-fatal: header parses, scaling does not -----------------------
36
+ /** digitalMinimum === digitalMaximum. Division by zero. EDF+ spec item 5. */
37
+ DEGENERATE_DIGITAL_RANGE: 'deferred',
38
+ /** physicalMinimum === physicalMaximum. Every sample would map to one value. */
39
+ DEGENERATE_PHYSICAL_RANGE: 'deferred',
40
+ /** digitalMinimum > digitalMaximum. Violates EDF+ item 5; we will not guess the intent. */
41
+ INVERTED_DIGITAL_RANGE: 'deferred',
42
+ /** Physical dimension is exactly 'Filtered': values are log-compressed (edffloat.html),
43
+ * so the linear formula would be wrong by orders of magnitude. Refused, not applied. */
44
+ LOG_TRANSFORMED_CHANNEL: 'deferred',
45
+ /** `toPhysical` was called on a signal with no scale and the specific cause was not
46
+ * re-derivable from the signal alone. Naming the wrong cause would be worse. */
47
+ SCALE_UNAVAILABLE: 'deferred',
48
+ // ---- Warnings: the file stays readable and what we return is true ----------
49
+ /** Header byte-count field disagrees with 256*(ns+1). The computed value always wins. */
50
+ HEADER_SIZE_MISMATCH: 'warning',
51
+ /** recordCount was -1 (writer never closed the file); recovered from the source length. */
52
+ RECORD_COUNT_RECOVERED: 'warning',
53
+ /** The file is shorter than its declared record count implies. */
54
+ TRUNCATED_FILE: 'warning',
55
+ /** A final partial record exists. Only whole records are exposed; nothing is zero-padded. */
56
+ PARTIAL_FINAL_RECORD: 'warning',
57
+ /** Bytes exist beyond the last whole data record. */
58
+ TRAILING_BYTES: 'warning',
59
+ /** Record exceeds the 61440-byte recommendation. */
60
+ RECORD_SIZE_ABOVE_RECOMMENDED: 'warning',
61
+ /** Reserved field is neither blank nor a recognised EDF+/BDF+ marker. */
62
+ NONSTANDARD_RESERVED_FIELD: 'warning',
63
+ /** A header field carries bytes outside printable ASCII. Decoded as Latin-1. */
64
+ NON_ASCII_HEADER_FIELD: 'warning',
65
+ /** A numeric field is right-justified; the spec requires left-justified. */
66
+ NUMERIC_FIELD_NOT_LEFT_JUSTIFIED: 'warning',
67
+ /** Header startdate and the recording-id Startdate disagree. Both are exposed; we pick no winner. */
68
+ DATE_FIELDS_DISAGREE: 'warning',
69
+ /** The date could not be parsed at all. */
70
+ DATE_UNPARSEABLE: 'warning',
71
+ /** Patient identification does not follow the EDF+ subfield grammar. */
72
+ PATIENT_ID_NONCONFORMANT: 'warning',
73
+ /** Recording identification does not follow the EDF+ subfield grammar. */
74
+ RECORDING_ID_NONCONFORMANT: 'warning',
75
+ /** Two signals share a label. `getSignal` by label will throw for these. */
76
+ DUPLICATE_SIGNAL_LABEL: 'warning',
77
+ /** Declared digital range exceeds what the sample width can represent. */
78
+ DIGITAL_RANGE_EXCEEDS_FORMAT: 'warning',
79
+ /** A signal declares 0 samples per record. */
80
+ ZERO_SAMPLES_PER_RECORD: 'warning',
81
+ /** Record duration is 0. Legal in EDF+, but sample rates become undefined. */
82
+ ZERO_RECORD_DURATION: 'warning',
83
+ /** An annotation signal's header fields deviate from the EDF+ requirements. */
84
+ ANNOTATION_SIGNAL_HEADER_NONCONFORMANT: 'warning',
85
+ /** An 'EDF Annotations' signal exists without an EDF+ marker in the reserved field.
86
+ * Annotations are parsed anyway; the channel is never exposed as an ordinary signal. */
87
+ MISSING_EDFPLUS_MARKER: 'warning',
88
+ /** A data record has no timekeeping TAL. */
89
+ TIMEKEEPING_TAL_MISSING: 'warning',
90
+ /** The timekeeping TAL exists but deviates (e.g. the widespread `+t 0x14 0x00` shorthand). */
91
+ TIMEKEEPING_TAL_NONCONFORMANT: 'warning',
92
+ /** Record 0's sub-second start offset fell outside [0, 1). */
93
+ START_OFFSET_OUT_OF_RANGE: 'warning',
94
+ /** A TAL did not match the grammar. That TAL is skipped; the file is kept. */
95
+ TAL_MALFORMED: 'warning',
96
+ /** A TAL ran past the end of its annotation region and was discarded. */
97
+ TAL_TRUNCATED_AT_REGION_END: 'warning',
98
+ /** The annotation region tail was not NUL padding. */
99
+ TAL_REGION_NOT_NUL_TERMINATED: 'warning',
100
+ /** Annotation text was not valid UTF-8; decoded as Latin-1 instead. */
101
+ ANNOTATION_TEXT_NOT_UTF8: 'warning',
102
+ /** Consecutive record onsets are not spaced by the record duration. */
103
+ RECORD_ONSET_SPACING_VIOLATION: 'warning',
104
+ /** A file marked EDF+C contains a real discontinuity. */
105
+ DISCONTINUITY_IN_CONTINUOUS_FILE: 'warning',
106
+ // ---- Info: correct, and deliberately not a warning ------------------------
107
+ /** A two-digit year was resolved through the spec's own 1985..2084 rule. Every conforming
108
+ * EDF file has a two-digit year, so this is the normal path, not a defect — it is
109
+ * reported only because the resolved century is worth being able to see. */
110
+ DATE_CLIPPED_TO_1985_2084: 'info',
111
+ /** physicalMinimum > physicalMaximum encodes a negative amplifier gain. Spec-sanctioned
112
+ * (EDF FAQ Q6). Never "fixed" — swapping them is a silent polarity flip. */
113
+ INVERTED_PHYSICAL_RANGE: 'info',
114
+ /** A negative annotation onset is normal for pre-stimulus events. */
115
+ NEGATIVE_ANNOTATION_ONSET: 'info',
116
+ };
117
+ export const DIAGNOSTIC_DISPOSITIONS = DISPOSITIONS;
118
+ /** Unknown codes are treated as warnings — an unrecognised note must never escalate. */
119
+ export function dispositionOf(code) {
120
+ return DISPOSITIONS[code] ?? 'warning';
121
+ }
122
+ export function severityOf(code) {
123
+ const disposition = dispositionOf(code);
124
+ if (disposition === 'fatal' || disposition === 'deferred')
125
+ return 'error';
126
+ if (disposition === 'warning')
127
+ return 'warning';
128
+ return 'info';
129
+ }
130
+ /** Fires regardless of `strict`: proceeding would require inventing data. */
131
+ export function isAlwaysFatal(code) {
132
+ return dispositionOf(code) === 'fatal';
133
+ }
134
+ //# sourceMappingURL=codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codes.js","sourceRoot":"","sources":["../../src/diagnostics/codes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The strict-vs-collect decision.
3
+ *
4
+ * Layer 1. Every diagnostic edfcore emits is built here, which is what makes two invariants
5
+ * structural rather than conventional:
6
+ *
7
+ * - `strict: true` throws `EdfFormatError` carrying the first would-be diagnostic, so under
8
+ * strict every `diagnostics` array is empty by construction;
9
+ * - a code whose disposition is `fatal` throws whether or not `strict` is set, because
10
+ * proceeding would require inventing data.
11
+ *
12
+ * The caller never passes a severity — it is derived from the code by `severityOf`, so one code
13
+ * cannot acquire two severities in two call sites.
14
+ */
15
+ import { EdfFormatError } from '../errors.js';
16
+ import type { EdfDiagnostic, ParseOptions } from '../types.js';
17
+ import { type EdfDiagnosticCode } from './codes.js';
18
+ /**
19
+ * Everything a diagnostic may carry except its severity. Optional here, required-or-undefined
20
+ * on `EdfDiagnostic`: `createDiagnostic` normalises between the two, which is what lets the
21
+ * result type stay exhaustive under `exactOptionalPropertyTypes`.
22
+ */
23
+ export interface DiagnosticInit {
24
+ readonly code: EdfDiagnosticCode;
25
+ /** Must name the field, the raw bytes as written, the rule, and an actionable next step. */
26
+ readonly message: string;
27
+ readonly field?: string;
28
+ readonly byteOffset?: number;
29
+ readonly byteLength?: number;
30
+ readonly rawBytes?: Uint8Array;
31
+ readonly raw?: string;
32
+ readonly expected?: string;
33
+ readonly actual?: string;
34
+ readonly signalIndex?: number;
35
+ readonly recordIndex?: number;
36
+ /** A spec clause, e.g. 'EDF+ additional specification 5'. */
37
+ readonly specReference?: string;
38
+ }
39
+ export declare function createDiagnostic(init: DiagnosticInit): EdfDiagnostic;
40
+ /**
41
+ * The one way to turn a diagnostic into the error that carries it. `EdfFormatError` re-derives
42
+ * `field`/`byteOffset`/`signalIndex`/`recordIndex` from the diagnostic, so they are not repeated.
43
+ */
44
+ export declare function toFormatError(diagnostic: EdfDiagnostic, cause?: unknown): EdfFormatError;
45
+ /**
46
+ * `toFormatError(createDiagnostic(init))`, for the paths that throw without a sink — an always
47
+ * fatal code detected before one exists, or where the type system needs the `throw` to be
48
+ * visible at the call site.
49
+ */
50
+ export declare function fatalError(init: DiagnosticInit, cause?: unknown): EdfFormatError;
51
+ export declare class DiagnosticSink {
52
+ #private;
53
+ /**
54
+ * Readable so a caller can skip building a message it is about to throw away. Never so a
55
+ * caller can re-implement the decision in `report`.
56
+ */
57
+ readonly strict: boolean;
58
+ constructor(options?: ParseOptions);
59
+ get size(): number;
60
+ /**
61
+ * Records the diagnostic, or throws `EdfFormatError` when the code is always fatal, or when
62
+ * `strict` is set and the code describes an actual defect. There is no severity parameter
63
+ * by design.
64
+ *
65
+ * `info` codes are exempt from `strict`. They exist to explain something that is correct but
66
+ * surprising — a spec-sanctioned negative gain, a pre-stimulus onset, the mandated two-digit
67
+ * year rule — so throwing on one would contradict what the severity means, and would make
68
+ * `strict` reject conforming files. Every `info` note is still collected and readable.
69
+ */
70
+ report(init: DiagnosticInit): void;
71
+ /** A frozen copy: an array already attached to a result must not grow if reporting continues. */
72
+ get diagnostics(): readonly EdfDiagnostic[];
73
+ /** `diagnostics`, then reset — for a sink reused across records. */
74
+ drain(): readonly EdfDiagnostic[];
75
+ }
76
+ //# sourceMappingURL=collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../src/diagnostics/collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,cAAc,EAA2B,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,KAAK,iBAAiB,EAA6B,MAAM,YAAY,CAAC;AAE/E;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAkBpE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,cAAc,CAMxF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,cAAc,CAEhF;AAED,qBAAa,cAAc;;IACzB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAIb,OAAO,CAAC,EAAE,YAAY;IAIlC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAQlC,iGAAiG;IACjG,IAAI,WAAW,IAAI,SAAS,aAAa,EAAE,CAE1C;IAED,oEAAoE;IACpE,KAAK,IAAI,SAAS,aAAa,EAAE;CAKlC"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The strict-vs-collect decision.
3
+ *
4
+ * Layer 1. Every diagnostic edfcore emits is built here, which is what makes two invariants
5
+ * structural rather than conventional:
6
+ *
7
+ * - `strict: true` throws `EdfFormatError` carrying the first would-be diagnostic, so under
8
+ * strict every `diagnostics` array is empty by construction;
9
+ * - a code whose disposition is `fatal` throws whether or not `strict` is set, because
10
+ * proceeding would require inventing data.
11
+ *
12
+ * The caller never passes a severity — it is derived from the code by `severityOf`, so one code
13
+ * cannot acquire two severities in two call sites.
14
+ */
15
+ import { EdfFormatError } from '../errors.js';
16
+ import { isAlwaysFatal, severityOf } from './codes.js';
17
+ export function createDiagnostic(init) {
18
+ return {
19
+ code: init.code,
20
+ severity: severityOf(init.code),
21
+ message: init.message,
22
+ field: init.field,
23
+ byteOffset: init.byteOffset,
24
+ byteLength: init.byteLength,
25
+ // Copied, not aliased: a diagnostic outlives the read that produced it, and the caller's
26
+ // view is typically a subarray of a buffer an I/O adapter is free to reuse.
27
+ rawBytes: init.rawBytes === undefined ? undefined : init.rawBytes.slice(),
28
+ raw: init.raw,
29
+ expected: init.expected,
30
+ actual: init.actual,
31
+ signalIndex: init.signalIndex,
32
+ recordIndex: init.recordIndex,
33
+ specReference: init.specReference,
34
+ };
35
+ }
36
+ /**
37
+ * The one way to turn a diagnostic into the error that carries it. `EdfFormatError` re-derives
38
+ * `field`/`byteOffset`/`signalIndex`/`recordIndex` from the diagnostic, so they are not repeated.
39
+ */
40
+ export function toFormatError(diagnostic, cause) {
41
+ const init = cause === undefined
42
+ ? { code: diagnostic.code, diagnostic }
43
+ : { code: diagnostic.code, diagnostic, cause };
44
+ return new EdfFormatError(`[${diagnostic.code}] ${diagnostic.message}`, init);
45
+ }
46
+ /**
47
+ * `toFormatError(createDiagnostic(init))`, for the paths that throw without a sink — an always
48
+ * fatal code detected before one exists, or where the type system needs the `throw` to be
49
+ * visible at the call site.
50
+ */
51
+ export function fatalError(init, cause) {
52
+ return toFormatError(createDiagnostic(init), cause);
53
+ }
54
+ export class DiagnosticSink {
55
+ /**
56
+ * Readable so a caller can skip building a message it is about to throw away. Never so a
57
+ * caller can re-implement the decision in `report`.
58
+ */
59
+ strict;
60
+ #collected = [];
61
+ constructor(options) {
62
+ this.strict = options?.strict === true;
63
+ }
64
+ get size() {
65
+ return this.#collected.length;
66
+ }
67
+ /**
68
+ * Records the diagnostic, or throws `EdfFormatError` when the code is always fatal, or when
69
+ * `strict` is set and the code describes an actual defect. There is no severity parameter
70
+ * by design.
71
+ *
72
+ * `info` codes are exempt from `strict`. They exist to explain something that is correct but
73
+ * surprising — a spec-sanctioned negative gain, a pre-stimulus onset, the mandated two-digit
74
+ * year rule — so throwing on one would contradict what the severity means, and would make
75
+ * `strict` reject conforming files. Every `info` note is still collected and readable.
76
+ */
77
+ report(init) {
78
+ const diagnostic = createDiagnostic(init);
79
+ if (isAlwaysFatal(diagnostic.code) || (this.strict && diagnostic.severity !== 'info')) {
80
+ throw toFormatError(diagnostic);
81
+ }
82
+ this.#collected.push(diagnostic);
83
+ }
84
+ /** A frozen copy: an array already attached to a result must not grow if reporting continues. */
85
+ get diagnostics() {
86
+ return Object.freeze(this.#collected.slice());
87
+ }
88
+ /** `diagnostics`, then reset — for a sink reused across records. */
89
+ drain() {
90
+ const collected = this.diagnostics;
91
+ this.#collected = [];
92
+ return collected;
93
+ }
94
+ }
95
+ //# sourceMappingURL=collector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collector.js","sourceRoot":"","sources":["../../src/diagnostics/collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,cAAc,EAA2B,MAAM,cAAc,CAAC;AAEvE,OAAO,EAA0B,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwB/E,MAAM,UAAU,gBAAgB,CAAC,IAAoB;IACnD,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QAC/B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,yFAAyF;QACzF,4EAA4E;QAC5E,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE;QACzE,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,aAAa,EAAE,IAAI,CAAC,aAAa;KAClC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,UAAyB,EAAE,KAAe;IACtE,MAAM,IAAI,GACR,KAAK,KAAK,SAAS;QACjB,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE;QACvC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACnD,OAAO,IAAI,cAAc,CAAC,IAAI,UAAU,CAAC,IAAI,KAAK,UAAU,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAoB,EAAE,KAAe;IAC9D,OAAO,aAAa,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,OAAO,cAAc;IACzB;;;OAGG;IACM,MAAM,CAAU;IAEzB,UAAU,GAAoB,EAAE,CAAC;IAEjC,YAAY,OAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;IAChC,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,IAAoB;QACzB,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,QAAQ,KAAK,MAAM,CAAC,EAAE,CAAC;YACtF,MAAM,aAAa,CAAC,UAAU,CAAC,CAAC;QAClC,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAED,iGAAiG;IACjG,IAAI,WAAW;QACb,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Rendering diagnostics for humans.
3
+ *
4
+ * Layer 1. Layout only: by the message contract a diagnostic's own message already names the
5
+ * field, the raw bytes as written, the rule and the next step, so this module adds structure —
6
+ * severity marker, code, location, the bytes as hex — and invents no wording.
7
+ *
8
+ * Output is deterministic and asserted as such: no locale-sensitive number or date formatting,
9
+ * no iteration over an unordered collection, and no ANSI escapes unless `color` is requested.
10
+ */
11
+ import type { EdfDiagnostic } from '../types.js';
12
+ export interface FormatDiagnosticsOptions {
13
+ readonly color?: boolean;
14
+ readonly maxItems?: number;
15
+ }
16
+ /**
17
+ * A multi-line report, one block per diagnostic. Returns `''` for an empty list so the result
18
+ * can be concatenated into a larger report without a stray blank line.
19
+ */
20
+ export declare function formatDiagnostics(diagnostics: readonly EdfDiagnostic[], options?: FormatDiagnosticsOptions): string;
21
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/diagnostics/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AAE9D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAgBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,SAAS,aAAa,EAAE,EACrC,OAAO,CAAC,EAAE,wBAAwB,GACjC,MAAM,CAgBR"}