@welldot/core 0.3.0 → 0.4.0-next.20261014.26249fd

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.
@@ -0,0 +1,483 @@
1
+ # `.well` File Format Specification — Version 2.3: Water Quality
2
+
3
+ **See also:** [overview.md](./overview.md) · [format-reference.md](./format-reference.md) · [object-schemas.md](./object-schemas.md) · [interoperability.md](./interoperability.md)
4
+
5
+ This document specifies the `water_samples` block _(since v2.3)_: the sample and result schemas, the recommended values, the unit rules, the `welldot` parameter vocabulary and how jurisdiction requirements are expressed. Cross-cutting rules that also cover water samples stay in their general sections: datetimes and the `_resolution: "day"` convention, canonical units and field bindings, cross-references, uniqueness and precision in [format-reference.md](format-reference.md); attachments, ledger corrections and `history_logs` links in [object-schemas.md](object-schemas.md). The Complete Example in object-schemas.md includes water samples.
6
+
7
+ ## Contents
8
+
9
+ - [`water_samples[]`](#water_samples)
10
+ - [Recommended values](#recommended-values)
11
+ - [Units](#units)
12
+ - [Parameter vocabulary](#parameter-vocabulary)
13
+ - [Regulatory profiles](#regulatory-profiles)
14
+
15
+ ---
16
+
17
+ ## `water_samples[]`
18
+
19
+ A ledger: each entry is one collection of water from the well, with the results measured on it in the field and in the laboratory. Entries are never edited in place. A corrected laboratory report, or a revalidation of data already recorded, is a new sample whose `corrects` holds the id of the retracted sample, following object-schemas.md § `hydrodynamic_events[]` — Corrections (retracted samples stay in the file and are excluded from every derivation; chains are allowed; a cycle emits a warning).
20
+
21
+ The block serves drinking-water compliance, environmental and mining monitoring under licence, and hydrochemistry, in any jurisdiction. It follows the rigor of audited monitoring (chain of custody, field QA/QC, filtration, lab flags, data validation) with almost every field optional. It is jurisdiction-neutral: limits, exceedances and national rules are never stored; see § Parameter vocabulary — Limit sets are not part of the file.
22
+
23
+ Out of scope: limits and exceedances, sampling schedules, high-frequency telemetry (only aggregates enter), interpreted assessment (class, facies), and the laboratory's internal QC (method blanks, matrix spikes).
24
+
25
+ ### `WaterSample`
26
+
27
+ | Field | Type | Required | Unit | Description |
28
+ | ----------------------- | ---------------- | ----------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29
+ | `id` | string | yes | | Unique within `water_samples`. UUID v4 recommended. |
30
+ | `datetime` | string (instant) | yes | | RFC 3339 instant of collection. |
31
+ | `sample_type` | string | yes | | `routine`, `field_duplicate`, `split_sample`, `field_blank`, `trip_blank`, `equipment_blank`, `x-…`. See format-reference.md § `water_samples[].sample_type`. |
32
+ | `parent_sample_id` | string | conditional | | `water_samples[].id` of the original sample. Expected for `field_duplicate` and `split_sample` (absence emits a warning). |
33
+ | `sequence` | integer | no | | Tie-breaker among samples at the same instant, lower first. Orders the samples of a vertical profile. |
34
+ | `campaign` | string | no | | Free identifier of the sampling campaign. |
35
+ | `sampling_method` | string | no | | How the well was purged: `low_flow`, `volumetric_purge`, `no_purge`, `pump_discharge`, `x-…`. |
36
+ | `sampling_point` | `SamplingPoint` | no | | Where the water was taken. See below. |
37
+ | `purge` | `Purge` | no | | Purge before collection. See below. |
38
+ | `static_level_event_id` | string | no | | `hydrodynamic_events[].id` with the water level measured at collection. |
39
+ | `collected_by` | string | no | | Person or team that collected the sample. |
40
+ | `preservation` | string | no | | Preservation and packaging, free text. |
41
+ | `chain_of_custody` | string | no | | Chain of custody number. |
42
+ | `laboratory` | `Laboratory` | no | | See below. |
43
+ | `corrects` | string | no | | `water_samples[].id` of the sample this one retracts. |
44
+ | `notes` | string | no | | |
45
+ | `attachments` | `Attachment[]` | no | | The laboratory report with `document_type: "lab_report"`. See § Attachment. |
46
+ | `results` | `Result[]` | yes | | At least one. Field and laboratory results in one list, distinguished by `measured_in`. |
47
+
48
+ ### `SamplingPoint`
49
+
50
+ | Field | Type | Required | Unit | Description |
51
+ | ---------------------- | ------ | -------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `type` | string | yes | | `pump_discharge`, `wellhead_tap`, `in_well`, `x-…`. See format-reference.md § `water_samples[].sampling_point.type`. |
53
+ | `depth` | number | no | m | Point depth of the sampler intake, from ground level. Mutually exclusive with `from`/`to`. |
54
+ | `depth_precision` | number | no | m | One-sigma precision of `depth`. |
55
+ | `from` | number | no | m | Top of the isolated interval. |
56
+ | `to` | number | no | m | Bottom of the isolated interval. |
57
+ | `device` | string | no | | What collected the water: `bailer`, `discrete_depth_sampler`, `passive_diffusion_bag`, `grab_sleeve`, `low_flow_pump`, `packer_pump`, `x-…`. |
58
+ | `pump_installation_id` | string | no | | `pump_installations[].id`, when the sample is taken at the production pump. |
59
+
60
+ A sample's depth is a point, an interval, derived from the production pump, or unknown. All depths are measured from ground level.
61
+
62
+ | Situation | `type` | Geometry | Example |
63
+ | ----------------- | ---------------- | ------------------------------------------------------------- | ---------------------------------- |
64
+ | Production pump | `pump_discharge` | `pump_installation_id`; depth derived from its `intake_depth` | Supply well |
65
+ | Wellhead tap | `wellhead_tap` | None | Routine potability |
66
+ | Point sampler | `in_well` | `depth` | Bailer, HydraSleeve, diffusion bag |
67
+ | Low-flow | `in_well` | `depth` of the sampling pump intake | ASTM D6771, ISO 5667-11 |
68
+ | Isolated interval | `in_well` | `from` and `to` | Packer, multilevel well |
69
+ | Unknown | `in_well` | None | Historical record without depth |
70
+
71
+ - `depth` together with `from` or `to` is malformed.
72
+ - With `pump_installation_id`, the depth is not repeated on the sampling point.
73
+ - Field measurements taken from the top of casing are converted to ground level by the producer, by the same rule as water levels.
74
+ - A vertical profile or a multilevel well is several samples at different depths, with the same `campaign` and `sequence` for ordering.
75
+
76
+ ### `Purge`
77
+
78
+ All fields optional.
79
+
80
+ | Field | Type | Unit | Description |
81
+ | ------------ | ---------------- | ---- | --------------------------------------------------- |
82
+ | `duration` | number | min | Purge duration. |
83
+ | `volume` | number | m³ | Purged volume. |
84
+ | `flow_rate` | number | m³/h | Purge flow rate. |
85
+ | `stabilized` | boolean | | Field parameters were stabilized before collection. |
86
+ | `readings` | `PurgeReading[]` | | Stabilization readings taken during the purge. |
87
+
88
+ ### `PurgeReading`
89
+
90
+ | Field | Type | Required | Unit | Description |
91
+ | ----------- | ----------- | -------- | --------- | ---------------------------------------- |
92
+ | `elapsed` | number | yes | min | Time since the purge started. |
93
+ | `parameter` | `Parameter` | yes | | `{ code, vocabulary }`, as in `results`. |
94
+ | `value` | number | yes | canonical | Value in the parameter's canonical unit. |
95
+
96
+ `purge.readings` holds the stabilization series (pH, conductivity, temperature, dissolved oxygen, ORP, turbidity) that audits require. The final values at the time of collection go in `results` with `measured_in: "field"`.
97
+
98
+ ### `Laboratory`
99
+
100
+ | Field | Type | Required | Unit | Description |
101
+ | ------------------------ | ---------------- | -------- | ---- | ----------------------------------------------------------------------------------------- |
102
+ | `name` | string | yes | | |
103
+ | `accreditation` | string | no | | ISO/IEC 17025 (or equivalent) accreditation identifier. |
104
+ | `report_number` | string | no | | Report number, preserved as issued. |
105
+ | `batch_id` | string | no | | Laboratory batch or work order. |
106
+ | `sample_id` | string | no | | The laboratory's own sample identifier. |
107
+ | `received_at` | string (instant) | no | | When the laboratory received the sample. |
108
+ | `received_at_resolution` | string | no | | `day` when only the date is known. See format-reference.md § Instants known only by date. |
109
+ | `received_temperature` | number | no | °C | Sample temperature on receipt. |
110
+
111
+ ### `Result`
112
+
113
+ | Field | Type | Required | Unit | Description |
114
+ | ------------------------ | ---------------- | --------- | --------- | ----------------------------------------------------------------------------- |
115
+ | `parameter` | `Parameter` | yes | | What was measured. See below. |
116
+ | `value` | number | one form | canonical | Numeric result. |
117
+ | `presence` | boolean | one form | | Presence (`true`) or absence (`false`). |
118
+ | `text` | string | one form | | Qualitative result (odor, taste). |
119
+ | `qualifier` | string (enum) | no | | `<`, `>`, `not_detected`, `estimated`. Censoring and estimation only. |
120
+ | `unit` | string | `x-` only | | UCUM unit. Required for `x-` vocabularies, malformed for `welldot` and `cas`. |
121
+ | `detection_limit` | number | no | canonical | Detection limit (LD / LOD). |
122
+ | `quantification_limit` | number | no | canonical | Quantification limit (LQ / LOQ). |
123
+ | `value_precision` | number | no | canonical | One-sigma uncertainty. Expanded uncertainty from a report is divided by k. |
124
+ | `fraction` | string (enum) | no | | `total`, `dissolved`, `suspended`. |
125
+ | `filtration` | `Filtration` | no | | Filtration applied before analysis. Expected with `fraction: "dissolved"`. |
126
+ | `measured_in` | string (enum) | no | | `field` or `lab`. |
127
+ | `method` | string | no | | Analytical method, e.g. `ISO 7027`, `US EPA 200.8`, `SMEWW 4500-NO3 B`. |
128
+ | `analyzed_at` | string (instant) | no | | When the result was analyzed. |
129
+ | `analyzed_at_resolution` | string | no | | `day` when only the date is known. |
130
+ | `lab_flags` | string[] | no | | Laboratory flags as issued, uninterpreted. |
131
+ | `validation` | `Validation` | no | | Data validation by a reviewer. See below. |
132
+ | `notes` | string | no | | |
133
+
134
+ "Canonical" is the unit of the parameter in the vocabulary, or `unit` for `x-` codes. See § Units.
135
+
136
+ ### `Parameter`
137
+
138
+ | Field | Type | Required | Description |
139
+ | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------- |
140
+ | `code` | string | yes | Code in the vocabulary: a `welldot` code, a CAS Registry Number (`71-43-2`) or a custom code. |
141
+ | `vocabulary` | string | yes | `welldot`, `cas` or `x-…`. Any other value is malformed. |
142
+
143
+ The `welldot` codes, their units, value forms and CAS equivalences are listed in § Parameter vocabulary.
144
+
145
+ ### `Filtration`
146
+
147
+ All fields optional.
148
+
149
+ | Field | Type | Unit | Description |
150
+ | ----------- | ------------- | ---- | ------------------------------ |
151
+ | `pore_size` | number | µm | Filter pore size, e.g. `0.45`. |
152
+ | `location` | string (enum) | | `field` or `lab`. |
153
+
154
+ ### `Validation`
155
+
156
+ | Field | Type | Required | Description |
157
+ | -------------- | ---------------- | -------- | -------------------------------------------------------------- |
158
+ | `status` | string (enum) | yes | `unvalidated`, `validated`, `qualified`, `rejected`. |
159
+ | `qualifier` | string | no | Code of the validation guideline applied, e.g. `J`, `UJ`, `R`. |
160
+ | `guideline` | string | no | Guideline used, free text. |
161
+ | `validated_by` | string | no | |
162
+ | `validated_at` | string (instant) | no | |
163
+
164
+ An absent `validation` is equivalent to `status: "unvalidated"`.
165
+
166
+ ### Result rules
167
+
168
+ Each result has exactly one value form. Three annotation layers are kept apart: the censoring qualifier, the laboratory flags and the reviewer's validation.
169
+
170
+ | Form | Field | Used for | `qualifier` allowed |
171
+ | ------------ | ---------- | ------------------------------------------------ | -------------------------------------- |
172
+ | Numeric | `value` | Almost every parameter | `<`, `>`, `estimated` |
173
+ | Presence | `presence` | Presence codes (`total_coliforms`, `e_coli`) | None |
174
+ | Text | `text` | Odor, taste and other qualitative results | None |
175
+ | Not detected | none | Below the detection limit with no reported value | `not_detected`, with `detection_limit` |
176
+
177
+ | Layer | Field | Set by | Example |
178
+ | ---------------- | ------------ | ----------------------------------- | --------------------------------------------- |
179
+ | Censoring | `qualifier` | Normalized on import | `<` 0.001 |
180
+ | Laboratory flags | `lab_flags` | The laboratory, preserved as issued | `B` (blank contamination), `H` (holding time) |
181
+ | Validation | `validation` | The data reviewer | `qualified` with `J` |
182
+
183
+ - `<` with `value` records the value the laboratory reported, usually the quantification or detection limit. It is neither zero nor absent.
184
+ - `>` covers upper bounds, such as counts above the method range.
185
+ - `estimated` marks values between the detection and quantification limits.
186
+ - `not_detected` without `detection_limit` emits a warning, because the result has no scale.
187
+ - `value_precision` is one sigma. Reports give an expanded uncertainty (usually k = 2); converters divide it by the coverage factor.
188
+ - `fraction: "dissolved"` is expected with `filtration`; without it, a warning is emitted. Dissolved metals filtered in the laboratory and in the field are not comparable in an audit.
189
+ - `lab_flags` has no vocabulary: codes vary by laboratory and country, and interpreting them is the job of validation.
190
+ - A `rejected` result is excluded from every derivation and from comparisons with limits. `qualified` results are kept.
191
+ - Revalidating the data of a recorded sample creates a new sample with `corrects`, like any ledger correction.
192
+ - The same parameter may appear twice in a sample when `fraction` or `measured_in` differ (total and dissolved iron; field and laboratory pH). An exact duplicate emits a warning.
193
+ - A `welldot` code and its CAS equivalent are the same parameter for duplicate detection and derivations.
194
+
195
+ ### Malformed (rejected)
196
+
197
+ - `results` empty or absent.
198
+ - A result with more than one value form, or none without `qualifier: "not_detected"`; `not_detected` together with a value form; `<`, `>` or `estimated` without `value`.
199
+ - `unit` on a `welldot` or `cas` parameter; an `x-` parameter without `unit`; a `vocabulary` other than `welldot`, `cas` or `x-…`.
200
+ - `sampling_point.depth` together with `from` or `to`.
201
+ - An `_resolution` field with a value other than `day`; an instant without an offset.
202
+ - A value outside the closed enums `qualifier`, `fraction`, `measured_in`, `filtration.location`, `validation.status`. `sample_type`, `sampling_method`, `sampling_point.type` and `device` are open vocabularies.
203
+
204
+ ### Validation (warnings, never rejection)
205
+
206
+ - `field_duplicate` or `split_sample` without `parent_sample_id`.
207
+ - `fraction: "dissolved"` without `filtration`.
208
+ - A sample depth outside every `well_screen` interval (stagnant casing water).
209
+ - A sample depth below `well_depth`.
210
+ - A sample depth above the static level of `static_level_event_id`.
211
+ - `presence` on a numeric code, or `value` on a presence code (value form different from the vocabulary's).
212
+ - `not_detected` without `detection_limit`.
213
+ - The same parameter, `fraction` and `measured_in` repeated in one sample.
214
+ - A `welldot` code unknown to the vocabulary version.
215
+ - `analyzed_at` or `laboratory.received_at` earlier than the collection `datetime`.
216
+ - A reference that does not resolve: `corrects`, `parent_sample_id`, `static_level_event_id`, `sampling_point.pump_installation_id`.
217
+ - A `corrects` cycle.
218
+
219
+ ### Derived values
220
+
221
+ Derived values are computed by `@welldot/utils` and never stored. Retracted samples and `rejected` results are excluded from all of them.
222
+
223
+ - Exceedances against a selectable limit set (WHO, EU 2020/2184, Brazil, …) or the user's own limits. `<` and `not_detected` results never exceed; for presence limits, presence exceeds. `turbidity`, `turbidity_ntu` and `turbidity_fnu` compare with nephelometric limits, and `turbidity_fau` is never compared.
224
+ - Ion balance, converting mg/L to meq/L with the molar mass and charge of each code.
225
+ - Relative percent difference (RPD) between a duplicate or split and its original sample, per parameter.
226
+ - Blank contamination: parameters detected in `field_blank`, `trip_blank` or `equipment_blank` samples of the same `campaign`.
227
+ - Holding time, from `datetime` to `analyzed_at`, at the available resolution.
228
+ - Receipt temperature compliance.
229
+ - Purge stabilization from `purge.readings`.
230
+ - Acid drainage indicators (net alkalinity, sulfate/chloride ratio).
231
+ - Piper and Stiff coordinates and the hydrochemical facies.
232
+ - Effective sample depth (from `sampling_point`, or the pump's `intake_depth`) and whether the sample represents formation water (inside a screen interval and below the static level).
233
+
234
+ ### History log links
235
+
236
+ A `history_logs` entry (typically `maintenance` with `maintenance_type: "water_sampling"`, or an `inspection`) records the task and points to the samples it collected with `sample_ids`. A water-quality `monitoring` permit condition is fulfilled by a condition fulfillment pointing to the sample the same way. See object-schemas.md § `history_logs[]` — Data links and § `permits[]` — Condition fulfillment.
237
+
238
+ ---
239
+
240
+ ## Recommended values
241
+
242
+ Non-canonical values of these open vocabularies SHOULD use the `x-` prefix.
243
+
244
+ ### `water_samples[].sample_type` — Recommended values _(since v2.3)_
245
+
246
+ | Value | Portuguese (BR) | Description |
247
+ | ----------------- | ------------------------ | ------------------------------------------------------------------------------------- |
248
+ | `routine` | Amostra de rotina | Regular sample of the well water. |
249
+ | `field_duplicate` | Duplicata de campo | Second sample collected at the same time and point. Requires `parent_sample_id`. |
250
+ | `split_sample` | Amostra dividida (split) | One sample split between laboratories (interlaboratory). Requires `parent_sample_id`. |
251
+ | `field_blank` | Branco de campo | Analyte-free water exposed to field conditions. |
252
+ | `trip_blank` | Branco de transporte | Analyte-free water that travels with the samples, unopened. |
253
+ | `equipment_blank` | Branco de equipamento | Analyte-free water passed through the sampling equipment after decontamination. |
254
+
255
+ Non-canonical values SHOULD use the `x-` prefix. Laboratory internal QC (method blanks, matrix spikes) is out of scope.
256
+
257
+ ### `water_samples[].sampling_method` — Recommended values _(since v2.3)_
258
+
259
+ `sampling_method` describes the purge; `sampling_point.device` describes what collected the water. Portuguese terms follow ABNT NBR 15847 (métodos de purga).
260
+
261
+ | Value | Portuguese (BR) | Description |
262
+ | ------------------ | --------------------------------- | --------------------------------------------------------------------------------------- |
263
+ | `low_flow` | Purga de baixa vazão (micropurga) | Low-flow purging with stabilization of field parameters (e.g. ASTM D6771, ISO 5667-11). |
264
+ | `volumetric_purge` | Purga de volume determinado | Removal of a set number of well volumes before sampling. |
265
+ | `no_purge` | Amostragem sem purga | Passive or no-purge sampling. |
266
+ | `pump_discharge` | Bomba de produção em operação | Collected from the running production pump. |
267
+
268
+ Non-canonical values SHOULD use the `x-` prefix.
269
+
270
+ ### `water_samples[].sampling_point.type` — Recommended values _(since v2.3)_
271
+
272
+ | Value | Portuguese (BR) | Description |
273
+ | ---------------- | -------------------------- | --------------------------------------------------------------------------- |
274
+ | `pump_discharge` | Saída da bomba de produção | Production pump discharge; depth derived from `pump_installation_id`. |
275
+ | `wellhead_tap` | Torneira do cavalete | Tap at the wellhead; no depth. |
276
+ | `in_well` | Dentro do poço | Sampler inside the well, at `depth`, over `from`/`to`, or at unknown depth. |
277
+
278
+ Non-canonical values SHOULD use the `x-` prefix.
279
+
280
+ ### `water_samples[].sampling_point.device` — Recommended values _(since v2.3)_
281
+
282
+ | Value | Portuguese (BR) |
283
+ | ------------------------ | ------------------------------------------ |
284
+ | `bailer` | Bailer (amostrador de retenção) |
285
+ | `discrete_depth_sampler` | Amostrador pontual de profundidade |
286
+ | `passive_diffusion_bag` | Bolsa de difusão passiva |
287
+ | `grab_sleeve` | Amostrador passivo tipo luva (HydraSleeve) |
288
+ | `low_flow_pump` | Bomba de baixa vazão |
289
+ | `packer_pump` | Bomba com obturador (packer) |
290
+
291
+ Non-canonical values SHOULD use the `x-` prefix.
292
+
293
+ ---
294
+
295
+ ## Units
296
+
297
+ A water quality result never declares a unit for a known parameter: the unit comes from the parameter code.
298
+
299
+ 1. **Substances** (vocabulary `welldot` or `cas`) are always in mg/L. µg/L, ng/L and g/L are converted on import.
300
+ 2. **Parameters that are not concentrations** (pH, conductivity, temperature, redox potential, turbidity, color, microbiology, radioactivity) have the unit fixed by their `welldot` code; see § Parameter vocabulary. pH is dimensionless.
301
+ 3. **`x-` vocabulary codes** require `unit`, in UCUM. This is the only exception to the rule that the file never declares units. `unit` on a `welldot` or `cas` parameter is malformed, and an `x-` code without `unit` is malformed.
302
+ 4. **Turbidity and microbiological counts** are separated by code, not by unit: `turbidity_ntu`, `turbidity_fnu`, `turbidity_fau` and the generic `turbidity` (FTU, formazin with no identified method); `_mpn` and `_cfu` count codes beside the presence codes.
303
+
304
+ Regional unit names are normalized on import:
305
+
306
+ | Reported unit | Normalized to |
307
+ | -------------------- | ------------------------------------------------- |
308
+ | µg/L, ng/L, g/L | mg/L |
309
+ | mS/m | µS/cm (×10) |
310
+ | mS/cm | µS/cm (×1000) |
311
+ | uT, FTU | generic `turbidity` code |
312
+ | NTU, FNU, FAU | `turbidity_ntu`, `turbidity_fnu`, `turbidity_fau` |
313
+ | mg Pt-Co/L, PCU, TCU | uH (numerically equal) |
314
+
315
+ The basis of expression lives in the code, never in the unit: nitrate reported "as N" and "as NO₃⁻" are different parameters (`nitrate_as_n`, `nitrate_as_no3`). `@welldot/core` provides `concentrationToCanonical`, `milliSiemensPerMeterToMicroSiemensPerCm`, `milliSiemensPerCmToMicroSiemensPerCm`, `turbidityCodeForUnit`, `normalizeColorUnit` and `expandedToStandardUncertainty` for these conversions.
316
+
317
+ ---
318
+
319
+ ## Parameter vocabulary
320
+
321
+ A parameter is identified by an object `{ code, vocabulary }`, like `texture`:
322
+
323
+ ```json
324
+ { "code": "nitrate_as_n", "vocabulary": "welldot" }
325
+ { "code": "71-43-2", "vocabulary": "cas" }
326
+ { "code": "glyphosate_ampa", "vocabulary": "x-lab-codes" }
327
+ ```
328
+
329
+ `vocabulary` is `welldot`, `cas` or an `x-…` vocabulary; any other value is malformed.
330
+
331
+ ### Vocabulary rules
332
+
333
+ 1. **Substances** (`welldot` or `cas`) are always in mg/L. Conversions such as µg/L → mg/L happen on import.
334
+ 2. **Parameters that are not concentrations** have their unit fixed by the `welldot` vocabulary (pH, conductivity, temperature, redox potential, turbidity, color, microbiology, radioactivity).
335
+ 3. **`x-` codes** require `unit`, in UCUM. This is the only exception to the spec's unit rule.
336
+ 4. **The basis of expression is part of the code.** `nitrate_as_n` and `nitrate_as_no3` are different parameters. A CAS code always means the substance itself. There is no `basis` field.
337
+ 5. **Preference.** When a `welldot` code exists it is used instead of the CAS number. The vocabulary publishes the equivalences, and parsers treat a `welldot` code and its equivalent CAS number as the same parameter.
338
+ 6. **Labels and translations.** The canonical label is English. Translations, regional synonyms and descriptions live in the published vocabulary, which is versioned (`WATER_QUALITY_VOCABULARY_VERSION` in `@welldot/core`).
339
+ 7. **Regional unit names** are normalized on import: uT and FTU → `turbidity`; mg Pt-Co/L, PCU and TCU → uH; mS/m → µS/cm (×10). See § Units.
340
+
341
+ A `welldot` code unknown to the vocabulary version emits a warning, never a rejection. Any other substance is recorded by its CAS number, in mg/L.
342
+
343
+ ### Core vocabulary
344
+
345
+ The core list has 98 `welldot` codes. The Mining and redox group and the additional metals cover acid drainage, gold leaching (cyanide) and explosives (nitrogen species). The Form column gives the value form a result of that code uses (`value`, `presence` or `text`); see § `water_samples[]` — Result rules.
346
+
347
+ | Group | Code | Label (EN) | Portuguese (BR) | Unit | Form | CAS |
348
+ | ---------------- | --------------------------------- | ----------------------------------------------------- | ------------------------------------------- | ---------- | -------- | ---------- |
349
+ | Physical / field | `temperature` | Temperature | Temperatura | °C | value | |
350
+ | Physical / field | `ph` | pH | pH | — | value | |
351
+ | Physical / field | `specific_conductance` | Specific conductance at 25 °C | Condutividade elétrica a 25 °C | µS/cm | value | |
352
+ | Physical / field | `conductivity_uncompensated` | Electrical conductivity, uncompensated | Condutividade sem compensação | µS/cm | value | |
353
+ | Physical / field | `dissolved_oxygen` | Dissolved oxygen | Oxigênio dissolvido | mg/L | value | |
354
+ | Physical / field | `orp` | Oxidation-reduction potential vs. reference electrode | Potencial redox vs. eletrodo de referência | mV | value | |
355
+ | Physical / field | `eh` | Redox potential vs. SHE | Potencial redox corrigido vs. EPH | mV | value | |
356
+ | Physical / field | `turbidity` | Turbidity, formazin, method unspecified | Turbidez, formazina sem método identificado | FTU | value | |
357
+ | Physical / field | `turbidity_ntu` | Turbidity, nephelometric white light | Turbidez nefelométrica, luz branca | NTU | value | |
358
+ | Physical / field | `turbidity_fnu` | Turbidity, nephelometric infrared (ISO 7027) | Turbidez nefelométrica, infravermelho | FNU | value | |
359
+ | Physical / field | `turbidity_fau` | Turbidity, attenuation (ISO 7027) | Turbidez por atenuação | FAU | value | |
360
+ | Physical / field | `apparent_color` | Apparent color | Cor aparente | uH | value | |
361
+ | Physical / field | `true_color` | True color | Cor verdadeira | uH | value | |
362
+ | Physical / field | `odor` | Odor | Odor | — | text | |
363
+ | Physical / field | `taste` | Taste | Gosto | — | text | |
364
+ | Physical / field | `total_dissolved_solids` | Total dissolved solids | Sólidos totais dissolvidos | mg/L | value | |
365
+ | Physical / field | `total_suspended_solids` | Total suspended solids | Sólidos suspensos totais | mg/L | value | |
366
+ | Physical / field | `total_solids` | Total solids | Sólidos totais | mg/L | value | |
367
+ | Physical / field | `free_co2` | Free carbon dioxide | Gás carbônico livre | mg/L | value | |
368
+ | Aggregate | `alkalinity_total_as_caco3` | Total alkalinity as CaCO₃ | Alcalinidade total como CaCO₃ | mg/L | value | |
369
+ | Aggregate | `alkalinity_bicarbonate_as_caco3` | Bicarbonate alkalinity as CaCO₃ | Alcalinidade de bicarbonatos como CaCO₃ | mg/L | value | |
370
+ | Aggregate | `alkalinity_carbonate_as_caco3` | Carbonate alkalinity as CaCO₃ | Alcalinidade de carbonatos como CaCO₃ | mg/L | value | |
371
+ | Aggregate | `alkalinity_hydroxide_as_caco3` | Hydroxide alkalinity as CaCO₃ | Alcalinidade de hidróxidos como CaCO₃ | mg/L | value | |
372
+ | Aggregate | `acidity_total_as_caco3` | Total acidity as CaCO₃ | Acidez total como CaCO₃ | mg/L | value | |
373
+ | Aggregate | `hardness_total_as_caco3` | Total hardness as CaCO₃ | Dureza total como CaCO₃ | mg/L | value | |
374
+ | Aggregate | `hardness_calcium_as_caco3` | Calcium hardness as CaCO₃ | Dureza de cálcio como CaCO₃ | mg/L | value | |
375
+ | Major ion | `calcium` | Calcium | Cálcio | mg/L | value | 7440-70-2 |
376
+ | Major ion | `magnesium` | Magnesium | Magnésio | mg/L | value | 7439-95-4 |
377
+ | Major ion | `sodium` | Sodium | Sódio | mg/L | value | 7440-23-5 |
378
+ | Major ion | `potassium` | Potassium | Potássio | mg/L | value | 7440-09-7 |
379
+ | Major ion | `bicarbonate` | Bicarbonate as HCO₃⁻ | Bicarbonato como HCO₃⁻ | mg/L | value | 71-52-3 |
380
+ | Major ion | `carbonate` | Carbonate as CO₃²⁻ | Carbonato como CO₃²⁻ | mg/L | value | 3812-32-6 |
381
+ | Major ion | `chloride` | Chloride | Cloreto | mg/L | value | 16887-00-6 |
382
+ | Major ion | `sulfate` | Sulfate | Sulfato | mg/L | value | 14808-79-8 |
383
+ | Major ion | `fluoride` | Fluoride | Fluoreto | mg/L | value | 16984-48-8 |
384
+ | Major ion | `silica_as_sio2` | Silica as SiO₂ | Sílica como SiO₂ | mg/L | value | 7631-86-9 |
385
+ | Nutrient | `nitrate_as_n` | Nitrate as N | Nitrato como N | mg/L | value | |
386
+ | Nutrient | `nitrate_as_no3` | Nitrate as NO₃⁻ | Nitrato como NO₃⁻ | mg/L | value | 14797-55-8 |
387
+ | Nutrient | `nitrite_as_n` | Nitrite as N | Nitrito como N | mg/L | value | |
388
+ | Nutrient | `nitrite_as_no2` | Nitrite as NO₂⁻ | Nitrito como NO₂⁻ | mg/L | value | 14797-65-0 |
389
+ | Nutrient | `ammonia_as_n` | Ammonia nitrogen as N | Nitrogênio amoniacal como N | mg/L | value | |
390
+ | Nutrient | `ammonia_as_nh3` | Ammonia as NH₃ | Amônia como NH₃ | mg/L | value | 7664-41-7 |
391
+ | Nutrient | `kjeldahl_nitrogen_as_n` | Total Kjeldahl nitrogen as N | Nitrogênio Kjeldahl total como N | mg/L | value | |
392
+ | Nutrient | `phosphorus_total_as_p` | Total phosphorus as P | Fósforo total como P | mg/L | value | |
393
+ | Nutrient | `orthophosphate_as_po4` | Orthophosphate as PO₄³⁻ | Ortofosfato como PO₄³⁻ | mg/L | value | |
394
+ | Organic | `total_organic_carbon` | Total organic carbon | Carbono orgânico total | mg/L | value | |
395
+ | Organic | `cod` | Chemical oxygen demand | Demanda química de oxigênio (DQO) | mg/L | value | |
396
+ | Organic | `bod5` | Biochemical oxygen demand, 5-day | Demanda bioquímica de oxigênio (DBO₅) | mg/L | value | |
397
+ | Organic | `total_petroleum_hydrocarbons` | Total petroleum hydrocarbons (range per method) | Hidrocarbonetos totais de petróleo | mg/L | value | |
398
+ | Organic | `oil_and_grease` | Oil and grease | Óleos e graxas | mg/L | value | |
399
+ | Disinfection | `free_chlorine` | Free chlorine residual | Cloro residual livre | mg/L | value | |
400
+ | Disinfection | `total_chlorine` | Total chlorine residual | Cloro residual total | mg/L | value | |
401
+ | Mining and redox | `ferrous_iron` | Ferrous iron (Fe²⁺) | Ferro ferroso (Fe²⁺) | mg/L | value | |
402
+ | Mining and redox | `chromium_hexavalent` | Hexavalent chromium | Cromo hexavalente | mg/L | value | 18540-29-9 |
403
+ | Mining and redox | `cyanide_total_as_cn` | Total cyanide as CN⁻ | Cianeto total como CN⁻ | mg/L | value | |
404
+ | Mining and redox | `cyanide_wad_as_cn` | Weak acid dissociable cyanide as CN⁻ | Cianeto WAD como CN⁻ | mg/L | value | |
405
+ | Mining and redox | `cyanide_free_as_cn` | Free cyanide as CN⁻ | Cianeto livre como CN⁻ | mg/L | value | |
406
+ | Mining and redox | `thiocyanate` | Thiocyanate | Tiocianato | mg/L | value | 302-04-5 |
407
+ | Mining and redox | `sulfide_total_as_s` | Total sulfide as S | Sulfeto total como S | mg/L | value | |
408
+ | Metal / trace | `iron` | Iron | Ferro | mg/L | value | 7439-89-6 |
409
+ | Metal / trace | `manganese` | Manganese | Manganês | mg/L | value | 7439-96-5 |
410
+ | Metal / trace | `aluminum` | Aluminium | Alumínio | mg/L | value | 7429-90-5 |
411
+ | Metal / trace | `antimony` | Antimony | Antimônio | mg/L | value | 7440-36-0 |
412
+ | Metal / trace | `arsenic` | Arsenic | Arsênio | mg/L | value | 7440-38-2 |
413
+ | Metal / trace | `barium` | Barium | Bário | mg/L | value | 7440-39-3 |
414
+ | Metal / trace | `beryllium` | Beryllium | Berílio | mg/L | value | 7440-41-7 |
415
+ | Metal / trace | `boron` | Boron | Boro | mg/L | value | 7440-42-8 |
416
+ | Metal / trace | `cadmium` | Cadmium | Cádmio | mg/L | value | 7440-43-9 |
417
+ | Metal / trace | `chromium` | Chromium, total | Cromo total | mg/L | value | 7440-47-3 |
418
+ | Metal / trace | `cobalt` | Cobalt | Cobalto | mg/L | value | 7440-48-4 |
419
+ | Metal / trace | `copper` | Copper | Cobre | mg/L | value | 7440-50-8 |
420
+ | Metal / trace | `lead` | Lead | Chumbo | mg/L | value | 7439-92-1 |
421
+ | Metal / trace | `lithium` | Lithium | Lítio | mg/L | value | 7439-93-2 |
422
+ | Metal / trace | `mercury` | Mercury | Mercúrio | mg/L | value | 7439-97-6 |
423
+ | Metal / trace | `molybdenum` | Molybdenum | Molibdênio | mg/L | value | 7439-98-7 |
424
+ | Metal / trace | `nickel` | Nickel | Níquel | mg/L | value | 7440-02-0 |
425
+ | Metal / trace | `selenium` | Selenium | Selênio | mg/L | value | 7782-49-2 |
426
+ | Metal / trace | `silver` | Silver | Prata | mg/L | value | 7440-22-4 |
427
+ | Metal / trace | `strontium` | Strontium | Estrôncio | mg/L | value | 7440-24-6 |
428
+ | Metal / trace | `thallium` | Thallium | Tálio | mg/L | value | 7440-28-0 |
429
+ | Metal / trace | `uranium` | Uranium | Urânio | mg/L | value | 7440-61-1 |
430
+ | Metal / trace | `vanadium` | Vanadium | Vanádio | mg/L | value | 7440-62-2 |
431
+ | Metal / trace | `zinc` | Zinc | Zinco | mg/L | value | 7440-66-6 |
432
+ | Microbiology | `total_coliforms` | Total coliforms in 100 mL | Coliformes totais em 100 mL | — | presence | |
433
+ | Microbiology | `total_coliforms_mpn` | Total coliforms, count | Coliformes totais, contagem | MPN/100 mL | value | |
434
+ | Microbiology | `total_coliforms_cfu` | Total coliforms, count | Coliformes totais, contagem | CFU/100 mL | value | |
435
+ | Microbiology | `e_coli` | _E. coli_ in 100 mL | _E. coli_ em 100 mL | — | presence | |
436
+ | Microbiology | `e_coli_mpn` | _E. coli_, count | _E. coli_, contagem | MPN/100 mL | value | |
437
+ | Microbiology | `e_coli_cfu` | _E. coli_, count | _E. coli_, contagem | CFU/100 mL | value | |
438
+ | Microbiology | `thermotolerant_coliforms` | Thermotolerant coliforms in 100 mL | Coliformes termotolerantes em 100 mL | — | presence | |
439
+ | Microbiology | `thermotolerant_coliforms_mpn` | Thermotolerant coliforms, count | Coliformes termotolerantes, contagem | MPN/100 mL | value | |
440
+ | Microbiology | `thermotolerant_coliforms_cfu` | Thermotolerant coliforms, count | Coliformes termotolerantes, contagem | CFU/100 mL | value | |
441
+ | Microbiology | `heterotrophic_plate_count` | Heterotrophic plate count | Bactérias heterotróficas | CFU/mL | value | |
442
+ | Radioactivity | `gross_alpha` | Gross alpha activity | Atividade alfa total | Bq/L | value | |
443
+ | Radioactivity | `gross_beta` | Gross beta activity | Atividade beta total | Bq/L | value | |
444
+ | Radioactivity | `radium_226` | Radium-226 | Rádio-226 | Bq/L | value | 13982-63-3 |
445
+ | Radioactivity | `radium_228` | Radium-228 | Rádio-228 | Bq/L | value | 15262-20-1 |
446
+ | Radioactivity | `radon_222` | Radon-222 | Radônio-222 | Bq/L | value | 14859-67-7 |
447
+
448
+ The CAS numbers listed are the published equivalences: a result coded by that CAS number is treated as the corresponding `welldot` code. Codes whose basis differs from the substance (such as `nitrate_as_n`) or that measure an analytical fraction (such as `cyanide_wad_as_cn`) have no CAS equivalent. Radionuclides are in Bq/L even though they have CAS numbers, an exception to the mg/L rule; uranium by mass stays under `uranium` (mg/L).
449
+
450
+ ### Limit sets are not part of the file
451
+
452
+ Limits, guideline values and exceedances are never written to a `.well` file, and no field names a national standard. `@welldot/core` bundles selectable limit sets as separate data (WHO Guidelines for Drinking-water Quality, EU Directive 2020/2184, Brazil GM/MS 888/2021), and consumers compare results against the set the user picks, or against their own limits. In turbidity comparisons, `turbidity`, `turbidity_ntu` and `turbidity_fnu` compare with nephelometric limits; `turbidity_fau` is never compared. Local obligations (which parameters, how often) are expressed by profiles; see § Regulatory profiles.
453
+
454
+ ---
455
+
456
+ ## Regulatory profiles
457
+
458
+ Jurisdiction-specific water quality requirements live in profiles, not in the core format. A drinking-water profile for one jurisdiction might require, for wells with `well_purpose` `production`:
459
+
460
+ - at least one `water_samples` entry per calendar year with results for a fixed parameter list (e.g. `e_coli`, `total_coliforms`, `turbidity_ntu`, `free_chlorine`, `nitrate_as_n`, `fluoride`);
461
+ - monthly `e_coli` and `total_coliforms` results (presence form) and half-yearly results for the full list;
462
+ - `laboratory.accreditation` on every sample and an attachment with `document_type: "lab_report"`.
463
+
464
+ ```json
465
+ {
466
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
467
+ "$id": "https://example.org/profiles/drinking-water/v1/schema.json",
468
+ "allOf": [{ "$ref": "https://welldot.org/schema/v2/well.schema.json" }],
469
+ "required": ["water_samples"],
470
+ "properties": {
471
+ "water_samples": {
472
+ "items": {
473
+ "required": ["laboratory", "attachments"],
474
+ "properties": {
475
+ "laboratory": { "required": ["name", "accreditation"] }
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+ ```
482
+
483
+ Frequency rules that JSON Schema cannot express (one sample per month, one per year) are checked by profile-aware tools and surfaced as warnings, like any profile failure. The limits that results are compared against still come from a limit set, never from the file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welldot/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0-next.20261014.26249fd",
4
4
  "type": "module",
5
5
  "description": "TypeScript types, Zod validators, and serialization utilities for the .well open format — a JSON standard for water well data.",
6
6
  "keywords": [