# The .well format > An open JSON format for the complete record of a water well, from construction and lithology to tests, operation, permits and water quality. A `.well` file is the record of one water well, written as a single JSON document. It holds what a drilling report, a well log and the well's later history hold today, in a form that software can read, check and draw: the borehole and its casing, the lithological column, pumping tests, water levels, permits, meters and water analyses. The format is open (Apache 2.0) and defined by a written [specification](/format/spec). Any tool can read and write it. [welldot.org](https://welldot.org) is one editor among them, and the [`@welldot/*` packages](/core) are one implementation. ::well-preview{example="estrela-doeste-sp-bauru-serra-geral"} :: ## Where to start ::card-group :::card{title="What is .well?" icon="i-lucide-file-braces" to="/format/getting-started/what-is-well"} What a file holds, why it exists and how it relates to the spec and the packages. ::: :::card{title="For geologists" icon="i-lucide-mountain" to="/format/getting-started/for-geologists"} How a drilling report maps onto the file: lithology, construction, tests, operation, permits and water quality. ::: :::card{title="For developers" icon="i-lucide-code" to="/format/getting-started/for-developers"} Read, validate and write `.well` files from code, and what the schema guarantees. ::: :::card{title="Your first .well file" icon="i-lucide-file-plus" to="/format/getting-started/first-well-file"} Write a small, valid file by hand and see it drawn. ::: :: ## How this section is organized - **Getting started** introduces the format for each kind of reader. - **Concepts** explains the ideas the whole format rests on: depths, units, vocabularies, profiles, versions and extensions. - **Guides** walk through tasks: writing a file, converting a report, validating, extending the format and building software on it. - **Reference** lists every object and field (generated from the schema), links the JSON Schema and shows the example wells. - **Specification** is the normative text, rendered as it is written in the repository. When a guide and the spec seem to differ, the spec is right. - **Changelog** lists the revisions of the format. ::note These pages explain the format and link to the rules. The rules themselves live only in the [specification](/format/spec), so there is a single source to cite. :: # What is .well? > What a .well file holds, the problem it solves, and how the specification, the JSON Schema and the welldot packages fit together. `.well` is an open file format for water well data. One file describes one well: how it was built, what rock it crosses, what was measured in it and what happened throughout its life. The file is plain JSON, so it opens in any text editor and any programming language can read it. ## The problem Most well records live in scanned drilling reports, PDF profiles, spreadsheets and the closed formats of individual programs. A person can read them; software cannot. Comparing a hundred wells means retyping a hundred reports, and a profile drawn in one program cannot be edited in another. Registries, consultants and researchers each rebuild the same data by hand. A `.well` file records the same facts once, as structured data that can be validated, searched, compared, drawn and archived. ## Key principles The [design principles](/format/spec/v2/overview#design-principles) shape every field. The ones a new reader meets first: - **Self-describing.** Field names are written out in full (`well_screen`, `static_level`), so a file can be read without the spec at hand. - **One unit per quantity.** Depths are meters, diameters millimeters, flow rates m³/h. The file never declares units. See [Units and precision](/format/concepts/units-and-precision). - **Ground level is zero.** Depths are measured down from the ground surface. See [Depths and intervals](/format/concepts/depths-and-intervals). - **Graceful incompleteness.** A record with only a static level is still a valid record. Unknown values are left out, not invented. - **Open to extension.** Fields and blocks prefixed with `x-` carry data the spec does not define, and every reader keeps them. See [Extensibility](/format/concepts/extensibility). ## What a file holds | Part | What it records | Main blocks | | ------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Identity | Name, construction method, purpose, driller, location, registry identifiers | `name`, `well_type`, `well_purpose`, `location`, `well_id` | | Construction | Borehole, casing, screens, reductions, gravel pack and seals, centralizers, wellhead pad | `bore_hole`, `well_case`, `well_screen`, `reduction`, `surface_case`, `hole_fill`, `centralizers`, `cement_pad` | | Geology | Lithological column, fractures, cavities | `lithology`, `fractures`, `caves` | | Hydraulics | Water levels, pumping tests, interpreted aquifer parameters | `hydrodynamic_events`, `aquifer_analysis` | | Operation | History log, pumps, permits, meters, production, operating regime, documents | `history_logs`, `pump_installations`, `permits`, `meters`, `production`, `operating_regime`, `attachments` | | Water quality | Samples and their field and laboratory results | `water_samples` | The full list, in order, is the [top-level structure](/format/spec/v2/format-reference#top-level-structure) in the specification. Every object has a [reference page](/format/reference/objects) generated from the schema. ## What it is for The specification names three uses ([Purpose](/format/spec/v2/overview#purpose)): - **Visualization.** Enough construction and geological detail to draw an accurate well profile at true depth scale. - **Registration.** A complete static record that can be submitted to a regulator, stored in a registry and archived. - **Research.** Structured geology, tests and analyses that support comparison across many wells. ## The pieces ```mermaid flowchart LR S["Specification
(spec/, plain Markdown)"] --> J["JSON Schema
(generated)"] S --> C["@welldot/core
(types, validation)"] C --> J C --> U["@welldot/utils
(analysis)"] C --> R["@welldot/render
(profile drawing)"] C --> A["Your tool"] J --> A ``` - The **specification** is the standard. It is versioned Markdown in the [`spec/` folder](https://github.com/welldothq/welldot/tree/main/spec) of the repository, and this site renders it under [Specification](/format/spec). - The **JSON Schema** is generated from the reference implementation and published at [`https://welldot.org/schema/v2/well.schema.json`](https://welldot.org/schema/v2/well.schema.json). See [JSON Schema](/format/reference/json-schema). - **[`@welldot/core`](/core)** is the TypeScript reference implementation: types, validators and serialization. [`@welldot/utils`](/utils) analyzes a well, and [`@welldot/render`](/render) draws it. - **[welldot.org](https://welldot.org)** is a free web editor built on these packages. It is not required: any program that follows the spec can read and write `.well` files. ## Next - [For geologists](/format/getting-started/for-geologists): how the parts of a drilling report map onto the file. - [For developers](/format/getting-started/for-developers): reading and validating files from code. - [Your first .well file](/format/getting-started/first-well-file): a small file written by hand. # For geologists > How the parts of a drilling report and a well's later history map onto a .well file, explained in field terms. This page is for geologists, hydrogeologists and drillers who know what a well report contains and want to know where each item goes in a `.well` file. No programming is needed. A `.well` file is text, and the [welldot.org](https://welldot.org) editor writes it for you if you prefer forms to JSON. The well below is one of the [example files](/format/reference/examples): a 26 m well in the Dunas-Barreiras aquifer with two screens, a gravel pack and a cemented annulus. The profile is drawn from the file itself. ::well-preview{example="extremoz-rn-barreiras-raso"} :: ## Read a file as a report A file is a list of named blocks. Most blocks are lists of **intervals**: a `from` depth and a `to` depth in meters, measured down from the ground surface. A casing from 0 to 8 m, a sandstone layer from 1.5 to 6 m and a screen from 8 to 11 m are all written the same way. The [depth convention](/format/concepts/depths-and-intervals) is the one idea to learn before anything else. Measurements use one unit each, always the same: depths in meters, diameters in millimeters, flow rates in m³/h. A casing sold as 6 inches is written with its real outer diameter in millimeters. See [Units and precision](/format/concepts/units-and-precision). ## Identity and location | In the report | In the file | | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | Well name or code | `name` | | Type of well (tubular, hand-dug, gallery) | `well_type`, the construction method only ([values](/format/spec/v2/format-reference#well_type--recommended-values)) | | Use (supply, monitoring, piezometer) | `well_purpose`, a list ([values](/format/spec/v2/format-reference#well_purpose--recommended-values-since-v21)) | | Driller, completion date | `well_driller`, `construction_date` (`YYYY-MM-DD`) | | Registry numbers (SIAGAS, CNARH, a state agency) | `well_id`, one entry per authority ([rules](/format/spec/v2/format-reference#well_id)) | | Coordinates and ground elevation | `location`, in decimal degrees on WGS84 ([rules](/format/spec/v2/format-reference#location)) | ::tip An artesian well is not a type of construction. Write `well_type: "tubular"` and record the flowing condition as a water level above ground, which is a negative depth ([level sign convention](/format/spec/v2/format-reference#level-sign-convention)). :: ## Lithology, fractures and cavities Each layer of the lithological description is one entry of `lithology`: ```json { "from": 1.5, "to": 6, "description": "Arenito médio a grosso, pouco argiloso, creme-amarelado", "color": "#e2cf9a", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Grupo Barreiras", "aquifer_unit": "Sistema Aquífero Dunas-Barreiras" } ``` - `description` is the driller's or geologist's description, kept in the language of the report. - `texture` is the fill pattern drawn on the profile. By default it is a code from the FGDC geologic map symbolization standard; the 600 series covers sedimentary rocks and the 700 series metamorphic and igneous rocks. The [FGDC texture reference](/format/spec/reference/fgdc-textures) lists all 282 codes and marks the ones that have a drawn pattern. - `color` is the color used for the layer, as a hex code. - `geologic_unit` and `aquifer_unit` name the stratigraphic unit and the aquifer it belongs to. Discrete fractures go in `fractures` with their depth, dip, azimuth and whether they yield water. Cavities, common in karst, go in `caves` as intervals. See the [Lithology](/format/spec/v2/object-schemas#lithology--lithology), [Fracture](/format/spec/v2/object-schemas#fracture--fractures) and [Cave](/format/spec/v2/object-schemas#cave--caves) schemas. ## Construction Construction is described as the well was built (as-built), one block per component: | Component | Block | Notes | | --------------------------- | -------------- | ---------------------------------------------------------------------- | | Drilled hole | `bore_hole` | One entry per diameter; a telescoping hole has several. | | Blank casing | `well_case` | Material in `type`, outer diameter in mm. | | Screen (filter) | `well_screen` | Slot opening in mm (`screen_slot`), screen design in `slot_type`. | | Diameter reduction | `reduction` | Diameter at the top and at the bottom. | | Surface (protective) casing | `surface_case` | The outer casing near the surface. | | Gravel pack and seals | `hole_fill` | `type` is `gravel_pack` or `seal`; the material goes in `description`. | | Centralizers | `centralizers` | An interval and a spacing, as reports give them. | | Wellhead pad | `cement_pad` | Dimensions in meters. | A casing that sticks up above the ground starts at a negative depth, for example `"from": -0.5`. The schemas are in [Object schemas](/format/spec/v2/object-schemas#object-schemas), and the recommended names for materials are in [Construction materials](/format/spec/v2/format-reference#construction-materials--recommended-values-since-v23). ## Water levels and pumping tests Every measurement of the water level and every pumping test is an **event** in `hydrodynamic_events`, kept in time order. Each event has a type: | Field work | Event `type` | | -------------------------------------------- | ------------------ | | Static level read during a visit | `spot_measurement` | | Test at one constant flow rate | `constant_rate` | | Step-drawdown test | `step_drawdown` | | Air-lift development with an estimated yield | `airlift` | | Recovery readings without the drawdown phase | `recovery_only` | | Data logger series | `logger_series` | A test holds its static level, its pumping steps (rate in m³/h, duration in minutes, readings of elapsed time and depth) and an optional recovery phase. Drawdown and specific capacity are **not** stored: software computes them from the readings, so they can never disagree with the data. See [`hydrodynamic_events`](/format/spec/v2/object-schemas#hydrodynamic_events). The interpretation of a test is a separate entry in `aquifer_analysis`: transmissivity, storativity, Jacob coefficients, recommended flow rate, the method and the analyst, and the ids of the events it used. Several interpretations of the same test can coexist. See [`aquifer_analysis`](/format/spec/v2/object-schemas#aquifer_analysis). ## History, permits and operation - `history_logs` is the well's logbook: maintenance, inspections, incidents and changes of status (active, inactive, decommissioned, abandoned). - `permits` records abstraction permits (outorgas), their validity, granted flow and conditions (condicionantes), and when each condition was met. - `pump_installations` and `meters` record each pump and totalizer for the period it was installed. - `production` holds meter readings, from which volumes are calculated, and `operating_regime` the declared pumping hours and flow. These blocks are optional. A drilling report alone fills the construction and geology and leaves them out. ## Water quality Each sample is one entry of `water_samples`, with its date, sampling point, laboratory and a list of results. A result names its parameter by a code (`nitrate_as_n`, `ph`, `total_coliforms`) and gives a value, a presence or absence, or a text. Results below the detection limit keep their qualifier (`<`). The unit comes from the parameter code, so it is never written next to the value. Drinking water limits are **not** written in the file. Software compares the results with the limit set the reader chooses (WHO, the EU directive, the Brazilian ordinance). See [Water quality](/format/spec/v2/water-quality). ## What to do with missing data Leave it out. The format is built for [graceful incompleteness](/format/spec/v2/overview#design-principles): a field without a value is omitted, never filled with a guess, zero or `null` ([absent values](/format/spec/v2/format-reference#absent-values-since-v24)). A file with only construction and lithology is complete enough to be drawn and registered, and more can be added later. ::note The schema requires the construction and geology blocks even when they are empty: write `"fractures": []` when the report mentions none. The [Well object reference](/format/reference/objects/well) lists which blocks are required, and [Your first .well file](/format/getting-started/first-well-file) shows the minimum. :: ## Next - [Your first .well file](/format/getting-started/first-well-file) - [Converting a report](/format/guides/converting-a-report), by hand or with an AI assistant - [Depths and intervals](/format/concepts/depths-and-intervals) # For developers > Read, validate and write .well files from code, what the schema guarantees, and which parts of the spec matter most when you build on the format. A `.well` file is a JSON object with a fixed set of top-level blocks. You can read it with any JSON parser and validate it with the published [JSON Schema](/format/reference/json-schema) in any language. In TypeScript or JavaScript, [`@welldot/core`](/core) gives you types, validation that matches the spec, and the v1 upgrade. ## Read and validate in TypeScript ::code-group ```bash [pnpm] pnpm add @welldot/core ``` ```bash [npm] npm install @welldot/core ``` ```bash [yarn] yarn add @welldot/core ``` :: ```ts [read.ts] import { readFile } from 'node:fs/promises'; import { parseWell, type Well } from '@welldot/core'; const text = await readFile('PT-01.well', 'utf8'); // Throws a ZodError (with a path for each problem) when the file is invalid. // A version 1 file is upgraded to v2 on the way in. const well: Well = parseWell(text); console.log(well.name, well.bore_hole.length, well.lithology.length); ``` [Parsing and validating](/core/guides/parsing-and-validating) covers the error shape, `safeParse` and legacy files. [Serializing and sharing](/core/guides/serializing-and-sharing) covers writing a file back. ## Validate in other languages The JSON Schema (draft 2020-12) is published at a stable URL: ```text https://welldot.org/schema/v2/well.schema.json ``` Any JSON Schema validator can use it, for example `ajv` in JavaScript, `jsonschema` in Python or `santhosh-tekuri/jsonschema` in Go. The schema is generated from the Zod validators of `@welldot/core`, so the two accept the same documents. A few rules can only be checked in code; see [Validating](/format/guides/validating). ## What the shape guarantees - **Required fields.** `version`, `well_type` ([core identity](/format/spec/v2/format-reference#core-identity)) and the construction and geology arrays (`bore_hole`, `well_case`, `reduction`, `well_screen`, `surface_case`, `hole_fill`, `lithology`, `fractures`, `caves`, possibly empty) are required. Everything else is optional. See the [Well object](/format/reference/objects/well). Note that `parseWell` and the generated JSON Schema currently accept a file without `well_type`; write it anyway. - **Fixed units.** Every number has one canonical unit set by the spec, never by the file ([Units](/format/spec/v2/format-reference#units)). Convert on display, not in storage. - **Absent means unknown.** Optional fields without a value are omitted, never `null` ([Absent values](/format/spec/v2/format-reference#absent-values-since-v24)). - **Unknown members survive.** Readers must preserve fields and blocks they do not recognize, prefixed with `x-` or not ([Extensibility](/format/spec/v2/format-reference#extensibility)). Do not strip them on save. - **Open vocabularies.** Most string fields recommend values but accept any string. Show unknown values as they are ([Recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies)). - **String ids, local references.** Records point to each other by string `id` within the same file ([Cross-reference rules](/format/spec/v2/format-reference#cross-reference-and-uniqueness-rules)). A dangling reference is a warning, not an error. ::note `parseWell` in `@welldot/core` currently accepts a file without `version`. The spec says such a file SHOULD be rejected ([parser version handling](/format/spec/v2/format-reference#parser-version-handling)), so always write it. :: ## Parts of the spec to read first | If you are building | Read | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A reader or viewer | [Top-level structure](/format/spec/v2/format-reference#top-level-structure), [Versioning](/format/spec/v2/format-reference#versioning), [Units](/format/spec/v2/format-reference#units) | | An editor | the above, plus [Block kinds](/format/spec/v2/format-reference#block-kinds-since-v23), [Datetime conventions](/format/spec/v2/format-reference#datetime-conventions), [Duplicate ids](/format/spec/v2/format-reference#duplicate-ids-within-a-scope) | | A database or registry | [Cross-reference rules](/format/spec/v2/format-reference#cross-reference-and-uniqueness-rules), [`well_id`](/format/spec/v2/format-reference#well_id), [Interoperability](/format/spec/v2/interoperability) | | A converter from another format | [Units](/format/spec/v2/format-reference#units), [Precision](/format/spec/v2/format-reference#precision-and-uncertainty), [Changes from v1](/format/spec/v2/overview#changes-from-v1) | [Building on .well](/format/guides/building-on-well) collects the decisions that matter for editors, databases and pipelines. ## Draw and analyze - [`@welldot/render`](/render) draws a profile into an SVG element with D3. - [`@welldot/utils`](/utils) computes volumes, drawdown, permit status, production totals and water quality indicators. ```ts import { parseWell } from '@welldot/core'; import { calculateHoleFillVolume, getLatestStaticLevel } from '@welldot/utils'; const well = parseWell(text); calculateHoleFillVolume('gravel_pack', well); // m³ of gravel pack getLatestStaticLevel(well); // m below ground, negative if flowing ``` ## Test data The 19 [example wells](/format/reference/examples) are complete, valid files covering crystalline, karst, sedimentary, flowing, hand-dug and monitoring wells, plus incomplete and inconsistent records. They ship with `@welldot/core` as `@welldot/core/examples/` and are served at `https://welldot.org/examples/`. ## Working with an AI assistant Point your assistant at this site's [MCP server](/ai/mcp) to let it search these docs and validate the `.well` files it writes, or give it [`/llms.txt`](/ai/llms). # Your first .well file > Write a small, valid .well file by hand, step by step, and see the profile it draws. This page builds a small well from an empty file: a 60 m tubular well in sandstone with one casing, one screen, a gravel pack and three lithology layers. You need a text editor. Save the result as `demo.well`. ::steps ### Start with the version and the identity Every file starts with `version`. Files written today use `"2.4"`, as a string ([Versioning](/format/spec/v2/format-reference#versioning)). `well_type` is the construction method, here a drilled `tubular` well. The spec requires it ([core identity](/format/spec/v2/format-reference#core-identity)), although `parseWell` currently accepts a file without it. ```json { "version": "2.4", "name": "Demo well, sandstone aquifer", "well_type": "tubular", "location": { "lat": -22.2139, "lng": -49.9458, "elevation": 512 } } ``` `location` is optional. Coordinates are decimal degrees on WGS84 and the elevation is in meters ([location](/format/spec/v2/format-reference#location)). ### Describe the hole The borehole is a list of intervals, one per diameter. Depths are meters below ground and diameters are millimeters ([Units](/format/spec/v2/format-reference#units)). This hole is drilled at 254 mm (10 in) to 60 m. ```json "bore_hole": [{ "from": 0, "to": 60, "diameter": 254, "drilling_method": "rotary" }] ``` ### Add the casing, the screen and the annulus A blank casing from 0.5 m above ground to 36 m, then a screen to 54 m. The casing starts at `-0.5` because it sticks up above the ground. `hole_fill` describes the annulus: a cement seal at the top and a gravel pack around the screen. ```json "well_case": [{ "from": -0.5, "to": 36, "type": "pvc", "diameter": 168.3 }], "well_screen": [ { "from": 36, "to": 54, "type": "pvc", "diameter": 168.3, "screen_slot": 0.75 } ], "hole_fill": [ { "from": 0, "to": 30, "type": "seal", "diameter": 254, "description": "Cement grout" }, { "from": 30, "to": 60, "type": "gravel_pack", "diameter": 254, "description": "Gravel, 2–4 mm" } ] ``` Diameters are the real outer diameter (as-built), not the nominal size: a 6 in PVC casing is 168.3 mm ([as-built convention](/format/spec/v2/format-reference#diameter-and-the-as-built-convention)). ### Add the lithology Each layer has a depth interval, a description, a color, a texture code for the pattern and the geologic and aquifer units. Code 607 is "massive sand or sandstone" in the [FGDC texture reference](/format/spec/reference/fgdc-textures). ```json "lithology": [ { "from": 0, "to": 8, "description": "Red clayey soil", "color": "#9c5a3c", "texture": { "code": 620, "vocabulary": "fgdc" }, "geologic_unit": "Cover", "aquifer_unit": "Unsaturated zone" }, { "from": 8, "to": 30, "description": "Silty sandstone", "color": "#c9a27a", "texture": { "code": 612, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" }, { "from": 30, "to": 60, "description": "Medium-grained sandstone", "color": "#e0c48f", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" } ] ``` ### Close the required lists The construction and geology lists are required even when empty. This well has no reduction, surface casing, fractures or caves, so they are empty lists. ```json "reduction": [], "surface_case": [], "fractures": [], "caves": [] ``` :: ## The complete file Put the pieces together inside one object. The preview validates the file with `@welldot/core` and draws it with `@welldot/render`. ::well-preview ```well { "version": "2.4", "name": "Demo well, sandstone aquifer", "well_type": "tubular", "location": { "lat": -22.2139, "lng": -49.9458, "elevation": 512 }, "bore_hole": [ { "from": 0, "to": 60, "diameter": 254, "drilling_method": "rotary" } ], "well_case": [{ "from": -0.5, "to": 36, "type": "pvc", "diameter": 168.3 }], "reduction": [], "well_screen": [ { "from": 36, "to": 54, "type": "pvc", "diameter": 168.3, "screen_slot": 0.75 } ], "surface_case": [], "hole_fill": [ { "from": 0, "to": 30, "type": "seal", "diameter": 254, "description": "Cement grout" }, { "from": 30, "to": 60, "type": "gravel_pack", "diameter": 254, "description": "Gravel, 2–4 mm" } ], "lithology": [ { "from": 0, "to": 8, "description": "Red clayey soil", "color": "#9c5a3c", "texture": { "code": 620, "vocabulary": "fgdc" }, "geologic_unit": "Cover", "aquifer_unit": "Unsaturated zone" }, { "from": 8, "to": 30, "description": "Silty sandstone", "color": "#c9a27a", "texture": { "code": 612, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" }, { "from": 30, "to": 60, "description": "Medium-grained sandstone", "color": "#e0c48f", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" } ], "fractures": [], "caves": [] } ``` :: ## Check it - Open [welldot.org](https://welldot.org) and drop `demo.well` on the editor. It shows the same profile and lets you keep editing with forms. - Ask an AI assistant connected to the [docs MCP server](/ai/mcp) to validate it. - From code, call `parseWell` from [`@welldot/core`](/core/getting-started/first-use). If something is wrong, the validator names the field: `well_screen[0].screen_slot: Required` means the first screen has no slot size. [Validating](/format/guides/validating) explains the messages. ## Where to go next - Add a water level: a `spot_measurement` event in `hydrodynamic_events` ([schema](/format/spec/v2/object-schemas#spot_measurement)). - Read [Writing a .well by hand](/format/guides/writing-by-hand) for the habits that keep a file valid as it grows. - Browse the [example wells](/format/reference/examples) for complete records. # Depths and intervals > How .well measures depth from the ground surface, writes components as from/to intervals, and handles stick-up, water levels above ground and the well's usable depth. Every position along a well is a depth in meters, measured from the ground surface. Ground level is zero and depth increases downward. This single reference is used by the construction, the geology, the water levels and the sampling points, so they can be drawn and compared on one scale. The rule is a [design principle](/format/spec/v2/overview#design-principles) ("ground level as zero") and is stated for levels in the [level sign convention](/format/spec/v2/format-reference#level-sign-convention). ## Intervals Most components occupy a stretch of the hole and are written as an interval: `from` is the top and `to` is the bottom. ```json { "from": 36, "to": 54, "type": "pvc", "diameter": 168.3, "screen_slot": 0.75 } ``` A screen from 36 to 54 m, a sandstone layer from 8 to 30 m and a gravel pack from 30 to 60 m are written the same way. `from` and `to` are reserved for depth intervals in meters; time periods use `period_start` and `period_end` ([naming rules](/format/spec/v2/format-reference#naming-rules-since-v23)). A few things are points rather than intervals: a fracture has a single `depth`, and a water level is a single depth reading. ```mermaid flowchart TB A["−0.5 m · top of casing (stick-up)"] --> B["0 m · ground surface"] B --> C["3.9 m · static water level"] C --> D["36 m · top of screen"] D --> E["54 m · base of screen"] E --> F["60 m · bottom of hole"] ``` ## Above the ground: negative depths Anything above the ground surface has a negative depth. - **Stick-up.** A casing that rises 0.5 m above the ground starts at `"from": -0.5`. Renderers draw it above the wellhead pad. - **Flowing wells.** A water level 3.98 m above the ground is written `"static_level": -3.98`. This is how the format records an artesian (flowing) condition, instead of a special well type ([`well_type`](/format/spec/v2/format-reference#well_type--recommended-values)). The exception is the pump intake depth (`pump_installations[].intake_depth`), an equipment position that is always positive. ::do-dont #do Record a flowing well as a tubular well with a negative static level. ```json { "well_type": "tubular" } { "type": "spot_measurement", "static_level": -3.98 } ``` #dont Use the deprecated `artesian` type, or a positive level with a note. ```json { "well_type": "artesian" } { "type": "spot_measurement", "static_level": 3.98, "notes": "above ground" } ``` :: ## Total depth and usable depth The well's drilled depth is the deepest `to` of its construction. It does not need its own field: software reads it from the data (`calculatedWellDepth` in [`@welldot/core`](/core/api/functions/calculatedWellDepth)). `well_depth` is something else: the depth that is usable now, as measured or reported. It can be shallower than the drilled depth when the well has silted up or was partly backfilled ([core identity fields](/format/spec/v2/format-reference#core-identity)). Leave it out unless someone measured it. ## Overlaps and gaps The format does not require intervals of different blocks to line up. A screen sits inside a borehole interval, a gravel pack around several screens, a lithology layer across a casing joint. Within one block, real reports sometimes contain gaps or overlaps; the [inconsistent data example](/format/reference/examples) keeps them on purpose. Validation checks types and required fields, not geometry, so such a file is still valid. Editors and analysis tools may warn about it. ## Elevation is separate The ground elevation of the wellhead is stored once in `location.elevation`, in meters above the WGS84 ellipsoid or the datum named in `location.properties.elevation_datum` ([location](/format/spec/v2/format-reference#location)). To get the elevation of a point in the well, subtract its depth from that elevation. ## Related - [Units and precision](/format/concepts/units-and-precision) - The construction schemas: [`BoreHole`](/format/spec/v2/object-schemas#borehole--bore_hole), [`WellCase`](/format/spec/v2/object-schemas#wellcase--well_case), [`WellScreen`](/format/spec/v2/object-schemas#wellscreen--well_screen), [`HoleFill`](/format/spec/v2/object-schemas#holefill--hole_fill) # Units and precision > Why every number in a .well file has one fixed SI unit, which units apply to which fields, how to convert non-SI reports, and how to record measurement precision. A `.well` file never says which units it uses. Each kind of quantity has exactly one unit, fixed by the specification: depths in meters, diameters in millimeters, flow rates in cubic meters per hour, and so on. Two files that describe the same well therefore contain the same numbers. ## The units The main ones, from the spec's [canonical units](/format/spec/v2/format-reference#canonical-units) table: | Quantity | Unit | | ---------------------------------------- | -------------- | | Depth, length, elevation | m | | Diameter (hole, casing, screen, annulus) | mm | | Screen slot opening | mm | | Flow rate | m³/h | | Elapsed time, test duration | min | | Volume | m³ | | Transmissivity | m²/s | | Hydraulic conductivity | m/s | | Power | kW | | Substance concentration | mg/L | | Electrical conductivity | µS/cm at 25 °C | | Angles (azimuth, dip) | degrees | The spec binds every numeric field to its unit in the [field-to-unit binding](/format/spec/v2/format-reference#field-to-unit-binding-normative) list. Water quality results take their unit from the parameter code; see [water quality units](/format/spec/v2/water-quality#units). ::note The wellhead pad (`cement_pad`) is in meters like every other length, so a 1 × 1 m pad is written `1.0`, not `1000`. :: ## Why one unit Configurable units would let the same well be written in many ways, make comparison harder and allow files whose declared units do not match their numbers. The spec's [rationale](/format/spec/v2/format-reference#rationale) follows GeoJSON (always WGS84) and RFC 3339 (always an offset): one well, one encoding. Display and input units belong to the applications that read and write the files. ## Converting reports Reports often use inches, feet, gallons per minute or slot numbers. Convert them before writing the file ([rationale](/format/spec/v2/format-reference#rationale)): | In the report | In the file | | --------------------- | ---------------------------------------------------------------- | | Hole drilled at 10 in | `"diameter": 254` | | Casing 6 in (nominal) | the real outer diameter, for example `168.3` for 6 in steel pipe | | Screen slot 20 | `"screen_slot": 0.508` (20 thousandths of an inch) | | Depth 150 ft | `45.72` | | Yield 50 US gpm | `11.36` m³/h | | Pump 5 hp | `3.73` kW | Two details matter: - **As-built diameters.** Casing, screen and annulus diameters are the real outer diameter in millimeters, not the nominal trade size. A 6 in casing with an outer diameter of 168.3 mm is written `168.3`, not `152.4` ([as-built convention](/format/spec/v2/format-reference#diameter-and-the-as-built-convention)). - **Slot numbers.** A US slot number is thousandths of an inch: slot 20 is 0.508 mm ([screen slot](/format/spec/v2/format-reference#screen-slot)). `@welldot/core` ships the common conversions (`inchesToMm`, `feetToMeters`, `slotNumberToMm`, `usGallonsPerMinuteToCubicMeterPerHour`, `hpToKilowatts` and more). See [Unit conversion](/core/guides/unit-conversion). ## Dates and times Dates and times have fixed forms too ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)): - **Calendar dates** (`YYYY-MM-DD`) for the construction date and legal documents such as permits. - **Instants** (RFC 3339 with a UTC offset, such as `2024-09-03T08:00:00-03:00`) for every measurement, test, log entry and sample. An instant without an offset is invalid. A laboratory date known only to the day is written at local midnight with a `_resolution: "day"` marker ([instants known only by date](/format/spec/v2/format-reference#instants-known-only-by-date-since-v23)). ## Precision Some measurements can carry their uncertainty in a sibling field named with `_precision`: `static_level_precision`, `depth_precision`, `lat_precision`, a result's `value_precision`. The value is one standard deviation, in the unit of the measurement it qualifies. ```json { "static_level": 3.94, "static_level_precision": 0.01 } ``` Laboratories usually report an expanded uncertainty U with a coverage factor k (often k = 2). Store U / k, not U. The full list of precision fields is in [Precision and uncertainty](/format/spec/v2/format-reference#precision-and-uncertainty). ::tip Leave a precision field out when the report does not state one. A missing precision means "not documented", which is honest; an invented one is not. :: ## Coordinates Coordinates are decimal degrees in a geographic reference system, WGS84 by default. Projected coordinates such as UTM must be converted to latitude and longitude before writing; the original system can be kept in `location.properties.original_crs` ([CRS scope](/format/spec/v2/format-reference#crs-scope)). # Vocabularies > Open and closed vocabularies in .well, how recommended values and x- values work, and where the lithology textures and water quality parameters come from. Many fields in a `.well` file take a word from a list: the construction method, the casing material, the type of a pumping test, the category of a log entry. The format distinguishes two kinds of lists. ## Open vocabularies Most lists are **open**. The spec recommends a set of values so that files from different sources agree, but any string is valid. A value outside the recommended set should start with `x-`, and every tool must keep it and show it as written ([recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies)). ```json { "from": 0, "to": 12, "diameter": 200, "drilling_method": "x-jetting" } ``` Here `x-jetting` says plainly that jetting is not one of the recommended drilling methods (rotary, percussion, cable tool, auger, air hammer), while keeping the information. Values are stored as their key (`stainless_steel`), never as a translated label (`Aço inox`). Labels are the job of the application; `@welldot/core` publishes English and Portuguese labels for every recommended value ([Vocabularies and textures](/core/guides/vocabularies-and-textures)). Some open vocabularies, with their spec tables: | Field | Recommended values | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | `well_type` | [construction methods](/format/spec/v2/format-reference#well_type--recommended-values) | | `well_purpose` | [uses](/format/spec/v2/format-reference#well_purpose--recommended-values-since-v21) | | `bore_hole[].drilling_method` | [drilling methods](/format/spec/v2/format-reference#bore_holedrilling_method--recommended-values-since-v23) | | `well_case[].type`, `well_screen[].type`, `reduction[].type` | [construction materials](/format/spec/v2/format-reference#construction-materials--recommended-values-since-v23) | | `well_screen[].slot_type` | [screen designs](/format/spec/v2/format-reference#well_screenslot_type--recommended-values-since-v23) | | `hydrodynamic_events[].type` | [event types](/format/spec/v2/object-schemas#event-types) | | `aquifer_analysis[].method` | [analysis methods](/format/spec/v2/format-reference#aquifer_analysismethod--recommended-values) | | `history_logs[].category` | [log categories](/format/spec/v2/format-reference#history_logscategory--recommended-values) | The full list is under [Recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies) in the format reference. ::do-dont #do Use the recommended value when the report states it, and an `x-` value when it states something the list does not have. ```json { "type": "galvanized_steel" } { "type": "x-concrete" } { "drilling_method": "x-jetting" } ``` #dont Force a value into the nearest recommended word, or write a custom value without the prefix. ```json { "type": "carbon_steel" } { "type": "Tubo de concreto" } { "drilling_method": "rotary" } ``` :: An unprefixed custom value, such as `"Tubo de concreto"`, is still valid: readers must accept and keep it. It is not recommended, because the spec says non-canonical values SHOULD use the `x-` prefix ([recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies), [custom vocabulary values](/format/spec/v2/format-reference#custom-vocabulary-values--x--prefix)). The report's exact wording belongs in a free-text field such as `description`, `notes` or `obs`. ## Closed vocabularies A few fields accept only the listed values, and anything else makes the file invalid. These are fields that software must be able to act on without guessing: - `hole_fill[].type`: `gravel_pack` or `seal` ([HoleFill](/format/spec/v2/object-schemas#holefill--hole_fill)). - `history_logs[].status`: `active`, `maintenance`, `inactive`, `decommissioned` or `abandoned` ([allowed values](/format/spec/v2/format-reference#history_logsstatus--allowed-values-since-v23)). - `permits[].status`, result qualifiers, validation statuses and a few others, each marked in the spec. ## Lithology textures A lithology layer's `texture` names a fill pattern with a `code` and a `vocabulary` ([Texture](/format/spec/v2/object-schemas#texture)). The default vocabulary is `fgdc`, the lithology patterns of the US Federal Geographic Data Committee's geologic map symbolization standard (FGDC-STD-013-2006). The 600 series (sedimentary) and 700 series (metamorphic and igneous) are the most useful for well logs. All 282 codes, and which of them have a drawn pattern, are in the [FGDC texture reference](/format/spec/reference/fgdc-textures). ```json { "texture": { "code": 627, "vocabulary": "fgdc" } } ``` Other vocabularies are allowed: `cgi` (the CGI Simple Lithology terms), or the HTTPS address of a vocabulary published by a survey or regulator ([vocabulary values](/format/spec/v2/object-schemas#vocabulary-values)). ## Water quality parameters A result's parameter is also a code in a vocabulary: `{ "code": "nitrate_as_n", "vocabulary": "welldot" }`. The `welldot` vocabulary has 98 parameters, each with a fixed unit; CAS registry numbers (`"vocabulary": "cas"`) are accepted for substances, and `x-` vocabularies for anything else ([parameter vocabulary](/format/spec/v2/water-quality#parameter-vocabulary)). ## How values become official When an `x-` value proves common, a later revision of the spec can promote it. The `x-` form then stays equivalent until the next major version ([vocabulary value promotion](/format/spec/v2/format-reference#vocabulary-value-promotion)). To propose a value, open an issue; see [Extending the format](/format/guides/extending). # Profiles > How a .well file declares that it follows extra rules, such as a regulator's required fields, using JSON Schema profiles. The core format is deliberately neutral: it does not know any country's rules. A **profile** is how a regulator, a utility or a research network adds its own requirements on top. A profile is a JSON Schema published at an HTTPS address. A file lists the profiles it claims to follow in `profiles`: ```json "profiles": [ "https://welldot.org/profiles/brazil-ana/v1/schema.json" ] ``` The rules for profiles are in [`profiles`](/format/spec/v2/format-reference#profiles) in the format reference. ## What a profile can do A profile can make an optional field required, restrict an open vocabulary to certain values, demand a particular `x-` extension, or require certain water quality parameters for a monitoring program ([profile schema requirements](/format/spec/v2/format-reference#profile-schema-requirements)). It usually extends the canonical schema with `allOf` and adds its own constraints. For example, a registry could require `well_id`, `location` and `construction_date`, and limit `well_purpose` to the uses it recognizes. A drinking water program could require nitrate, coliforms and turbidity in every sample ([regulatory profiles](/format/spec/v2/water-quality#regulatory-profiles)). ## Conformance is a claim Listing a profile is a statement by the author, not a guarantee. A profile-aware reader validates the file against each profile it can reach and reports failures as **warnings**. A profile that cannot be downloaded is ignored, and a file is never rejected because of one ([profile validation](/format/spec/v2/format-reference#profile-validation)). A file that is valid `.well` stays valid everywhere. ```mermaid flowchart LR F[".well file"] --> C{"Core schema
(parseWell)"} C -->|invalid| E["errors"] C -->|valid| P{"Each listed profile
(if reachable)"} P -->|fails| W["warnings"] P -->|passes| OK["conforms"] ``` ## Profiles and jurisdiction National drinking water limits are not written into files either. They are data that software picks at display time (`@welldot/core` ships WHO, EU and Brazilian limit sets). Local obligations, such as which parameters must be sampled, enter through profiles. See [jurisdiction neutrality](/format/spec/v2/overview#design-principles) in the design principles and [limit sets](/format/spec/v2/water-quality#limit-sets-are-not-part-of-the-file). ## Status The `profiles` field and its rules are part of v2. No official profile is published yet; the `spec/profiles/` folder is reserved for them. If you need one for your agency or program, open an issue on [GitHub](https://github.com/welldothq/welldot/issues). The [Relationship to GWML2](/format/spec/v2/interoperability#profiles-for-regulatory-adoption) section describes how profiles support regulatory adoption. # Versions and deprecation > How .well versions are written and compared, what readers do with older and newer files, and how fields are deprecated without breaking existing files. Every `.well` file states the version of the format it follows in `version`. Readers use it to decide how to read the file, and to reject a file they cannot read rather than misread it. ## How versions are written | `version` | Meaning | | --------- | ------------------------------------------------------------------------------- | | `1` | The first version: construction and geology only. | | `2` | Version 2, revisions 2.0 to 2.3 (the revision is not stated). | | `"2.4"` | Version 2, revision 2.4. Since 2.4 the revision is written as a string `"M.m"`. | Revisions are compared as two integers, never as text: `"2.10"` comes after `"2.9"`. Patch levels such as `"2.4.1"` are not allowed. See [Versioning](/format/spec/v2/format-reference#versioning). ::warning `@welldot/core` 0.4.0 and earlier reject `"2.4"` files. This is the one intentional break in version 2. Upgrade the software that reads your files before writing 2.4 files. :: ## What a reader does The full rules are in [parser version handling](/format/spec/v2/format-reference#parser-version-handling). In short: - A v2 reader accepts `1`, `2` and any `"2.x"`. A v1 file is upgraded on read: flat coordinates become `location`, `fgdc_texture` becomes `texture`, `screen_slot_mm` becomes `screen_slot` ([changes from v1](/format/spec/v2/overview#changes-from-v1)). - A newer minor revision (say a future `"2.9"`) is still read as v2. Fields the reader does not know are kept, not dropped. - A version that is malformed (`2.4` as a number, `"v2.4"`) or from an unknown major version (`3`, `"3.0"`) is rejected with an error that names it. ## Minor revisions are additive Within version 2, each revision only adds: new optional fields, new blocks, new recommended values and clarifications. It never removes a field or changes its meaning, so every valid 2.3 file is valid 2.4 once its `version` is updated ([deprecation policy](/format/spec/v2/format-reference#deprecation-policy)). The [changelog](/format/changelog) lists what each revision added. ## Deprecation A field or value goes through three states ([field lifecycle](/format/spec/v2/format-reference#field-lifecycle)): ```mermaid flowchart LR A["Active"] -->|"announced in a minor revision"| D["Deprecated
still valid, readers warn"] D -->|"only in a new major version"| R["Removed"] ``` A deprecated field is still accepted and preserved. Readers may warn and suggest the replacement, and editors may offer to migrate it, but only when the user asks ([parser behavior](/format/spec/v2/format-reference#parser-behavior)). Removal happens only in a new major version, with a documented rule for reading old files. The one deprecation in version 2 so far is `well_type: "artesian"` (since 2.1, removal in v3): an artesian well is usually a `tubular` well with a negative static level ([`well_type`](/format/spec/v2/format-reference#well_type--recommended-values)). ## What to write today - New files: `"version": "2.4"`. `serializeWell` from `@welldot/core` writes it for you. - Existing files with `"version": 2` stay valid. Rewriting them as `"2.4"` is safe once all your readers are on `@welldot/core` 0.5.0 or later. - Archives of v1 files can stay as they are; every v2 reader upgrades them on read. ## Related - [Changelog](/format/changelog) - [v1 specification (archived)](/format/spec/v1/well-format) - [Versions and time zones](/core/guides/versions-and-time-zones) in `@welldot/core` # Extensibility > How to carry data the spec does not define, with x- fields, x- blocks and x- vocabulary values, and why every reader must keep them. No format can foresee every field a project needs. `.well` reserves the `x-` prefix for data the spec does not define, and requires every reader to keep that data untouched. A tool can add its own information without breaking any other tool, and the information survives a round trip through software that does not understand it. The rules are in [Extensibility](/format/spec/v2/format-reference#extensibility). ## Three kinds of extension **Fields.** Any object can carry fields whose names start with `x-`. Their content needs no prefix ([custom fields](/format/spec/v2/format-reference#custom-fields--x--prefix)). ```json { "id": "evt-07", "type": "constant_rate", "datetime": "2006-03-14T08:00:00-03:00", "x-siagas": { "registro": "SP-0042819" } } ``` **Top-level blocks.** Whole blocks the spec does not define must start with `x-`, for example a block of gas readings or geophysical logs ([custom top-level blocks](/format/spec/v2/format-reference#custom-top-level-blocks--x--prefix-required)). ```json { "version": "2.4", "x-acme-gas_monitoring": [ { "datetime": "2025-02-01T09:00:00-03:00", "ch4_pct": 0.2 } ] } ``` **Vocabulary values.** In an open vocabulary, a value outside the recommended list should start with `x-`, such as `"type": "x-slug_test"` for a hydrodynamic event ([custom vocabulary values](/format/spec/v2/format-reference#custom-vocabulary-values--x--prefix)). ## Name your extensions after yourself Since 2.4 the spec recommends `x--`, for example `x-welldot-example` or `x-acme-gas_monitoring`. Two tools that both invent `x-custom` would collide; two that use their own name do not. ## Readers keep what they do not understand A reader must preserve `x-` fields and blocks it does not recognize and write them back unchanged. The same applies to any other member it does not know, such as a field added by a newer revision of the spec ([design principles](/format/spec/v2/overview#design-principles), "Preserve unknown members"). Dropping them on save is a bug. ::do-dont #do Read the file, change what you need, and write every other member back as it was. `parseWell` and `serializeWell` in `@welldot/core` do this. ```ts const well = parseWell(text); well.name = 'PT-01 (renamed)'; const out = serializeWell(well); // x- members are still there ``` #dont Rebuild the object from the fields your program knows. Every `x-` member and every newer field is lost. ```ts const { version, name, bore_hole } = JSON.parse(text); const out = JSON.stringify({ version, name, bore_hole }); ``` :: ## Fields without the prefix A field that is neither in the spec nor prefixed is "technically non-conformant": readers must not reject the file, but should warn. Use the prefix for anything you add. ## Extensions are a path into the spec An extension that many people need can become part of the format. A revision adopts the field or value, and for vocabularies the `x-` form stays equivalent until the next major version ([vocabulary value promotion](/format/spec/v2/format-reference#vocabulary-value-promotion)). [Extending the format](/format/guides/extending) explains when to extend locally and when to propose a change. ## Related: offline packages Attachments are referenced by URL. For archives and regulators, a `.well` file can travel with its documents in a ZIP package with the `.wellz` extension. The `.well` file inside is an ordinary file ([offline packaging](/format/spec/v2/format-reference#offline-packaging-since-v24)). # Writing a .well by hand > Habits that keep a hand-written .well file valid and faithful as it grows, from the order of blocks to ids, dates and test readings. Most files are written by software, but a hand-written file is often the fastest way to record a single well, prepare test data or fix a detail. This guide collects the habits that keep such a file valid. Start from [Your first .well file](/format/getting-started/first-well-file) if you have not written one yet. ## Start from something valid Copy an [example well](/format/reference/examples) that resembles yours, or the minimal file from the first-file guide, and change it step by step. Validate after each block you add (see [Validating](/format/guides/validating)): an error is easier to find in ten new lines than in three hundred. A JSON-aware editor helps. In VS Code, associate the extension with JSON (`"files.associations": { "*.well": "json" }` in your settings) and add a `$schema` member at the top of the file. You then get completion and inline errors from the published schema: ```json { "$schema": "https://welldot.org/schema/v2/well.schema.json", "version": "2.4" } ``` `$schema` is an unknown member to the format, so readers keep it; the example wells carry one. ## Keep the report's meaning - **Transcribe, do not interpret.** Free-text fields (`description`, `notes`, `obs`) keep the report's own words and language. Open vocabulary fields, such as a casing `type`, take the recommended value the report states, or an `x-` value for anything else; an unprefixed custom value is valid but not recommended ([custom vocabulary values](/format/spec/v2/format-reference#custom-vocabulary-values--x--prefix)). - **Leave out what you do not know.** A missing value is omitted, never written as `null`, `0` or an empty string ([absent values](/format/spec/v2/format-reference#absent-values-since-v24)). - **Convert units once, carefully.** Meters, millimeters and m³/h only ([Units and precision](/format/concepts/units-and-precision)). Write the real outer diameter of a casing, not its nominal size. ## Required fields `version` and `well_type` are required by the spec ([core identity](/format/spec/v2/format-reference#core-identity)). `bore_hole`, `well_case`, `reduction`, `well_screen`, `surface_case`, `hole_fill`, `lithology`, `fractures` and `caves` must be present, even as `[]` ([Well object](/format/reference/objects/well)). Every other block can be left out entirely. ::note `parseWell` in `@welldot/core` currently accepts a file without `well_type`. Write it anyway: the spec requires it, and other readers may reject the file. :: ::note `parseWell` in `@welldot/core` currently accepts a file without `version`. The spec says such a file SHOULD be rejected ([parser version handling](/format/spec/v2/format-reference#parser-version-handling)), so always write it. :: ## Ids and references Records in the history, tests, permits, meters, production and samples have an `id`. Ids are strings, unique within their block. UUIDs are recommended for software; short readable ids such as `evt-01` or `am-2025` are fine by hand ([`id` field convention](/format/spec/v2/format-reference#id-field-convention)). References use those ids: an analysis lists the tests it used in `source_event_ids`, a log entry points to a sample in `sample_ids`. Every reference resolves within the same file ([cross-array references](/format/spec/v2/format-reference#cross-array-references)). ```json "hydrodynamic_events": [ { "id": "evt-01", "type": "constant_rate", "datetime": "2019-05-20T08:00:00-03:00", "static_level": 3.9 } ], "aquifer_analysis": [ { "id": "an-01", "datetime": "2019-06-02T10:00:00-03:00", "source_event_ids": ["evt-01"], "method": "cooper_jacob" } ] ``` ::warning Two records with the same id in one block make the file malformed. Software must not guess which one you meant ([duplicate ids](/format/spec/v2/format-reference#duplicate-ids-within-a-scope)). :: ## Dates and times - Construction dates and permit dates are calendar dates: `"2019-05-20"`. - Everything that was measured or happened at a moment is an instant with its UTC offset: `"2019-05-20T08:00:00-03:00"`. An instant without an offset is invalid ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)). - If the report gives only the day of a test, write the local time you know (often the start of the working day) and say so in `notes`. ## Pumping tests A test is one event. Its readings sit inside steps: ```json { "id": "evt-02", "type": "constant_rate", "datetime": "2019-05-20T08:00:00-03:00", "static_level": 3.94, "steps": [ { "rate": 18, "duration": 1440, "readings": [ { "elapsed": 1, "depth": 9.8 }, { "elapsed": 60, "depth": 14.2 }, { "elapsed": 1440, "depth": 15.1 } ] } ], "recovery": { "readings": [ { "elapsed": 1, "depth": 10.4 }, { "elapsed": 120, "depth": 4.3 } ] } } ``` `rate` is m³/h, `duration` and `elapsed` are minutes from the start of the step, and `depth` is the water level in meters below ground. Do not store drawdown, specific capacity or Jacob coefficients on the event: drawdown is derived from the readings, and interpretations go in `aquifer_analysis` ([design principles](/format/spec/v2/overview#design-principles), "Derived values are never stored raw"). The step and reading schemas are [`PumpingStep`](/format/spec/v2/object-schemas#pumpingstep) and [`LevelReading`](/format/spec/v2/object-schemas#levelreading). ## Corrections To correct a measurement after a file has been shared, add a new entry whose `corrects` holds the id of the wrong one, instead of editing it. The old entry stays in the file and drops out of every calculation ([corrections](/format/spec/v2/object-schemas#corrections-since-v23)). This applies to `hydrodynamic_events`, `production` and `water_samples`, the three ledger blocks ([block kinds](/format/spec/v2/format-reference#block-kinds-since-v23)). ## When the file grows Past a few hundred lines, a hand-written file is easier to maintain in the [welldot.org](https://welldot.org) editor, which opens and saves the same file. For many wells, write a script with [`@welldot/core`](/core) instead. # Converting a report > Turn a drilling report, well log or laboratory report into a .well file, by hand or with an AI assistant running the welldot-converter skill, and check the result. Most well data starts as a document: a drilling report, a well completion log, a pumping test sheet, a laboratory report. This guide shows how to turn one into a `.well` file and what to check before you trust the result. ## Choose a route | Route | Good for | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | The [welldot.org](https://welldot.org) editor | One well at a time, with forms and a live profile. Saves a `.well` file. | | By hand | Short reports, or fixing a converted file. See [Writing a .well by hand](/format/guides/writing-by-hand). | | An AI assistant with the [`welldot-converter` skill](/ai/conversion-skill) | PDFs, scans and photos, in any language. Fast, but always reviewed. | | A script | Many records from one source, such as a registry export. See [Building on .well](/format/guides/building-on-well). | ## Map the report Whatever the route, the same mapping applies. The [For geologists](/format/getting-started/for-geologists) page goes through it in detail. | Section of the report | Blocks | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | Header (name, owner, location, dates, registry numbers) | `name`, `well_type`, `well_purpose`, `location`, `well_id`, `well_driller`, `construction_date` | | Lithological description | `lithology`, `fractures`, `caves` | | Construction profile | `bore_hole`, `well_case`, `well_screen`, `reduction`, `surface_case`, `hole_fill`, `centralizers`, `cement_pad` | | Pumping test sheet | `hydrodynamic_events` (the readings), `aquifer_analysis` (the interpretation) | | Laboratory report | `water_samples` | | Permits, maintenance records | `permits`, `history_logs`, `pump_installations`, `meters` | ## Rules that matter most These are the mistakes that produce a valid but wrong file: - **Never estimate a number.** If the report does not give a value, leave the field out. A guessed diameter or interpolated reading cannot be told apart from a measured one. - **Keep the report's words.** Descriptions and notes stay in the original language and wording. In open vocabulary fields, such as casing material, use the recommended value when the report states it, otherwise an `x-` value; an unprefixed custom value is valid but not recommended ([custom vocabulary values](/format/spec/v2/format-reference#custom-vocabulary-values--x--prefix)). - **Convert units exactly.** Inches to millimeters (× 25.4), feet to meters (× 0.3048), slot numbers to millimeters, gallons per minute to m³/h ([Units](/format/spec/v2/format-reference#units)). Use real outer diameters for casing ([as-built convention](/format/spec/v2/format-reference#diameter-and-the-as-built-convention)). - **Convert projected coordinates.** UTM coordinates must become latitude and longitude; keep the source system in `original_crs` ([CRS scope](/format/spec/v2/format-reference#crs-scope)). - **Total depth is not usable depth.** The drilled depth belongs in the deepest `bore_hole[].to`. `well_depth` is only for a measured current depth. - **Datetimes carry an offset.** Test and sample times are written with the local UTC offset, for example `-03:00` in most of Brazil ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)). ::do-dont #do Write what the report states, using the recommended value it names (here "tubo geomecânico", geomechanical PVC), and leave out what it does not say. ```json { "from": 0, "to": 36, "type": "geomechanical_pvc", "diameter": 168.3 } ``` #dont Fill gaps with plausible values or map the material to the nearest recommended word. ```json { "from": 0, "to": 36, "type": "pvc", "diameter": 150 } ``` :: ## With an AI assistant The [`welldot-converter` skill](/ai/conversion-skill) is a written procedure an assistant follows to read the report, extract every section, write the file and report what it left out. It works with Claude, ChatGPT, Gemini, Cursor and other assistants. ```mermaid flowchart LR R["Report
(PDF, scan, DOCX)"] --> A["Assistant
+ welldot-converter"] A --> W[".well draft"] W --> V{"validate_well
(docs MCP server)"} V -->|errors| A V -->|valid| H["Human review
against the report"] H --> F["Final .well"] ``` Connect the assistant to the [docs MCP server](/ai/mcp) so it can check its own output with `validate_well` and look up the spec instead of relying on memory. ::caution A valid file is not a correct file. Validation checks types and required fields; it cannot know that a casing is 168.3 mm and not 152.4 mm. Compare the drawn profile with the report's drawing, and the numbers with the tables, before you rely on a converted file. :: ## Review checklist 1. The profile drawn at [welldot.org](https://welldot.org) matches the report's construction drawing. 2. Every depth interval in the report has an entry, and no entry has invented depths. 3. Diameters are in millimeters and look plausible (a casing is narrower than its hole). 4. Lithology descriptions are the report's text, and each layer has a sensible texture code. 5. The static level, test rates and durations match the test sheet; levels above ground are negative. 6. The file passes [validation](/format/guides/validating). 7. Anything uncertain is noted in `obs` or a `notes` field rather than silently decided. ## Registry imports For Brazilian wells registered in SIAGAS, [`@welldot/utils`](/utils/guides/siagas-import) can import a well directly from the registry page, with warnings for anything it could not carry over. # Validating > Check that a .well file is valid, in the browser, with an AI assistant, from TypeScript or with the JSON Schema in any language, and read the error messages. A file is valid when it follows the schema: the required blocks are present, every field has the right type and every closed vocabulary has an allowed value. There is one definition of valid, `parseWell` from [`@welldot/core`](/core), and every route below uses it or the JSON Schema generated from it. ## Ways to validate ::tabs :::tabs-item{label="Browser" icon="i-lucide-globe"} Open [welldot.org](https://welldot.org) and drop the file on the editor. An invalid file is reported with the path of each problem. On this site, a `::well-preview` block validates its content and lists the errors instead of drawing. ::: :::tabs-item{label="AI assistant" icon="i-lucide-sparkles"} Connect your assistant to the [docs MCP server](/ai/mcp) and ask it to validate the file. The `validate_well` tool runs `parseWell` and returns errors with their paths, plus advisory warnings. ::: :::tabs-item{label="TypeScript" icon="i-lucide-code"} ```ts import { parseWell } from '@welldot/core'; try { const well = parseWell(text); } catch (error) { // Schema problems: a ZodError whose issues each have a path. // Invalid JSON or an unsupported version: a plain Error (SyntaxError for JSON). } ``` See [Parsing and validating](/core/guides/parsing-and-validating). ::: :::tabs-item{label="Other languages" icon="i-lucide-braces"} Validate against the JSON Schema with any draft 2020-12 validator: ```text https://welldot.org/schema/v2/well.schema.json ``` See [JSON Schema](/format/reference/json-schema). ::: :: ## Reading the errors Each error has a path into the document and a message. Array positions count from zero. | Error | Meaning | | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `well_screen[0].screen_slot: Required` | The first screen has no `screen_slot`. | | `bore_hole[1].to: Expected number, received string` | A depth was written in quotes, or with its unit (`"60 m"`). | | `hole_fill[2].type: Invalid enum value. Expected 'gravel_pack' \| 'seal', received 'gravel'` | `hole_fill` types are a closed list. | | `fractures: Required` | A required list is missing; write `[]` if there are none. | | `construction_date: construction_date must be YYYY-MM-DD` | A date in another form, such as `20/05/2019`. | | `version: Unsupported .well format version: "3.0"` (code `unsupported_version`) | The file declares a version this reader does not know. | The last row is not a schema error. `parseWell` throws a plain `Error` with the message `Unsupported .well format version: "3.0"` (or `Malformed .well format version: …`) before the schema runs. The MCP validator and this site report it with the path `version` and the code `unsupported_version`. Fix the first errors first: one missing bracket or a wrong type high in the file can cause several messages below it. ## Errors and warnings **Errors** make a file invalid. They come from the schema. **Warnings** do not. They point at data that is valid but probably wrong, or that a calculation cannot use: a permit whose validity ends before it starts, a meter reading that goes backwards, a pump intake below the bottom of the well, a dangling reference. The spec marks these "warnings, never rejection"; see for example [permit validation](/format/spec/v2/object-schemas#validation-warnings-never-rejection-1) and [dangling references](/format/spec/v2/format-reference#dangling-references). The MCP server returns them separately, computed by [`@welldot/utils`](/utils). ## What validation does not check Validation checks shape, not truth. It does not check that: - a casing fits inside its hole, or that intervals do not overlap; - `from` is smaller than `to`; - the numbers match the source report; - the texture code fits the description. Some of these are good candidates for warnings in your own tools. The spec decides which rules exist; a validator never adds its own, so that every tool accepts the same files. ## The JSON Schema and the library The JSON Schema is generated from the same Zod validators that `parseWell` uses, so they accept the same documents. A few rules can only be expressed in code, and `parseWell` also upgrades v1 files before checking them. When in doubt, `parseWell` is the reference. See [Schema validation](/format/spec/v2/interoperability#schema-validation) in the spec. # Extending for your own needs > Add the data your project needs to .well files with x- extensions, decide between a local extension, a profile and a spec change, and propose a change to the format. Sooner or later a project needs something the spec does not define: an internal asset code, a field the local regulator asks for, a measurement nobody else takes. This guide shows how to add it without breaking other tools, and how to get it into the format when it is useful to everyone. ## Decide where the data belongs ```mermaid flowchart TD Q["Does the spec already
have a field for it?"] -->|yes| U["Use it.
Search the object reference."] Q -->|no| L{"Who needs it?"} L -->|"only your tool"| X["x-vendor-name field or block"] L -->|"everyone under one
regulator or program"| P["A profile, plus an x- extension
if the field is new"] L -->|"most well data"| S["Propose a spec change"] ``` Before adding anything, search the [object reference](/format/reference/objects) and the [recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies). Many needs are already covered: free-text `notes` and `obs`, attachments for documents, open vocabularies that accept any value. ## A local extension Use the `x-` prefix, with your organization's name, on a field or a whole block ([Extensibility](/format/spec/v2/format-reference#extensibility)): ```json { "version": "2.4", "name": "PT-07", "x-acme-asset": { "code": "ACM-PT-07", "cost_center": "4410" }, "well_case": [ { "from": 0, "to": 40, "type": "carbon_steel", "diameter": 219, "x-acme-batch": "B-2291" } ] } ``` - Prefix with your name (`x-acme-…`) so you do not collide with another tool's `x-custom` (recommended since 2.4). - Keep units SI and names self-describing, as the rest of the format does ([naming rules](/format/spec/v2/format-reference#naming-rules-since-v23)). - Do not use an extension to restate a value the spec already holds; two sources for one fact will drift. Every conforming reader keeps your extension and writes it back unchanged, so the file can pass through other tools safely. [Extensibility](/format/concepts/extensibility) explains the rule. ## A new value in an open vocabulary Write it with the prefix: `"drilling_method": "x-jetting"`, `"category": "x-site_visit"` ([custom vocabulary values](/format/spec/v2/format-reference#custom-vocabulary-values--x--prefix)). Closed vocabularies, such as `hole_fill[].type`, cannot be extended this way; a new value there needs a spec change. ## Requirements for a group: profiles When an agency or a monitoring network needs every file to carry certain fields or values, publish a [profile](/format/concepts/profiles): a JSON Schema at an HTTPS address that files list in `profiles`. The profile can require your `x-` extension, so the extension becomes part of the agreement without changing the core format ([profile schema requirements](/format/spec/v2/format-reference#profile-schema-requirements)). ## Propose a change to the format If an extension would help most people who record wells, propose it for the spec. ::steps ### Open an issue Describe the data, who records it, where it appears in real reports and an example. Format changes start with an issue on [GitHub](https://github.com/welldothq/welldot/issues); see the repository's [contributing guide](https://github.com/welldothq/welldot/blob/main/CONTRIBUTING.md). ### Agree on the shape Field name, type, unit, whether it is required, and which block it belongs to. The new field must fit the [design principles](/format/spec/v2/overview#design-principles): SI units, no derived values, no jurisdiction-specific meaning. ### Change the spec first The spec text in `spec/` is edited first, in plain Markdown, then the reference implementation, the JSON Schema and the docs. The [update rules](/contributing/update-rules) list everything that changes together. :: A new optional field ships in a minor revision and never breaks existing files ([deprecation policy](/format/spec/v2/format-reference#deprecation-policy)). An `x-` vocabulary value that is adopted stays equivalent to the new value until the next major version ([vocabulary value promotion](/format/spec/v2/format-reference#vocabulary-value-promotion)). # Building an editor or database on .well > Design decisions for software that reads and writes .well files at scale, such as editors, registries, databases and data pipelines, with links to the rules behind each. This guide is for developers building a second editor, a well registry, a database import or a data pipeline on the format. It lists the decisions that are easy to get wrong and the spec rules that settle them. ## Reading - **Accept every version you can.** Read `1`, `2` and `"2.x"`; upgrade v1 on read; reject malformed and unknown major versions with a clear message ([parser version handling](/format/spec/v2/format-reference#parser-version-handling)). `parseWell` in `@welldot/core` does all of this. - **Do not assume order.** Events, samples and log entries may appear in any order. Sort by instant (as absolute UTC time) and use `sequence` to break ties ([ordering and ties](/format/spec/v2/format-reference#ordering-and-ties)). - **Treat absent as unknown.** A missing static level is not zero; a missing status means the status is unknown ([absent values](/format/spec/v2/format-reference#absent-values-since-v24)). - **Show unknown vocabulary values as they are.** Do not map `x-jetting` to "other" ([recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies)). ## Writing - **Round-trip everything.** Preserve `x-` members and any member you do not know, at every level ([design principles](/format/spec/v2/overview#design-principles), "Preserve unknown members"). The safest pattern is to parse into an object, change the fields you edit, and serialize the same object. - **Write the current version.** New files are `"2.4"`. Upgrading `2` to `"2.4"` is safe; `serializeWell` writes it. - **Omit, do not null.** Remove optional fields that have no value ([absent values](/format/spec/v2/format-reference#absent-values-since-v24)). - **Store SI, display anything.** Convert to the user's units on screen and back on input. The file never changes unit ([Units](/format/spec/v2/format-reference#units)). - **Store keys, not labels.** Write `stainless_steel`, show "Stainless steel" or "Aço inox". ## Ids and references - Ids are strings, unique per block; generate UUID v4 for new records ([`id` field convention](/format/spec/v2/format-reference#id-field-convention)). - References resolve only within the same file ([cross-array references](/format/spec/v2/format-reference#cross-array-references)). - A dangling reference is a warning; never delete it silently on import or export ([dangling references](/format/spec/v2/format-reference#dangling-references)). - Duplicate ids are malformed. An interactive editor asks the user to merge, rename or abort; a batch importer rejects the file. Never rename automatically ([duplicate ids](/format/spec/v2/format-reference#duplicate-ids-within-a-scope)). ## Editing history correctly Each top-level list is one of three kinds, and the kind decides how a correction is recorded ([block kinds](/format/spec/v2/format-reference#block-kinds-since-v23)): | Kind | Blocks | How to correct | | -------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Ledger | `hydrodynamic_events`, `production`, `water_samples` | Append a new entry with `corrects`. Editing in place is allowed but not recommended after export. | | Mutable record | `history_logs`, `permits`, `operating_regime` | Edit in place and update `updated_at`. | | Installation | `pump_installations`, `meters` | Edit in place; a reinstalled unit is a new entry with the same serial. | Construction and geology are edited in place. An `aquifer_analysis` is an interpretation: a new interpretation is a new entry, and earlier ones stay. ## Derived values Never store what can be computed: drawdown, specific capacity from readings, production volumes, permit status, condition deadlines, exceedances ([design principles](/format/spec/v2/overview#design-principles)). Compute them when you display or query. [`@welldot/utils`](/utils) implements the spec's derivations, including the normative ones for [volumes](/format/spec/v2/object-schemas#volume-derivation-normative) and [permit deadlines](/format/spec/v2/object-schemas#deadline-generation-normative), and [Querying current state](/format/spec/v2/object-schemas#querying-current-state) describes how to answer "what is the well's status now". ## Databases A `.well` file is a document, and the simplest durable storage is the document itself: a JSON column, a document store or files in object storage. Index what you query (identifiers, location, depth, status) in separate columns, derived from the document on write. When you normalize into tables: - Key a well by its `well_id` entries (authority plus id), not by `name`; the pair is unique within a file ([`well_id`](/format/spec/v2/format-reference#well_id)). - Key child records by the well plus their `id`; ids are only unique within one block of one file. - Keep the original document, or at least its `x-` members and unknown fields, so you can export a faithful file again. - Collective permits and campaign blanks are repeated in each well's file with the same id; you may merge them by id ([repeated records](/format/spec/v2/format-reference#repeated-records-since-v24)). ## Sharing and privacy To publish a well without its owner data, permits or location, remove those parts rather than blanking them, so the result is still valid. `redactWell` in `@welldot/core` does this by section ([Serializing and sharing](/core/guides/serializing-and-sharing)). ## Interoperability - GroundWater ML 2 (GWML2) is the OGC standard for groundwater data; the spec maps `.well` objects to its classes ([relationship to GWML2](/format/spec/v2/interoperability#relationship-to-gwml2)). - `@context` is reserved for a JSON-LD context; it is optional and readers must accept it ([JSON-LD context](/format/spec/v2/interoperability#json-ld-context-reserved)). - The MIME type is `application/vnd.well+json` and the extension `.well` ([overview](/format/spec/v2/overview)). ## Testing your implementation Run your reader and writer against all 19 [example wells](/format/reference/examples): every one must parse, and writing it back must keep every member. Include the [inconsistent data](/format/reference/examples) and [incomplete record](/format/reference/examples) examples, which are valid on purpose. # JSON Schema > The published JSON Schema for .well v2, where it comes from, and how to validate files with it in editors and in languages other than TypeScript. The `.well` v2 JSON Schema describes the shape of a valid file in a form every language can use. It is published at a stable address, which is also its `$id`: ```text https://welldot.org/schema/v2/well.schema.json ``` [Open the schema](https://welldot.org/schema/v2/well.schema.json). It uses JSON Schema draft 2020-12. ## Where it comes from The schema is generated from the Zod validators of [`@welldot/core`](/core), the reference implementation, so the schema and the library accept the same documents. It lives in the repository at [`spec/schema/v2/well.schema.json`](https://github.com/welldothq/welldot/blob/main/spec/schema/v2/well.schema.json) and is never edited by hand; CI fails when it is out of date. It also ships in the npm package as `@welldot/core/well.schema.json`. The prose specification remains the authority on meaning. The schema covers structure: required fields, types, enumerations and patterns. Rules that need context, such as how a reader handles a v1 file, are in the spec and in `parseWell` ([Schema validation](/format/spec/v2/interoperability#schema-validation)). ## In an editor Add `$schema` to a file to get completion and inline errors in editors that support JSON Schema, such as VS Code (map `*.well` to JSON first): ```json { "$schema": "https://welldot.org/schema/v2/well.schema.json", "version": "2.4" } ``` ## In code ::code-group ```ts [JavaScript (ajv)] import Ajv2020 from 'ajv/dist/2020.js'; import addFormats from 'ajv-formats'; const schema = await ( await fetch('https://welldot.org/schema/v2/well.schema.json') ).json(); const ajv = new Ajv2020({ allErrors: true, strict: false }); addFormats(ajv); const validate = ajv.compile(schema); if (!validate(JSON.parse(text))) console.log(validate.errors); ``` ```python [Python (jsonschema)] import json, urllib.request from jsonschema import Draft202012Validator url = "https://welldot.org/schema/v2/well.schema.json" schema = json.load(urllib.request.urlopen(url)) validator = Draft202012Validator(schema) with open("PT-01.well", encoding="utf-8") as f: for error in validator.iter_errors(json.load(f)): print(list(error.path), error.message) ``` :: In TypeScript, prefer `parseWell` from `@welldot/core`: it is the same rule set, returns typed data and upgrades v1 files ([Parsing and validating](/core/guides/parsing-and-validating)). ## Versions of the schema There is one schema per major version. The v2 schema accepts every v2 revision, from the integer `2` to `"2.4"`. A future v3 would get its own address ([deprecation policy](/format/spec/v2/format-reference#deprecation-policy)). ## Related - [Object reference](/format/reference/objects): the schema rendered as one page per object. - [Validating](/format/guides/validating): all the ways to check a file. # Example wells > Nineteen complete, valid .well files built on real Brazilian wells, covering crystalline, karst, sedimentary, shallow and problem cases, with live previews and download links. The format ships with 19 complete example files. Each one is valid and each shows a different situation: rock types, construction styles, test records, operation records, permits, water quality and the messy records real archives contain. Use them to learn the format, to test software and as starting points for your own files. They are built on real wells from SIAGAS, the groundwater registry of the Geological Survey of Brazil: region, geology, construction and the order of magnitude of test values come from the registry. Names, identifiers, permits, analyses and readings are invented but consistent. Every file says so in an `x-welldot-example` block. ## Get the files - Download: `https://welldot.org/examples/.well` (the links below). - From npm: `@welldot/core/examples/.well`, installed with [`@welldot/core`](/core/getting-started/installation). - On GitHub: [`packages/core/examples`](https://github.com/welldothq/welldot/tree/main/packages/core/examples), with a README on how they were made. - In the editor: [welldot.org](https://welldot.org) offers them when you start a new well. ## Three to start with A crystalline supply well with a long history: telescoping hole, screens alternating with casing, centralizers, a 24 h test with its analysis, three permits, meters and production. ::well-preview{example="coluna-mg-cristalino-abastecimento"} :: A karst well in the Bambuí limestone, with three caves and a solution fracture. ::well-preview{example="irece-ba-carste-bambui"} :: A hand-dug well (cacimba), 10 m deep and 1.2 m wide, with brick lining. ::well-preview{example="sao-gabriel-rs-cacimba"} :: ## All examples | Example | Place | Depth | What it shows | Id | | -------------------------------------------------------------------------------------------------------------------- | ------------------ | ------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | [Public supply in a crystalline aquifer](https://welldot.org/examples/coluna-mg-cristalino-abastecimento.well) | Coluna/MG | 204 m | Granite under thick saprolite, Nold screens, 24 h test, permits, meters and production. | `coluna-mg-cristalino-abastecimento` | | [Industrial karst well, decommissioned](https://welldot.org/examples/sete-lagoas-mg-carste-industrial.well) | Sete Lagoas/MG | 218 m | Bambuí limestone with a cavity, old test record, 1989 analysis, plugged in 2021. | `sete-lagoas-mg-carste-industrial` | | [Irecê karst: caves and nitrate](https://welldot.org/examples/irece-ba-carste-bambui.well) | Irecê/BA | 60 m | Bambuí limestone with caves, 85 m³/h/m specific capacity, very hard water and nitrate above the limit in 2023. | `irece-ba-carste-bambui` | | [Public supply in the Lagoa Santa karst](https://welldot.org/examples/lagoa-santa-mg-carste-abastecimento.well) | Lagoa Santa/MG | 78 m | 100 m³/h with 3 m of drawdown, 24 h test, telemetry and a permit renewed with a lower rate inside the protected area. | `lagoa-santa-mg-carste-abastecimento` | | [Incomplete record](https://welldot.org/examples/minas-novas-mg-cadastro-incompleto.well) | Minas Novas/MG | 210 m | Macaúbas schist; construction and lithology only, no test or analysis. | `minas-novas-mg-cadastro-incompleto` | | [Brackish water in semi-arid crystalline rock](https://welldot.org/examples/quixeramobim-ce-cristalino-salobro.well) | Quixeramobim/CE | 60 m | Fractured biotite schist, 3.5 g/L TDS, community desalinator and salinity during the 2012–2017 drought. | `quixeramobim-ce-cristalino-salobro` | | [Rural gneiss well: from insignificant use to a permit](https://welldot.org/examples/colatina-es-gnaisse-rural.well) | Colatina/ES | 80 m | One producing fracture at 67 m, insignificant-use certificate, irrigation permit with a meter and yearly reports. | `colatina-es-gnaisse-rural` | | [1561 m stratigraphic well (Guarani)](https://welldot.org/examples/rinopolis-sp-estratigrafico-guarani.well) | Rinópolis/SP | 1561 m | Bauru, 1092 m of basalt and confined Guarani; flowing, alkaline thermal water. | `rinopolis-sp-estratigrafico-guarani` | | [Telescoping well, Bauru + Serra Geral](https://welldot.org/examples/estrela-doeste-sp-bauru-serra-geral.well) | Estrela d'Oeste/SP | 201 m | Five hole diameters, five screens, mixed intake and rising nitrate. | `estrela-doeste-sp-bauru-serra-geral` | | [Inconsistent data](https://welldot.org/examples/gastao-vidigal-sp-dados-inconsistentes.well) | Gastão Vidigal/SP | 207 m | Casing from 0.7 m, duplicated grout and an implausible test, kept on purpose. | `gastao-vidigal-sp-dados-inconsistentes` | | [Deep urban supply well (Barreiras/Pirabas)](https://welldot.org/examples/belem-pa-pirabas-abastecimento.well) | Belém/PA | 265 m | 14"→8" reduction, stainless screens, step test, permit, telemetry and 25 layers. | `belem-pa-pirabas-abastecimento` | | [Licensed condominium well (CNARH + permit)](https://welldot.org/examples/belem-pa-condominio-outorga.well) | Belém/PA | 60 m | Current regulatory flow: CNARH, permit with conditions, meter and monthly readings. | `belem-pa-condominio-outorga` | | [Water quality: acidic water and censored results](https://welldot.org/examples/belem-pa-qualidade-agua-acida.well) | Icoaraci, Belém/PA | 60 m | pH below 5, values below the detection limit, duplicate, blank and bacterial contamination. | `belem-pa-qualidade-agua-acida` | | [Clogged well](https://welldot.org/examples/belem-pa-poco-colmatado.well) | Belém/PA | 102 m | Specific capacity loss, camera inspection, failed rehabilitation and plugging. | `belem-pa-poco-colmatado` | | [Abandoned shallow well](https://welldot.org/examples/belem-pa-poco-raso-abandonado.well) | Belém/PA | 12 m | 12 m jetted well, 48 m³/h, contaminated by sewage and abandoned unplugged. | `belem-pa-poco-raso-abandonado` | | [Flowing well in the Alter do Chão aquifer](https://welldot.org/examples/santarem-pa-jorrante-alter-do-chao.well) | Santarém/PA | 160 m | Head above ground, 52 h test and Jacob analysis (T = 0.003 m²/s). | `santarem-pa-jorrante-alter-do-chao` | | [Rural hand-dug well](https://welldot.org/examples/sao-gabriel-rs-cacimba.well) | São Gabriel/RS | 10 m | Hand-dug well, 2024 flood, coliforms and disinfection. | `sao-gabriel-rs-cacimba` | | [Shallow Dunas-Barreiras well with nitrate](https://welldot.org/examples/extremoz-rn-barreiras-raso.well) | Extremoz/RN | 26 m | Stainless screens, flowing in 1978 and now 3 m below ground; nitrate above 10 mg/L N since 2020. | `extremoz-rn-barreiras-raso` | | [Fuel station monitoring well](https://welldot.org/examples/belem-pa-monitoramento-posto.well) | Belém/PA | 12 m | 2" well, low-flow sampling and BTEX decline during remediation. | `belem-pa-monitoramento-posto` | ## Use them in these docs Writers can draw any example on a page with its id: ```mdc ::well-preview{example="santarem-pa-jorrante-alter-do-chao"} :: ``` See [Content components](/contributing/components). ## How they are checked The `@welldot/core` test suite checks that every example parses, survives a serialize and parse round trip, and matches the gallery index. This site's CI validates every example a page uses. # Reference > Lookup material for the .well format, generated from the schema and the spec: objects and fields, the JSON Schema, vocabularies and example files. These pages are for looking things up. The object reference and the JSON Schema are generated from the reference implementation, so they always match it. The meaning of each field, and the rules around it, are in the [specification](/format/spec), and every object page links to its section there. ::card-group :::card{title="Object reference" icon="i-lucide-table-properties" to="/format/reference/objects"} Every block and object of a `.well` file, with its fields, types, constraints and spec section. ::: :::card{title="JSON Schema" icon="i-lucide-file-json" to="/format/reference/json-schema"} The published schema, how it is generated and how to validate with it in any language. ::: :::card{title="Example wells" icon="i-lucide-library" to="/format/reference/examples"} Nineteen complete files with live previews, for learning and testing. ::: :::card{title="FGDC textures" icon="i-lucide-grid-3x3" to="/format/spec/reference/fgdc-textures"} All 282 lithology pattern codes used by `texture`. ::: :: ## Vocabularies The recommended and allowed values of each field are listed in the specification: - [Recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies): construction methods, uses, materials, event types, log categories, permit types and more. - [Water quality recommended values](/format/spec/v2/water-quality#recommended-values) and the [parameter vocabulary](/format/spec/v2/water-quality#parameter-vocabulary). - [Texture vocabularies](/format/spec/v2/object-schemas#vocabulary-values) for lithology patterns. The same lists, with English and Portuguese labels, are exported by `@welldot/core` as data ([Vocabularies and textures](/core/guides/vocabularies-and-textures)). [Vocabularies](/format/concepts/vocabularies) explains open and closed lists. ## Units Every field's unit is in the spec's [field-to-unit binding](/format/spec/v2/format-reference#field-to-unit-binding-normative). [Units and precision](/format/concepts/units-and-precision) summarizes it. # The specification > The normative .well specification, how it is organized, how it relates to this site and to the packages, and how it changes. The specification is the standard that defines the `.well` format. It is written as plain Markdown in the [`spec/` folder](https://github.com/welldothq/welldot/tree/main/spec) of the repository and rendered on this site as it is, with heading anchors and links adapted for the web. Everything else on this site explains it; when a guide and the spec seem to disagree, the spec is right. The current revision is **2.4**. ## The documents | Document | What it covers | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Overview](/format/spec/v2/overview) | The changes in each revision, the purpose of the format, its design principles and its known limitations. | | [Format reference](/format/spec/v2/format-reference) | Top-level fields, versioning and deprecation, datetimes, recommended vocabularies, `well_id`, `location`, units, profiles, extensibility, cross-references and precision. | | [Object schemas](/format/spec/v2/object-schemas) | Every object: construction, geology, hydrodynamic events, aquifer analysis, history logs, pumps, permits, meters, production, operating regime, and a complete example. | | [Water quality](/format/spec/v2/water-quality) | Water samples and results, their units, the parameter vocabulary and regulatory profiles. | | [Interoperability](/format/spec/v2/interoperability) | The reserved JSON-LD context, the mapping to GroundWater ML 2 and JSON Schema validation. | | [FGDC textures](/format/spec/reference/fgdc-textures) | The 282 lithology pattern codes. | | [v1 (archived)](/format/spec/v1/well-format) | The first version, kept for reference. v2 readers upgrade v1 files on read. | ## How to read it The spec uses the keywords MUST, SHOULD and MAY in their usual standards sense: MUST is a requirement, SHOULD a strong recommendation, MAY an option. Sections marked _(since v2.x)_ were added in that revision. If you are new to the format, start with [What is .well?](/format/getting-started/what-is-well) and the [concepts](/format/concepts/depths-and-intervals), then come back to the spec for the exact rules. ## How it relates to the code - [`@welldot/core`](/core) implements the spec. Its tests compare the vocabulary tables in the spec with the published constants and parse the spec's complete example, so the two cannot drift apart silently. - The [JSON Schema](/format/reference/json-schema) is generated from `@welldot/core` and published at `https://welldot.org/schema/v2/well.schema.json`. - The [object reference](/format/reference/objects) is generated from the schema and links back to the spec sections. ## How it changes Changes to the format start as an issue on [GitHub](https://github.com/welldothq/welldot/issues). The spec text is edited first, then the implementation and these docs, in the same change ([update rules](/contributing/update-rules)). Each revision adds a "Changes in v2.x" section to the overview; the [changelog](/format/changelog) links to them. The spec files are licensed under Apache 2.0, like the rest of the project. # Format changelog > The revisions of the .well format, each linking to its "Changes in v2.x" section in the specification overview. The format's changelog is part of the specification: each revision adds a "Changes in v2.x" section to the [overview](/format/spec/v2/overview). This page lists the revisions and links to those sections, so there is one record of what changed. Package releases have their own changelogs ([core](/core/changelog), [utils](/utils/changelog), [render](/render/changelog)). | Revision | `version` written | Shipped in | Changes | | -------- | ----------------- | --------------------- | ----------------------------------------------------------- | | 2.4 | `"2.4"` | `@welldot/core` 0.5.0 | [Changes in v2.4](/format/spec/v2/overview#changes-in-v24) | | 2.3 | `2` | `@welldot/core` 0.4.0 | [Changes in v2.3](/format/spec/v2/overview#changes-in-v23) | | 2.1 | `2` | `@welldot/core` 0.3.0 | [Changes in v2.1](/format/spec/v2/overview#changes-in-v21) | | 2.0 | `2` | `@welldot/core` 0.2.0 | [Changes from v1](/format/spec/v2/overview#changes-from-v1) | | 1 | `1` | | [v1 specification (archived)](/format/spec/v1/well-format) | ## In brief - **2.4** writes the version as the string `"M.m"` and adds the site time zone, data logger series, offline `.wellz` packages and vendor-named extensions, plus clarifications. Readers older than `@welldot/core` 0.5.0 reject `"2.4"` files. No deprecations ([details](/format/spec/v2/overview#changes-in-v24)). - **2.3** adds the operational blocks (attachments, pumps, permits, meters, production, operating regime) and water quality samples, with recommended vocabularies for most open fields. No deprecations ([details](/format/spec/v2/overview#changes-in-v23)). - **2.1** adds `well_purpose` and `centralizers`, and deprecates `well_type: "artesian"` ([details](/format/spec/v2/overview#changes-in-v21), [deprecations](/format/spec/v2/overview#deprecations-1)). - **2.0** adds `location`, `well_id`, profiles, the texture object, hydrodynamic events, aquifer analysis, history logs and precision fields, and fixes all units to SI ([details](/format/spec/v2/overview#changes-from-v1)). Within version 2, every revision is additive: a valid file from an earlier revision stays valid ([deprecation policy](/format/spec/v2/format-reference#deprecation-policy)). [Versions and deprecation](/format/concepts/versions-and-deprecation) explains how readers handle them. # `.well` File Format Specification — Version 2.4: Overview **Version:** 2.4 **Extension:** `.well` **Encoding:** UTF-8 **Base format:** JSON **MIME type:** `application/vnd.well+json` **JSON Schema:** `https://welldot.org/schema/v2/well.schema.json` **JSON Schema draft:** 2020-12 **JSON-LD Context (optional):** `https://welldot.org/context/v2.jsonld` **Status:** v2.0 ratified — shipped in `@welldot/core` v0.2.0 · v2.1 shipped in `@welldot/core` v0.3.0 · v2.3 shipped in `@welldot/core` v0.4.0 · v2.4 shipped in `@welldot/core` v0.5.0 **See also:** [format-reference.md](./format-reference.md) · [object-schemas.md](./object-schemas.md) · [interoperability.md](./interoperability.md) · [water-quality.md](./water-quality.md) ## Overview The `.well` v2 specification is split into five documents. This one gives the context: what changed in each revision (v2.4, v2.3, v2.1 and from v1), why the format exists, the design principles the other parts follow, and the known limitations. The other four are: - [format-reference.md](./format-reference.md) — the top-level fields, versioning and deprecation, datetimes, recommended vocabularies, `well_id`, `location`, units, profiles, extensibility, cross-reference rules and precision. - [object-schemas.md](./object-schemas.md) — every object schema, the `hydrodynamic_events` ledger, `aquifer_analysis`, `history_logs`, the operational blocks (pumps, permits, meters, production, operating regime) and a complete example. - [water-quality.md](./water-quality.md) — water samples and results, their units, the parameter vocabulary and regulatory profiles. - [interoperability.md](./interoperability.md) — the JSON-LD context, the relationship to GWML2 and JSON Schema validation. ## Table of contents - [Changes in v2.4](#changes-in-v24) - [Deprecations](#deprecations) - [Changes in v2.3](#changes-in-v23) - [Changes in v2.1](#changes-in-v21) - [Deprecations](#deprecations-1) - [Changes from v1](#changes-from-v1) - [Purpose](#purpose) - [Design Principles](#design-principles) - [Known Limitations of v2](#known-limitations-of-v2) --- ## Changes in v2.4 v2.4 files write `"version": "2.4"`; parsers up to `@welldot/core` 0.4.0 reject them. Beyond that, v2.4 is an additive revision: every valid v2.3 document stays valid when its `version` is rewritten, and the integer `2` still means "v2.0 to v2.3, revision unknown". ``` v2.4 additions: - version is the string "M.m" (^\d+\.\d+$), compared as two integers, never as text; patch levels are not allowed; readers accept 1, 2 and "M.m" - location.properties.timezone (optional, IANA name): the local date of an instant is its date converted to this zone; a missing timezone in a file with condition fulfillments emits the warning missing_timezone - logger_series (hydrodynamic event type): a continuous level series recorded by an installed sensor, with sensor_depth, interval, barometric_compensation and aggregation (none, hourly, daily with depth_min / depth_max); not a controlled test, not a static level - LevelReading.datetime: the instant of a reading, required in a logger_series, where elapsed is absent - Offline packaging: an optional ZIP (.wellz) with the .well file at the root and attachments at relative paths; relative uri allowed and sha256 required only inside the package - Application extensions: x-- is RECOMMENDED (e.g. x-welldot-custom) v2.4 clarifications: - A known hydrodynamic event type is validated against its own schema and no longer falls back to the open x- rule; step counts per type are SHOULD (a free count is valid), and steps must be valid PumpingStep entries - An optional field without a value is omitted, never written as null (storativity is no longer nullable); v2 integer files that still carry it are tolerated on read - Ledgers MAY be edited in place; editing is NOT RECOMMENDED after a record has been exported, and corrects remains the way to retract a measurement. Result.validation is editable too - Repeated records (collective permits, campaign blanks) share the same id and SHOULD have identical content; consumers MAY merge them by id. A fulfillment event_id or sample_id resolves only in the file of the well that produced it - Unresolved references MUST be preserved when a file is re-exported, never silently removed - A duplicated operating_regime.effective_from and an estimated_volume period_end not after period_start are warnings - daily_operating_time above 24 is normalized to 24 with a warning (the stored value stays as written); a negative value is an error - Attachment.document_type condition_evidence is typically found in permits[].conditions[].fulfillments - The attachment lists (id uniqueness, "root is not an aggregate", where attachments are allowed) name the same set of records - history_logs updated_at may be earlier than datetime for a pre-dated entry; neither ordering emits a warning - well_id: producers SHOULD mark exactly one entry primary: true; "the first entry is canonical" is only the fallback ``` ### Deprecations None. --- ## Changes in v2.3 v2.3 is an additive minor revision. Every valid v2.1 document is a valid v2.3 document, and documents keep declaring `"version": 2`. It adds the operational blocks (equipment, documents, permits, metering, production and operating regime) and the water quality block (`water_samples`). ``` v2.3 additions: - attachments (optional Attachment[]): root-level documents about the well as a whole (drilling report, as-built drawing, registry record…) - Attachment becomes a common type and gains document_type (drilling_report, as_built_drawing, registry_record, photo, permit_document, condition_evidence, pump_curve, field_sheet, test_report, lab_report, invoice) - pump_installations (optional PumpInstallation[]): pump installation history, with nameplate, intake depth, riser and electrical data - permits (optional Permit[]): legal instruments governing abstraction (outorga, dispensa, cadastro) with validity, granted flow, daily operating time, volume limits, monthly schedule and conditions (condicionantes) whose deadlines are derived from ISO 8601 date durations; renewals are linked with supersedes. Each permit carries its identifier and/or request_identifier (verbatim, any format), a closed administrative status (requested, granted, suspended, revoked, denied, withdrawn), an administrative history (filing, process, notifications, fees, with done/due_date and attachments) and, on each condition, a responsible and the fulfillments of its deadlines (datetime, due_date, author, event_id, sample_id, attachments) - meters (optional Meter[]): totalizer (hidrômetro) installation history; device facts only (type, serial, nominal diameter, max_reading for rollover) - production (optional ProductionEntry[]): ledger of meter_reading entries (register values in m³) and estimated_volume entries (volumes estimated for a period without a meter); volumes are derived from consecutive readings, never stored as totals. Volumes declared to a regulator are not production data: they are the fulfillment of the permit's reporting condition - operating_regime (optional OperatingRegime[]): declared flow rate, daily operating time and days per week, each entry in force from effective_from - history_logs category status_change, with status (active, maintenance, inactive, decommissioned, abandoned): the well's operating status; current status is unknown without one - history_logs category maintenance gains maintenance_type, pump_installation_id and meter_id - history_logs gains hydrodynamic_event_ids and sample_ids (any category): references to the data an entry produced or observed, never copies of measured values - hydrodynamic_events[].corrects: retracts an earlier event (ledger correction) - hydrodynamic_events[].attachments, aquifer_analysis[].attachments - Canonical units kW (power), V (voltage), mm² (cable cross-section), m³ (volume), h (daily operating time only) - water_samples (optional WaterSample[]): ledger of water samples, each with its field and laboratory results in one list (distinguished by measured_in), sampling point (point depth or isolated interval, or the production pump), purge and stabilization readings, laboratory and chain of custody, field QA/QC (field_duplicate, split_sample via parent_sample_id; blanks), per-result filtration, lab flags and data validation; corrected reports and revalidations are new samples with corrects - Parameter identity { code, vocabulary } with vocabularies welldot, cas and x-…; the welldot parameter vocabulary (98 codes) is published as versioned data in @welldot/core, with CAS equivalences; the unit of a welldot or cas parameter comes from its code (substances always mg/L), and unit (UCUM) exists only for x- codes - Canonical units mg/L, µS/cm at 25 °C, °C, mV, NTU / FNU / FAU / FTU (by code), Hazen (uH), MPN/100 mL, CFU/100 mL, CFU/mL, Bq/L and µm - _resolution convention: analyzed_at_resolution and laboratory.received_at_resolution set to "day" mark an instant known only by its date; consumers MUST ignore the time of day - permit condition fulfillments gain sample_id (e.g. a water-quality monitoring condition) - well_screen[].slot_type (screen design: continuous_slot, slotted, bridge_slot, louver, perforated, nold); type remains the screen material - Recommended vocabularies for drilling_method, construction materials (well_case / well_screen / reduction type, riser_material), well_screen slot_type, centralizer steels, cement_pad.type, history_logs severity, measurement_method and aquifer_analysis method; every recommended vocabulary is published by @welldot/core with en/pt labels v2.3 clarifications: - Block kinds: every top-level array is a ledger, a mutable record or an installation; the kind decides how records are corrected - Open vocabulary values are stored as their key, never as a translated label - Datetime families: legal documents use calendar dates (YYYY-MM-DD), everything else uses RFC 3339 instants - Naming rules: no units in field names; nameplate values use the rated_ prefix; time intervals use period_start / period_end - supersedes (legal succession between permits) is distinct from corrects (ledger retraction) - Permit status and condition deadlines are derived on the local civil date at the well site, never stored - Volume derivation is normative: rollover via max_reading, never across two meters, unknown (never zero or negative) where readings do not cover; metered volume takes precedence over estimated volumes - history_logs category change_of_use is no longer a recommended value; a change of use is an event entry (the current use lives in well_purpose) - Jurisdiction neutrality: water quality limits, exceedances and national rules are never written to the file; limit sets (WHO, EU, Brazil…) are data in @welldot/core and local requirements enter through profiles ``` v2.3 contains no deprecations. --- ## Changes in v2.1 v2.1 is an additive minor revision. Every valid v2.0 document is a valid v2.1 document, and documents keep declaring `"version": 2`. ``` v2.1 additions: - well_purpose (optional string[]): intended use(s) of the well — production, monitoring, piezometer, water_level_indicator (INA), observation, exploration, injection, dewatering - centralizers (optional Centralizer[]): casing/screen centralizers as depth interval + spacing - history_logs category `change_of_use` v2.1 clarifications: - well_type describes the construction method only - Level sign convention: water levels above ground are negative (made explicit; already implied by "ground level as zero") ``` ### Deprecations | Deprecated | Replacement | Removal | | ----------------------- | --------------------------------------------------------------------------------------------------------- | ------- | | `well_type: "artesian"` | Construction method in `well_type` (usually `tubular`) + negative `static_level` in `hydrodynamic_events` | v3 | --- ## Changes from v1 ``` v2.0 additions: - location object (supersedes top-level lat/lng/elevation) - well_id (array of authority-scoped identifiers) - profiles (JSON Schema URLs for regulatory and domain-specific constraints) - texture object (multi-vocabulary, replaces v1 fgdc_texture string) - hydrodynamic_events (ledger of water-level observations) - aquifer_analysis (interpreted aquifer parameter sets) - history_logs (operational log of interventions and incidents, with updated_at edit timestamp) - optional @context (reserved for JSON-LD interoperability) - optional *_precision fields on key measurements - WellScreen.screen_slot renamed from screen_slot_mm v2.0 tightenings: - All numeric values are SI by spec. The format does not encode a units declaration; see § Units for the canonical mapping. v1 was de facto SI in practice and no v1 file in existence declared non-SI units, so this is a clarification rather than a breaking change. Display and input units are application concerns. v2.0 preserves all v1 fields. Parsers must accept v1 documents unchanged and must accept the following v1-to-v2 normalizations: - Top-level lat/lng/elevation → location.lat/lng/elevation - fgdc_texture: "" → texture: { code, vocabulary: "fgdc" } - screen_slot_mm → screen_slot (value preserved; both versions are in millimeters) - bole_hole → bore_hole (v1 typo compatibility) - diam_pol (inches) → diameter (mm), multiplied by 25.4 ``` --- ## Purpose The `.well` format is an open standard for representing water well data. It is designed to serve three distinct but complementary use cases: **Visualization.** The format contains sufficient constructive and geological detail to produce accurate technical well profile drawings — including borehole geometry, casing strings, screens, gravel packs, lithological columns, fractures, and caves — at true depth scale. **Registration.** The format captures the administrative and physical identity of a well, providing a complete static record suitable for submission to regulatory bodies, inclusion in national well registries, and long-term archival. **Research.** The structured geological and constructive data, combined with the hydrodynamic event ledger and interpreted aquifer parameters, enables cross-well analysis, aquifer characterization, and hydrogeological research. Field vocabulary choices are aligned with international standards. --- ## Design Principles - **Self-describing** — full field names are used throughout; a `.well` file can be read and understood without consulting the spec. - **Versioned** — includes a `version` field for forward compatibility; parsers must reject unrecognized versions. - **Self-contained** — a single file encodes the complete record of one well. - **Units are SI, declared by spec** — all numeric values in a `.well` file are stored in SI units. The format does not encode a per-file units declaration. See § Units for the canonical unit of each quantity. Conversion to and from a user's preferred display or input units is an application concern. - **CRS is declared, not assumed** — geographic coordinates are accompanied by an explicit coordinate reference system declaration. The default is WGS84 (`EPSG:4326`). - **Ground level as zero** — all depth values are measured from ground level (0); elevation above the WGS84 ellipsoid (or declared datum) is stored separately in `location.elevation`. Water levels above ground are negative. - **Geographic north** — all azimuth values are referenced to geographic north. - **Three block kinds** — every top-level array is one of three kinds, and the kind decides how records are corrected: - **Ledger** (`hydrodynamic_events`, `production`, `water_samples`) — grows by appending. Entries MAY be edited in place, but editing is NOT RECOMMENDED after an entry has been exported. A measurement correction (a re-measurement, a revised laboratory report) is best written as a new entry whose `corrects` points to the retracted entry; a retracted entry stays in the file but is excluded from every derivation. The format provides `id`, `datetime`, `sequence` and `corrects` to support this discipline; it does not enforce it. - **Mutable record** (`history_logs`, `permits`, `operating_regime`) — edited in place; `updated_at` records the last edit. - **Installation** (`pump_installations`, `meters`) — equipment present in the well for a period (`installed_at` / `removed_at`); edited in place with `updated_at`. - **Derived values are never stored raw** — `s/Q`, `Q/s`, drawdown `s`, and any other value computable from stored fields must not appear as stored fields. Applications compute them on demand. - **Jurisdiction neutrality** — the file records what was measured, never what it means under a given rule. Water quality limits, exceedances and national requirements are not stored and no field names a national standard; limit sets (WHO, EU, Brazil, …) are versioned data in `@welldot/core`, chosen by the consumer at display time, and local obligations (required parameters, sampling frequencies) enter through profiles. - **Interpreted results are versioned** — `aquifer_analysis` records which method was used, who performed the analysis, and which events were used as input. Multiple analyses may coexist. - **Graceful incompleteness** — a record with only a flow rate and a dynamic level is valid. A record with only a static level measurement is valid. Validators must not reject incomplete events. - **Mutable operational log with edit traceability** — `history_logs` entries are editable to allow correction of field errors. Each entry carries a separate `updated_at` timestamp that records when the entry itself was last modified, distinct from the `datetime` field that records when the logged event occurred. - **Preserve unknown members** — parsers must preserve and pass through any unrecognized fields or top-level blocks without modification, following the GeoJSON foreign members convention. This ensures forward compatibility. --- ## Known Limitations of v2 The following are recognized limitations of this version, reserved for future versions: - **No internationalization of text fields.** All free-text fields (`description`, `notes`, `author`, etc.) are opaque strings with no language tag. A file produced in Brazil will have Portuguese content; one from Norway will have Norwegian. v3 will consider BCP 47 language objects for text fields, with backward-compatible string fallback. - **No interpreted water quality assessment.** `water_samples` records samples and measured results only. Interpreted assessments (water class, hydrochemical facies as a stored judgment, fitness for use) have no block yet; facies, ion balance and exceedances are derived on demand. - **Collective permits are repeated per well.** A grant covering several wells is repeated in each well's file until cross-file references arrive in v3. - **No geophysical logs block.** Downhole geophysical surveys (resistivity, gamma ray, caliper) are out of scope for v2 and reserved for v3. - **No multi-well linking.** Storativity determination requires observation well data. The top-level `references` field name is reserved for v3 to link wells across files. --- _`.well` Format Specification v2.4_ # `.well` File Format Specification — Version 2.4: Format Reference **See also:** [overview.md](./overview.md) · [object-schemas.md](./object-schemas.md) · [interoperability.md](./interoperability.md) · [water-quality.md](./water-quality.md) ## Overview This part of the `.well` v2.4 specification defines the file as a whole: the top-level structure and fields, how versions and deprecations work, datetime conventions, the recommended values of every open vocabulary, `well_id` and `location`, the canonical units and which unit each field uses, `profiles`, the `x-` extension rules, cross-reference and uniqueness rules, and precision fields. The objects inside each block are specified in [object-schemas.md](./object-schemas.md). ## Table of contents - [`.well` File Format Specification — Version 2.4: Format Reference](#well-file-format-specification--version-24-format-reference) - [Overview](#overview) - [Table of contents](#table-of-contents) - [Top-level Structure](#top-level-structure) - [Absent values _(since v2.4)_](#absent-values-since-v24) - [Versioning](#versioning) - [Parser version handling](#parser-version-handling) - [Deprecation Policy](#deprecation-policy) - [Field lifecycle](#field-lifecycle) - [Parser behavior](#parser-behavior) - [Vocabulary value promotion](#vocabulary-value-promotion) - [Canonical deprecation registry](#canonical-deprecation-registry) - [Top-level Fields](#top-level-fields) - [Core identity](#core-identity) - [v2 additions](#v2-additions) - [v2.1 additions](#v21-additions) - [v2.3 additions](#v23-additions) - [Block kinds _(since v2.3)_](#block-kinds-since-v23) - [Naming rules _(since v2.3)_](#naming-rules-since-v23) - [Datetime Conventions](#datetime-conventions) - [Instants known only by date _(since v2.3)_](#instants-known-only-by-date-since-v23) - [Ordering and ties](#ordering-and-ties) - [Recommended vocabularies](#recommended-vocabularies) - [`well_type` — Recommended values](#well_type--recommended-values) - [`well_purpose` — Recommended values _(since v2.1)_](#well_purpose--recommended-values-since-v21) - [`bore_hole[].drilling_method` — Recommended values _(since v2.3)_](#bore_holedrilling_method--recommended-values-since-v23) - [Construction materials — Recommended values _(since v2.3)_](#construction-materials--recommended-values-since-v23) - [`well_screen[].slot_type` — Recommended values _(since v2.3)_](#well_screenslot_type--recommended-values-since-v23) - [`centralizers[].type` — Recommended values _(since v2.1)_](#centralizerstype--recommended-values-since-v21) - [`cement_pad.type` — Recommended values _(since v2.3)_](#cement_padtype--recommended-values-since-v23) - [`pump_installations[].type` — Recommended values _(since v2.3)_](#pump_installationstype--recommended-values-since-v23) - [`pump_installations[].power_source` — Recommended values _(since v2.3)_](#pump_installationspower_source--recommended-values-since-v23) - [`permits[].type` — Recommended values _(since v2.3)_](#permitstype--recommended-values-since-v23) - [`permits[].water_use` — Recommended values _(since v2.3)_](#permitswater_use--recommended-values-since-v23) - [`permits[].conditions[].category` — Recommended values _(since v2.3)_](#permitsconditionscategory--recommended-values-since-v23) - [`permits[].history[].type` — Recommended values _(since v2.3)_](#permitshistorytype--recommended-values-since-v23) - [`meters[].type` — Recommended values _(since v2.3)_](#meterstype--recommended-values-since-v23) - [`production[]` — Recommended values _(since v2.3)_](#production--recommended-values-since-v23) - [`water_samples[]` — Recommended values _(since v2.3)_](#water_samples--recommended-values-since-v23) - [`hydrodynamic_events[].measurement_method` — Recommended values](#hydrodynamic_eventsmeasurement_method--recommended-values) - [`aquifer_analysis[].method` — Recommended values](#aquifer_analysismethod--recommended-values) - [`history_logs[].category` — Recommended values](#history_logscategory--recommended-values) - [`history_logs[].severity` — Recommended values](#history_logsseverity--recommended-values) - [`history_logs[].maintenance_type` — Recommended values _(since v2.3)_](#history_logsmaintenance_type--recommended-values-since-v23) - [`history_logs[].status` — Allowed values _(since v2.3)_](#history_logsstatus--allowed-values-since-v23) - [`Attachment.document_type` — Recommended values _(since v2.3)_](#attachmentdocument_type--recommended-values-since-v23) - [`well_id`](#well_id) - [Numeric ID conversion](#numeric-id-conversion) - [Uniqueness](#uniqueness) - [`location`](#location) - [`LocationProperties`](#locationproperties) - [`elevation_datum` — Recommended values](#elevation_datum--recommended-values) - [CRS scope](#crs-scope) - [Units](#units) - [Canonical units](#canonical-units) - [Field-to-unit binding (normative)](#field-to-unit-binding-normative) - [Water quality units _(since v2.3)_](#water-quality-units-since-v23) - [Level sign convention](#level-sign-convention) - [Diameter and the as-built convention](#diameter-and-the-as-built-convention) - [Screen slot](#screen-slot) - [Rationale](#rationale) - [Water quality parameter vocabulary _(since v2.3)_](#water-quality-parameter-vocabulary-since-v23) - [`profiles`](#profiles) - [Profile schema requirements](#profile-schema-requirements) - [Example: a regulatory water quality profile _(since v2.3)_](#example-a-regulatory-water-quality-profile-since-v23) - [Profile validation](#profile-validation) - [Extensibility](#extensibility) - [Custom fields — `x-` prefix](#custom-fields--x--prefix) - [Custom top-level blocks — `x-` prefix required](#custom-top-level-blocks--x--prefix-required) - [Custom vocabulary values — `x-` prefix](#custom-vocabulary-values--x--prefix) - [Offline packaging _(since v2.4)_](#offline-packaging-since-v24) - [Cross-reference and Uniqueness Rules](#cross-reference-and-uniqueness-rules) - [Uniqueness scope](#uniqueness-scope) - [Cross-array references](#cross-array-references) - [Dangling references](#dangling-references) - [Repeated records _(since v2.4)_](#repeated-records-since-v24) - [Duplicate IDs within a scope](#duplicate-ids-within-a-scope) - [`id` field convention](#id-field-convention) - [Precision and Uncertainty](#precision-and-uncertainty) - [Supported precision fields](#supported-precision-fields) --- ## Top-level Structure ```json { "version": "2.4", "@context": "https://welldot.org/context/v2.jsonld", "well_id": [ ... ], "location": { ... }, "name": "...", "well_type": "...", "well_purpose": [ ... ], "well_driller": "...", "construction_date": "YYYY-MM-DD", "obs": "...", "profiles": [ ... ], "bore_hole": [ ... ], "well_case": [ ... ], "reduction": [ ... ], "well_screen": [ ... ], "surface_case": [ ... ], "hole_fill": [ ... ], "centralizers": [ ... ], "cement_pad": { ... }, "lithology": [ ... ], "fractures": [ ... ], "caves": [ ... ], "hydrodynamic_events": [ ... ], "aquifer_analysis": [ ... ], "history_logs": [ ... ], "attachments": [ ... ], "pump_installations": [ ... ], "permits": [ ... ], "meters": [ ... ], "production": [ ... ], "operating_regime": [ ... ], "water_samples": [ ... ] } ``` ### Absent values _(since v2.4)_ An optional field without a value is omitted, never written as `null`. A `null` in an optional field is malformed. Absent means unknown. Readers accept `null` in `aquifer_analysis[].storativity` only in files whose `version` is the integer `2`, and treat it as absent. --- ## Versioning | Version | Description | | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `1` | Initial release — constructive and geologic data only | | `2` | Adds location, well_id (array), profiles, texture object, hydrodynamic_events, aquifer_analysis, history_logs, optional @context, \*\_precision fields, and extensibility conventions. Tightens unit semantics: all values are canonical SI, no per-file declaration. | | `"2.4"` | Version written as the string `"M.m"` (_since v2.4_). Read as v2; the integer `2` means v2.0 to v2.3. | Minor revisions (`2.1`, `2.3`, …) are additive. Up to v2.3 they did not change the `version` integer: a v2.3 document declares `"version": 2`. A v2.0 parser reading a v2.1 document sees the v2.1 fields (`well_purpose`, `centralizers`) as unrecognized members without the `x-` prefix. Per § Extensibility it MUST preserve them and MUST NOT reject the file, and it SHOULD emit a warning. The same rule applies to v2.3: a v2.0 or v2.1 parser reading a v2.3 document sees `attachments`, `pump_installations`, `permits`, `meters`, `production`, `operating_regime`, `water_samples`, `hydrodynamic_events[].corrects`, the `history_logs` data links (`hydrodynamic_event_ids`, `sample_ids`) and category-specific fields (`maintenance`, `status_change`) and the nested `attachments` arrays as unrecognized members. It MUST preserve them, MUST NOT reject the file, and SHOULD emit a warning. Such a parser does not know `corrects`, so it may still count a retracted event; this is the expected degradation. _(since v2.4)_ The version is written as the string `"M.m"` (for example `"2.4"`). Versions are compared as two integers, never as text: `"2.10"` is greater than `"2.9"`. Patch levels (`"2.4.1"`) are not allowed. The integer `2` means v2.0 to v2.3 with the revision unknown, and it remains valid for reading. There is no separate `spec_version` field. **Intentional break.** A parser of `@welldot/core` 0.4.0 or earlier rejects `"2.4"` files because it only accepts the integers `1` and `2`. This is the one intentional exception to the minor-revision compatibility rule above. Upgrade readers before writing v2.4 files. ### Parser version handling The `version` field MUST be written by producers. It takes one of three forms: the integer `1`, the integer `2`, or a string `"M.m"` matching `^\d+\.\d+$`. - A `version` that is none of these forms (`2.4` as a JSON number, `"2.4.1"`, `"v2.4"`, `"2"`) is malformed. A parser MUST reject the file with an error that names the value. - A major other than `1` or `2` (`3`, `"3.0"`) is unsupported. A parser MUST reject the file with an error that names the value. - Any minor of major `2` (`"2.4"`, a future `"2.9"`) is read as v2. Unknown members follow § Extensibility. - A v2 parser encountering `version: 1` MUST apply the v1-to-v2 normalizations listed in the changes section and treat the document as v2-equivalent for all subsequent processing. - A v1 parser encountering `version: 2` MUST reject the file with a clear error indicating an unsupported format version. v1 parsers MUST NOT attempt partial reads of v2 documents. - Either parser encountering an unrecognized integer version MUST reject the file with a clear error. - A file without a `version` field SHOULD be rejected. Parsers MAY emit a warning and attempt to read the file as v1 if all required v1 fields are present and no v2-specific blocks are detected, but this fallback behavior is implementation-specific and not guaranteed by the spec. Files with `version: 1` MUST NOT contain any v2-only blocks (`hydrodynamic_events`, `aquifer_analysis`, `history_logs`, `attachments`, `pump_installations`, `permits`, `meters`, `production`, `operating_regime`, `water_samples`, `profiles`, `well_id` as array, `location` object, `@context`). A v1 file containing such fields is malformed. --- ## Deprecation Policy The `.well` format evolves in two version dimensions: - **Major versions** (`1`, `2`, `3`, …): may remove deprecated fields and introduce breaking changes. Each major version has its own canonical JSON Schema URL. - **Minor revisions** within a major version (`2.0`, `2.1`, …): additive only. Minor revisions may introduce new fields, new vocabulary values, and **deprecation markers** for existing fields. Minor revisions MUST NOT remove fields, change field semantics, or break round-trip compatibility for files conforming to earlier minor revisions of the same major version. ### Field lifecycle A field moves through three states: 1. **Active** — defined and recommended by the current minor revision. 2. **Deprecated** — still defined and still valid, but flagged for removal in the next major version. Deprecation is announced in a minor revision (e.g. introduced in v2.0, deprecated in v2.1). 3. **Removed** — no longer defined. May only occur in a new major version (e.g. removed in v3.0). Each deprecation announcement MUST include, in the spec text for the relevant field: - The minor revision in which the field was deprecated (e.g. _"Deprecated in v2.1"_). - The major version in which the field will be removed (e.g. _"Will be removed in v3.0"_). - The replacement field or pattern, when applicable. - A v2-to-vNext normalization rule that parsers for the next major version MUST implement to read files using the deprecated field. ### Parser behavior Parsers conformant to the version that introduced a deprecation: - MUST accept the deprecated field as valid input. - MUST preserve the deprecated field on round-trip unless explicitly migrating the document to a newer version. - SHOULD emit a warning when encountering a deprecated field, naming the replacement. - MAY offer a migration mode that rewrites the field to its replacement in place. Migration mode MUST be opt-in (e.g. an explicit flag passed to the parser), not the default behavior of read operations. Parsers for the major version after removal: - MUST apply the documented normalization when reading older files (e.g. a v3 parser reading a v2 file that used a v2-deprecated field). - MUST NOT accept the removed field name in files declaring the new major version (e.g. a file with `version: 3` containing a v2-deprecated field is malformed). ### Vocabulary value promotion When a vocabulary value previously available only with the `x-` prefix is promoted to canonical (e.g. `x-slug_test` becomes `slug_test`): - The minor revision that performs the promotion MUST document both forms as equivalent. - Parsers from that revision onward MUST treat both forms as equivalent for the remainder of the major version. - The `x-` prefixed form is implicitly deprecated upon promotion and will be removed in the next major version, following the standard field lifecycle. ### Canonical deprecation registry Each minor revision of this spec MUST include a "Deprecations" subsection in its changelog listing all fields and vocabulary values deprecated in that revision, with their replacement and removal timeline. The v2.0 release contains no deprecations. --- ## Top-level Fields ### Core identity | Field | Type | Required | Description | | ------------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | integer \| string | yes | Format version. v2.4 files write `"2.4"`. | | `well_type` | string | yes | Construction method of the well. See vocabulary below. | | `name` | string | no | Well name or local identifier. | | `well_driller` | string | no | Name of the drilling company or individual. | | `construction_date` | string | no | ISO 8601 calendar date of well completion (`YYYY-MM-DD`). Date-only by design — time of day for construction completion is not meaningful at the precision the well registry cares about. Interpreted as the local civil date at the well site. | | `obs` | string | no | Free-text observations about the well. | | `well_depth` | number | no | The well's current usable depth in meters from ground level, as measured or reported. May be less than the deepest constructive element (see `bore_hole`) due to siltation, debris, or partial backfill. | ### v2 additions | Field | Type | Required | Description | | ---------- | ------------------------- | -------- | ---------------------------------------------------------------------------- | | `@context` | string \| array \| object | no | JSON-LD context. Reserved field; see § JSON-LD Context. | | `well_id` | `WellId[]` | no | Array of authority-scoped identifiers. See § well_id. | | `location` | `Location` | no | Geographic position and elevation. See § location. | | `profiles` | string[] | no | Array of JSON Schema URLs declaring conformance to profiles. See § profiles. | ### v2.1 additions | Field | Type | Required | Description | | -------------- | --------------- | -------- | ---------------------------------------------------------------- | | `well_purpose` | string[] | no | Intended use(s) of the well. See § well_purpose. | | `centralizers` | `Centralizer[]` | no | Casing/screen centralizers. See object-schemas.md § Centralizer. | ### v2.3 additions | Field | Type | Required | Description | | -------------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `attachments` | `Attachment[]` | no | Documents about the well as a whole, not tied to any record. See object-schemas.md § Attachment. | | `pump_installations` | `PumpInstallation[]` | no | Pump installation history (installation block). See object-schemas.md § `pump_installations[]`. | | `permits` | `Permit[]` | no | Legal instruments governing abstraction (mutable record block). See object-schemas.md § `permits[]`. | | `meters` | `Meter[]` | no | Totalizer (hidrômetro) installation history (installation block). See object-schemas.md § `meters[]`. | | `production` | `ProductionEntry[]` | no | Ledger of meter readings and declared volumes (ledger block). See object-schemas.md § `production[]`. | | `operating_regime` | `OperatingRegime[]` | no | Declared operating regimes, each in force from `effective_from` (mutable record block). See object-schemas.md § `operating_regime[]`. | | `water_samples` | `WaterSample[]` | no | Water samples and their field and laboratory results (ledger block). See water-quality.md. | ### Block kinds _(since v2.3)_ Every top-level array is one of three kinds. The kind decides how records are corrected. | Kind | Blocks | Correction | Edit tracking | | -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | Ledger | `hydrodynamic_events`, `production`, `water_samples` | MAY be edited in place (NOT RECOMMENDED after export). `corrects` is the recommended mechanism for measurement corrections. | `updated_at` where the block defines it; a `corrects` entry is the history of a correction. | | Mutable record | `history_logs`, `permits`, `operating_regime` | Edit in place. | `updated_at` (instant) on the record. | | Installation | `pump_installations`, `meters` | Edit in place. | `updated_at` (instant) on the record. | Constructive and geologic arrays describe the well as built and are edited in place. `aquifer_analysis` entries are interpretations: a new interpretation is a new entry, and earlier ones coexist. **Ledger corrections.** A ledger entry referenced by any `corrects` field is excluded from every derivation but stays in the file. Chains are allowed (C corrects B, which corrected A); only the last uncorrected entry counts. A `corrects` cycle is malformed and emits a warning. An `aquifer_analysis` whose `source_event_ids` or `static_level_source_id` points to a retracted event emits a warning naming both ids. **Installation pattern.** Installation blocks describe equipment present in the well for a period, with `installed_at` (required) and `removed_at` (absent means currently installed). - `removed_at` earlier than or equal to `installed_at` is malformed and emits a warning. - Each entry is one installation, not one piece of equipment. A unit pulled and reinstalled gets a new entry with the same `serial`. - Overlapping open installations in the same block emit a warning, not an error. Standby pumps and secondary meters exist. ### Naming rules _(since v2.3)_ - No unit in a field name. Units are bound by the field-to-unit table (§ Units). - `flow_rate` is the canonical name for a volumetric rate; nameplate values use the `rated_` prefix (`rated_flow_rate`, `rated_head`, `rated_power`). - `from` / `to` are reserved for depth intervals in meters. Time intervals use `period_start` / `period_end`. - References to another array end in `_id` (single) or `_ids` (array), plus the two relationship fields `corrects` and `supersedes`. **`supersedes` vs `corrects`.** `supersedes` expresses legal succession between two valid permits and appears only on `permits`. It is not a correction mechanism: `corrects` retracts ledger entries. --- ## Datetime Conventions The `.well` format distinguishes two kinds of temporal values: **Calendar dates** — `YYYY-MM-DD`, no time, no offset. Used for `construction_date` and, since v2.3, for legal documents: `permits[].issued_at`, `valid_from`, `valid_until`, `renewal_requested_at`; `permits[].conditions[].first_due`, `last_due`; `permits[].conditions[].fulfillments[].due_date`; and `permits[].history[].date`, `due_date`. Interpreted as the local civil date at the well site. Calendar dates are sortable lexicographically and applications MUST NOT attempt to derive an instant from them. **Instants** — RFC 3339 datetime strings with a mandatory UTC offset (e.g. `2006-03-14T08:00:00-03:00` or `2006-03-14T11:00:00Z`). Used for all event, analysis, and log timestamps: - `hydrodynamic_events[].datetime` - `aquifer_analysis[].datetime` - `history_logs[].datetime` - `history_logs[].updated_at` - `pump_installations[].installed_at`, `removed_at`, `updated_at` _(since v2.3)_ - `permits[].updated_at`, `permits[].history[].updated_at`, `permits[].conditions[].fulfillments[].datetime`, `updated_at` _(since v2.3)_ - `meters[].installed_at`, `removed_at`, `updated_at` _(since v2.3)_ - `production[].datetime` (`meter_reading`); `production[].period_start`, `period_end` (`estimated_volume`) _(since v2.3)_ - `operating_regime[].effective_from`, `updated_at` _(since v2.3)_ - `water_samples[].datetime`, `laboratory.received_at`, `results[].analyzed_at`, `results[].validation.validated_at` _(since v2.3)_ Validity and deadline checks compare calendar dates, never UTC instants. When an instant is compared with a calendar date — for example a condition fulfillment's `datetime` against its `due_date` — the instant's local date is the instant converted to `location.properties.timezone` _(since v2.4)_. Without `timezone`, it is the date part of the string, in the offset it carries; producers SHOULD then write such instants in the well site's local offset. A file without `timezone` whose permits have condition fulfillments emits a warning. Relative deadlines and recurrences are ISO 8601 date durations (`P90D`, `P6M`, `P1Y6M`); see object-schemas.md § Date duration. ### Instants known only by date _(since v2.3)_ Laboratory reports often give only the date of receipt or analysis. Such an instant is written at 00:00 local time at the well site, and a sibling field named after it with the `_resolution` suffix is set to `"day"`: ```json { "analyzed_at": "2026-09-17T00:00:00-03:00", "analyzed_at_resolution": "day" } ``` - Consumers MUST ignore the time of day when `_resolution` is `day`. Ordering, holding-time and deadline calculations on that field use day resolution. - `day` is the only allowed value; any other value is malformed. An absent `_resolution` means the instant is exact. - In v2.3 the convention applies to `water_samples[].laboratory.received_at` and `water_samples[].results[].analyzed_at`. It is the candidate general convention for any instant known only by its date. A datetime without an offset (naked `YYYY-MM-DDTHH:MM:SS`) is malformed and parsers MUST reject it. Time zone abbreviations (`EST`, `BRT`) are not RFC 3339 and MUST NOT be used. ### Ordering and ties Instants are ordered by absolute UTC instant, not by the lexical form of the string. The strings `2006-03-14T08:00:00-03:00` and `2006-03-14T11:00:00Z` represent the same instant and tie for ordering purposes. When two events share the same instant, the optional `sequence` integer breaks the tie (lower values sort first). When two events share the same instant and no `sequence` is provided, ordering is implementation-defined; producers SHOULD provide `sequence` when authoring events with identical instants. --- ## Recommended vocabularies Many string fields are **open**: they accept any value, but recommend a vocabulary for interoperability. The sections below list each one. Non-canonical values SHOULD use the `x-` prefix, and tools MUST preserve unrecognized values and display them as-is. Every recommended vocabulary is also published as data by `@welldot/core` (e.g. `PERMIT_TYPES`, `CONSTRUCTION_MATERIALS`, `SAMPLE_TYPES`), each entry carrying its `value` and display labels keyed by BCP 47 language tag (at least `en` and `pt`). The field types use the `OpenVocab` pattern (`T | (string & {})`): the recommended values are typed while any other string stays valid. The Portuguese column in the tables below may be more descriptive than the short display label. ## `well_type` — Recommended values The `well_type` field accepts any string and describes the well's **construction method**. The values below are the recommended vocabulary for interoperability. Tools should treat unrecognized values gracefully. | Value | Portuguese (BR) | Description | | ---------------------- | ----------------------- | ---------------------------------------------- | | `tubular` | Tubular | Conventional rotary or percussion drilled well | | `hand_dug` | Cacimba / poço escavado | Manually excavated well or cistern | | `horizontal` | Horizontal | Horizontal or sub-horizontal well | | `infiltration_gallery` | Galeria de infiltração | Horizontal subsurface infiltration gallery | | `artesian` | Artesiano | **Deprecated in v2.1** — see below | Non-canonical values SHOULD use the `x-` prefix (e.g. `x-radial_collector`). **`artesian` (deprecated in v2.1, removal in v3).** Artesianism is a hydraulic condition, not a construction method: an artesian well is usually a `tubular` well. A flowing artesian condition is recorded as a `hydrodynamic_events` entry whose `static_level` is negative (see § Level sign convention), and applications derive the label from it. Parsers MUST continue to accept `well_type: "artesian"` and MUST NOT rewrite it. Authoring tools SHOULD offer to migrate it to the actual construction method on edit. --- ## `well_purpose` — Recommended values _(since v2.1)_ `well_purpose` is an optional array of strings giving the well's current intended use(s). A well may have more than one purpose (e.g. `["production", "monitoring"]`). Changes of use over time belong in `history_logs` (category `event`). Non-canonical values SHOULD use the `x-` prefix. | Value | Portuguese (BR) | Description | | ----------------------- | ------------------------------- | ---------------------------------------------------------------- | | `production` | Produção / captação | Water supply abstraction | | `monitoring` | Poço de monitoramento | Groundwater level and/or quality monitoring | | `piezometer` | Piezômetro | Measures hydraulic head at a discrete depth interval | | `water_level_indicator` | INA — indicador de nível d'água | Open standpipe for water table readings (typically geotechnical) | | `observation` | Poço de observação | Observation well for aquifer tests on a nearby pumping well | | `exploration` | Pesquisa / exploratório | Investigation or test well | | `injection` | Injeção / recarga | Managed aquifer recharge or injection | | `dewatering` | Rebaixamento | Construction or mining dewatering | --- ## `bore_hole[].drilling_method` — Recommended values _(since v2.3)_ | Value | English | Portuguese (BR) | | ------------ | ---------- | ------------------ | | `rotary` | Rotary | Rotativo | | `percussion` | Percussion | Percussão | | `cable_tool` | Cable tool | Percussão a cabo | | `auger` | Auger | Trado | | `air_hammer` | Air hammer | Martelo pneumático | Non-canonical values SHOULD use the `x-` prefix. ## Construction materials — Recommended values _(since v2.3)_ Shared by `well_case[].type`, `well_screen[].type`, `reduction[].type` and `pump_installations[].riser_material`. | Value | English | Portuguese (BR) | | ------------------- | ----------------- | --------------- | | `pvc` | PVC | PVC | | `geomechanical_pvc` | Geomechanical PVC | PVC geomecânico | | `carbon_steel` | Carbon steel | Aço carbono | | `galvanized_steel` | Galvanized steel | Aço galvanizado | | `stainless_steel` | Stainless steel | Aço inox | | `fiberglass` | Fiberglass | Fibra de vidro | Non-canonical values SHOULD use the `x-` prefix. ## `well_screen[].slot_type` — Recommended values _(since v2.3)_ The screen design, independent of its material (`well_screen[].type`). | Value | English | Portuguese (BR) | | ----------------- | --------------- | ------------------------- | | `continuous_slot` | Continuous slot | Espiralado (tipo Johnson) | | `slotted` | Slotted | Ranhurado | | `bridge_slot` | Bridge slot | Tipo ponte | | `louver` | Louver | Veneziana | | `perforated` | Perforated | Perfurado | | `nold` | Nold type | Tipo Nold | `continuous_slot` is the wire-wrapped (wire-wound) screen. `nold` is the steel screen sold under that trade designation (_filtro tipo Nold_). Non-canonical values SHOULD use the `x-` prefix. ## `centralizers[].type` — Recommended values _(since v2.1)_ | Value | English | Portuguese (BR) | | ------------------ | ---------------- | ----------------- | | `spring_bow` | Spring bow | Mola (spring bow) | | `rigid` | Rigid | Rígido | | `semi_rigid` | Semi-rigid | Semirrígido | | `polymer` | Polymer | Polimérico | | `carbon_steel` | Carbon steel | Aço carbono | | `galvanized_steel` | Galvanized steel | Aço galvanizado | | `stainless_steel` | Stainless steel | Aço inox | The steel values name rigid centralizers by their material. Non-canonical values SHOULD use the `x-` prefix. ## `cement_pad.type` — Recommended values _(since v2.3)_ | Value | English | Portuguese (BR) | | --------------------- | ------------------- | --------------- | | `concrete` | Concrete | Concreto | | `reinforced_concrete` | Reinforced concrete | Concreto armado | | `cement` | Cement | Cimento | | `mortar` | Mortar | Argamassa | `type` describes the pad material; other free text (e.g. a shape) remains valid. --- ## `pump_installations[].type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Description | | -------------------- | ---------------------- | ------------------------------------------------- | | `submersible` | Bomba submersa | Electric submersible pump. | | `vertical_turbine` | Bomba de eixo vertical | Surface motor with line shaft. | | `jet` | Bomba injetora | Surface jet pump. | | `progressive_cavity` | Bomba helicoidal | Progressive cavity pump, common with solar power. | | `hand_pump` | Bomba manual | | | `compressor_airlift` | Compressor (air-lift) | Permanent airlift production. | Solar is a `power_source`, never a `type`. Non-canonical values SHOULD use the `x-` prefix. ## `pump_installations[].power_source` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | -------- | -------------------- | | `grid` | Rede elétrica | | `solar` | Solar (fotovoltaico) | | `diesel` | Gerador a diesel | | `hybrid` | Híbrido | ## `permits[].type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Description | | -------------------- | -------------------------------------- | ----------------------------------- | | `abstraction_permit` | Outorga de direito de uso | Grant to abstract groundwater. | | `preliminary_permit` | Outorga prévia / declaração de reserva | Permission before drilling. | | `exemption` | Dispensa / uso insignificante | Formal exemption for small users. | | `registration` | Cadastro (e.g. CNARH) | Registration without a grant. | | `dewatering_permit` | Outorga para rebaixamento | Mining and construction dewatering. | | `drilling_permit` | Autorização de perfuração | Authorization to drill the well. | `renewal` is not a type: a renewal is a new permit of the same type whose `supersedes` points to the previous one. Non-canonical values SHOULD use the `x-` prefix. ## `permits[].water_use` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | -------------- | -------------------------------------- | | `human_supply` | Abastecimento público / consumo humano | | `industrial` | Industrial | | `mining` | Mineração | | `irrigation` | Irrigação | | `livestock` | Dessedentação animal | | `commercial` | Comercial / serviços | `water_use` describes what the abstracted water is for; `well_purpose` describes the well's role. ## `permits[].conditions[].category` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Covers | | ------------------------ | -------------------------- | -------------------------------------------------------------------------- | | `monitoring` | Monitoramento | Water level, flow, abstracted volume and water quality monitoring/sampling | | `reporting` | Relatórios e declarações | Periodic reports and volume/flow declarations submitted to the authority | | `equipment_installation` | Instalação de equipamentos | Meters (hidrômetro), level gauges, sampling taps, telemetry | | `well_protection` | Proteção do poço | Sanitary slab, seal, protection perimeter, wellhead closure | | `environmental` | Ambiental | Environmental measures, compensation, licensing of the activity | | `legal` | Legal / administrativo | Renewal request, fees, registrations, document submissions | Non-canonical values SHOULD use the `x-` prefix. ## `permits[].history[].type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | -------------- | -------------------------------- | | `filing` | Protocolo / requerimento | | `process` | Andamento do processo / despacho | | `notification` | Notificação / ofício | | `fee` | Taxa / emolumento | | `inspection` | Vistoria | | `decision` | Decisão / publicação | | `renewal` | Renovação | Non-canonical values SHOULD use the `x-` prefix. The administrative `status` of a permit is a closed vocabulary; see object-schemas.md § `permits[]` — Administrative status. ## `meters[].type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | ----------------- | ----------------------- | | `mechanical` | Hidrômetro mecânico | | `electromagnetic` | Medidor eletromagnético | | `ultrasonic` | Medidor ultrassônico | Non-canonical values SHOULD use the `x-` prefix. ## `production[]` — Recommended values _(since v2.3)_ **`type`** — the entry discriminator: `meter_reading` (a totalizer register value) or `estimated_volume` (a volume estimated for a period, without a meter). Non-canonical entry types SHOULD use the `x-` prefix; parsers preserve them and exclude them from volume derivations. **`source`** (`meter_reading`): | Value | Portuguese (BR) | | ----------- | --------------- | | `manual` | Leitura manual | | `telemetry` | Telemetria | **`method`** (`estimated_volume`) — the basis of the estimate. Descriptive only: it never changes how the volume is counted. | Value | Portuguese (BR) | Description | | ---------------- | ------------------------- | ------------------------------------------------------------------ | | `flow_time` | Vazão × tempo de operação | Flow rate times operating time (hour meter, operating regime). | | `energy` | Consumo de energia | Derived from the pump's energy consumption. | | `user_statement` | Informado pelo usuário | Stated by the owner or operator, without a documented calculation. | ## `water_samples[]` — Recommended values _(since v2.3)_ Recommended values for `sample_type`, `sampling_method`, `sampling_point.type` and `sampling_point.device` are listed in [water-quality.md](water-quality.md) § Recommended values. ## `hydrodynamic_events[].measurement_method` — Recommended values Used by `spot_measurement` and `logger_series` _(since v2.4)_ events. | Value | English | Portuguese (BR) | | --------------------- | ------------------- | --------------------- | | `electric_probe` | Electric probe | Sonda elétrica | | `pressure_transducer` | Pressure transducer | Transdutor de pressão | | `air_line` | Air line | Linha de ar | | `tape` | Tape | Trena | Non-canonical values SHOULD use the `x-` prefix. ## `aquifer_analysis[].method` — Recommended values | Value | English | Portuguese (BR) | | ------------------- | ----------------- | --------------- | | `cooper_jacob` | Cooper-Jacob | Cooper-Jacob | | `theis` | Theis | Theis | | `neuman` | Neuman | Neuman | | `hantush` | Hantush | Hantush | | `birsoy_summers` | Birsoy-Summers | Birsoy-Summers | | `eden_hazel` | Eden-Hazel | Eden-Hazel | | `visual_inspection` | Visual inspection | Inspeção visual | Non-canonical values SHOULD use the `x-` prefix. ## `history_logs[].category` — Recommended values | Value | Portuguese (BR) | Category-specific fields | | --------------- | ------------------- | --------------------------------------------------------------------- | | `maintenance` | Manutenção | `maintenance_type`, `pump_installation_id`, `meter_id` _(since v2.3)_ | | `inspection` | Inspeção | — | | `incident` | Incidente | — | | `event` | Evento | — | | `status_change` | Mudança de situação | `status` _(since v2.3)_ | Descriptions are in object-schemas.md § `history_logs[]`. Category-specific fields MUST be absent on entries of other categories; their presence emits a warning. Non-canonical values SHOULD use the `x-` prefix. ## `history_logs[].severity` — Recommended values | Value | English | Portuguese (BR) | | ---------- | -------- | --------------- | | `low` | Low | Baixa | | `medium` | Medium | Média | | `high` | High | Alta | | `critical` | Critical | Crítica | Non-canonical values SHOULD use the `x-` prefix. ## `history_logs[].maintenance_type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | ------------------- | --------------------------- | | `inspection` | Inspeção | | `cleaning` | Limpeza | | `redevelopment` | Redesenvolvimento | | `disinfection` | Desinfecção | | `pump_service` | Manutenção da bomba | | `meter_calibration` | Aferição do hidrômetro | | `video_inspection` | Filmagem / perfilagem ótica | | `level_measurement` | Medição de nível | | `pump_test` | Teste de bombeamento | | `water_sampling` | Coleta de água | Non-canonical values SHOULD use the `x-` prefix. ## `history_logs[].status` — Allowed values _(since v2.3)_ Used by entries of category `status_change`. This is a **closed vocabulary**: `status` MUST be one of the five values below. Any other value, including an `x-` value, is a schema error and the file is rejected. | Value | Portuguese (BR) | Semantics | | ---------------- | ---------------------- | --------------------------------------- | | `active` | Ativo / em operação | Normal operation. | | `maintenance` | Em manutenção | Temporarily out of service for work. | | `inactive` | Paralisado | Out of service, recoverable. | | `decommissioned` | Desativado / tamponado | Permanently closed and properly sealed. | | `abandoned` | Abandonado | Left without proper sealing. | The well's current status is the `status` of the `status_change` entry with the latest `datetime`; with no such entry, the status is unknown. ## `Attachment.document_type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Typical location | | -------------------- | ------------------------------------------- | ------------------------------------- | | `drilling_report` | Relatório de perfuração / relatório técnico | Root | | `as_built_drawing` | Perfil construtivo as-built | Root | | `registry_record` | Ficha cadastral (SIAGAS, CNARH) | Root | | `photo` | Fotografia | Any | | `permit_document` | Portaria / certificado de outorga | `permits` | | `condition_evidence` | Comprovante de cumprimento de condicionante | `permits[].conditions[].fulfillments` | | `pump_curve` | Curva da bomba | `pump_installations` | | `field_sheet` | Planilha de campo | `hydrodynamic_events` | | `test_report` | Relatório de teste de bombeamento | `aquifer_analysis` | | `lab_report` | Laudo laboratorial | `water_samples` | | `invoice` | Nota fiscal | Any | The vocabulary is open; non-canonical values SHOULD use the `x-` prefix. --- ## `well_id` Array of authority-scoped identifiers linking the well to external registries. A well may be registered in multiple national, regional, or institutional registries simultaneously. | Field | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------------------------------------------- | | `authority` | string | yes | Name of the issuing registry or institution (e.g. `"SIAGAS"`, `"ANA"`, `"NGWD"`). | | `id` | string | yes | The identifier as issued by the authority. | | `primary` | boolean | no | Marks the canonical identifier. At most one entry may have `primary: true`. | Producers SHOULD mark exactly one entry `primary: true`. The rule "the first entry is canonical" applies only as a fallback when no entry is marked, because it is fragile when the order changes. Parsers MUST warn when multiple entries have `primary: true`. ### Numeric ID conversion All `id` values are strings. When the source identifier from an external system is an integer, the string representation MUST be the canonical base-10 representation with no leading zeros, no thousands separator, and no locale-specific formatting. The integer `42` becomes `"42"`, not `"042"` or `"42,"`. When the source identifier is a fixed-width string with significant leading zeros (e.g. `"00042819"` from a registry where width carries meaning), the original string MUST be preserved verbatim. Composite identifiers containing significant leading zeros in their numeric portion (e.g. SIAGAS `"SP-0042819"`) MUST be preserved as-is. ### Uniqueness The combination `(authority, id)` MUST be unique within the `well_id` array. See § Cross-reference and uniqueness rules for the duplicate-handling policy. ```json "well_id": [ { "authority": "SIAGAS", "id": "SP-0042819", "primary": true }, { "authority": "ANA", "id": "02000.001234/2006-78" }, { "authority": "utility-internal", "id": "PP-01" } ] ``` --- ## `location` Geographic position and elevation of the wellhead. Supersedes the v1 top-level `lat`, `lng`, and `elevation` fields. The three primary measurements — `lat`, `lng`, `elevation` — are top-level fields on the location object for ergonomics. All ancillary metadata (datum, CRS, precision, source CRS) lives under a nested `properties` object. | Field | Type | Required | Description | | ------------ | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | | `lat` | number | yes | Latitude in decimal degrees. | | `lng` | number | yes | Longitude in decimal degrees. | | `elevation` | number | no | Wellhead elevation in meters above the WGS84 reference ellipsoid (or the datum named in `properties.elevation_datum`). | | `properties` | `LocationProperties` | no | Ancillary metadata. When absent, all defaults apply. | ### `LocationProperties` | Field | Type | Required | Description | | --------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `elevation_datum` | string | no | Reference datum for `elevation`. See vocabulary below. Default: `wgs84_ellipsoid`. | | `crs` | string | no | EPSG code of the coordinate reference system. Must be a geographic (not projected) CRS. Default: `"EPSG:4326"` (WGS84). | | `lat_precision` | number | no | One-sigma precision of `lat` in decimal degrees. | | `lng_precision` | number | no | One-sigma precision of `lng` in decimal degrees. | | `elevation_precision` | number | no | One-sigma precision of `elevation` in meters. | | `original_crs` | string | no | EPSG code of the source CRS, preserved for traceability when source data was in a projected CRS that the converter transformed to geographic. Informational only. | | `timezone` | string | no | IANA time zone of the well site (e.g. `America/Belem`). A value that is not a valid IANA name is malformed. _(since v2.4)_ | ### `elevation_datum` — Recommended values | Value | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `wgs84_ellipsoid` | WGS84 reference ellipsoidal height. Default. | | `egm2008` | Earth Gravitational Model 2008 geoid | | `egm96` | Earth Gravitational Model 1996 geoid | | `local_msl` | Local mean sea level (imprecise; specific datum SHOULD be named via `x-` prefix, e.g. `x-imbituba` for Brazil's national vertical reference) | When `elevation_datum` is omitted, parsers MUST assume `wgs84_ellipsoid`. ### CRS scope Only geographic coordinate reference systems are permitted in `properties.crs`. Projected CRS values (e.g. UTM zones) are not valid. If source data uses a projected CRS, the converting application MUST transform to geographic coordinates before writing the file. The `original_crs` field MAY be populated with the source CRS for traceability. ```json "location": { "lat": -1.4558, "lng": -48.5039, "elevation": 12.5, "properties": { "elevation_datum": "wgs84_ellipsoid", "crs": "EPSG:4326", "lat_precision": 0.0001, "lng_precision": 0.0001, "elevation_precision": 0.5, "original_crs": "EPSG:31983" } } ``` --- ## Units All numeric values in a `.well` file are in **SI**. The format does not encode unit declarations; the units below are the single, canonical interpretation for every field of the corresponding kind. No file may override these. ### Canonical units | Quantity | Unit | UCUM | Stored as | | ------------------------ | -------------------------------------------------- | ---------------------------------------------- | ---------------------------------------- | | Depth, length, elevation | meter | `m` | number | | Diameter | millimeter | `mm` | number | | Volumetric flow rate | cubic meter per hour | `m3/h` | number | | Elapsed time, duration | minute | `min` | number | | Daily operating time | hour | `h` | number (above 24 read as 24) | | Volume | cubic meter | `m3` | number | | Power | kilowatt | `kW` | number | | Voltage | volt | `V` | number | | Cross-sectional area | square millimeter | `mm2` | number | | Mass concentration | milligram per liter | `mg/L` | number _(since v2.3)_ | | Electrical conductivity | microsiemens per centimeter at 25 °C | `uS/cm` | number _(since v2.3)_ | | Temperature | degree Celsius | `Cel` | number _(since v2.3)_ | | Redox potential | millivolt | `mV` | number _(since v2.3)_ | | Turbidity | NTU, FNU, FAU, FTU (by parameter code) | — | number _(since v2.3)_ | | Color | Hazen unit (uH = mg Pt-Co/L) | — | number _(since v2.3)_ | | Microbiological count | MPN/100 mL, CFU/100 mL, CFU/mL (by parameter code) | `{MPN}/(100.mL)`, `{CFU}/(100.mL)`, `{CFU}/mL` | number _(since v2.3)_ | | Activity concentration | becquerel per liter | `Bq/L` | number _(since v2.3)_ | | Filter pore size | micrometer | `um` | number _(since v2.3)_ | | Transmissivity | square meter per second | `m2/s` | number | | Hydraulic conductivity | meter per second | `m/s` | number | | Pressure | kilopascal | `kPa` | number | | Angle (azimuth, dip) | degree | `deg` | number (0–360 for azimuth, 0–90 for dip) | | Geographic coordinates | decimal degree (WGS84) | `deg` | number | | Calendar date | ISO 8601 | — | string `YYYY-MM-DD` | | Instant | RFC 3339 with UTC offset | — | string | UCUM is the **Unified Code for Units of Measure**, the international machine-parseable unit standard used by OGC SWE and the scientific community. The UCUM column above documents the canonical encoding of each unit for use in JSON-LD contexts, profile schemas, and downstream interoperability — it is not encoded in `.well` files themselves. ### Field-to-unit binding (normative) **Meter (`m`)** — all depths, lengths, and elevations: - `well_depth` - `bore_hole[].from`, `bore_hole[].to` - `well_case[].from`, `well_case[].to` - `reduction[].from`, `reduction[].to` - `well_screen[].from`, `well_screen[].to` - `surface_case[].from`, `surface_case[].to` - `hole_fill[].from`, `hole_fill[].to` - `centralizers[].from`, `centralizers[].to`, `centralizers[].spacing` - `cement_pad.width`, `cement_pad.length`, `cement_pad.thickness` - `lithology[].from`, `lithology[].to` - `fractures[].depth`, `fractures[].depth_precision` - `caves[].from`, `caves[].to` - `hydrodynamic_events[].static_level`, `static_level_precision` - `PumpingStep.readings[].depth`, `depth_precision` - `RecoveryPhase.readings[].depth`, `depth_precision` - `logger_series.sensor_depth`, `readings[].depth`, `depth_precision`, `depth_min`, `depth_max` _(since v2.4)_ - `aquifer_analysis[].static_level`, `static_level_precision` - `aquifer_analysis[].dynamic_level`, `dynamic_level_precision` - `aquifer_analysis[].aquifer_thickness` - `location.elevation`, `location.properties.elevation_precision` - `pump_installations[].intake_depth`, `rated_head`, `electrical.cable_length` _(since v2.3)_ - `water_samples[].sampling_point.depth`, `depth_precision`, `from`, `to` _(since v2.3)_ **Millimeter (`mm`)** — all diameters and slot openings: - `bore_hole[].diameter` - `well_case[].diameter` - `reduction[].diam_from`, `reduction[].diam_to` - `well_screen[].diameter`, `well_screen[].screen_slot` - `surface_case[].diameter` - `hole_fill[].diameter` - `centralizers[].diameter` - `pump_installations[].riser_diameter` _(since v2.3)_ - `meters[].nominal_diameter` _(since v2.3)_ **Cubic meter per hour (`m3/h`)** — all volumetric flow rates: - `PumpingStep.rate`, `rate_precision` - `aquifer_analysis[].flow_rate`, `flow_rate_precision` - `recovery_only.pumping_rate` - `pump_installations[].rated_flow_rate` _(since v2.3)_ - `permits[].flow_rate`, `permits[].monthly_schedule[].flow_rate` _(since v2.3)_ - `operating_regime[].flow_rate` _(since v2.3)_ - `water_samples[].purge.flow_rate` _(since v2.3)_ **Minute (`min`)** — all elapsed times and durations: - `PumpingStep.duration` - `LevelReading.elapsed` - `recovery_only.pumping_duration` - `logger_series.interval` _(since v2.4)_ - `water_samples[].purge.duration`, `purge.readings[].elapsed` _(since v2.3)_ **Cubic meter (`m3`)** — volume _(since v2.3)_: - `permits[].volume_limits[].volume` - `meters[].max_reading` - `production[].reading` (`meter_reading`), `production[].volume` (`estimated_volume`) - `water_samples[].purge.volume` **Hour (`h`)** — daily operating time _(since v2.3)_: - `permits[].daily_operating_time`, `permits[].monthly_schedule[].daily_operating_time` - `operating_regime[].daily_operating_time` The minute remains the canonical unit for elapsed time and durations. The hour is bound only to `daily_operating_time`, which regulators always express in hours. **Square meter per second (`m2/s`)** — transmissivity: - `aquifer_analysis[].transmissivity` **Meter per second (`m/s`)** — hydraulic conductivity: - `aquifer_analysis[].hydraulic_conductivity` **Kilopascal (`kPa`)** — pressure: - `LevelReading.pressure` **Kilowatt (`kW`)** — power _(since v2.3)_: - `pump_installations[].rated_power` **Volt (`V`)** — voltage _(since v2.3)_: - `pump_installations[].electrical.voltage` **Square millimeter (`mm2`)** — cross-sectional area _(since v2.3)_: - `pump_installations[].electrical.cable_section` **Degree Celsius (`Cel`)** — temperature _(since v2.3)_: - `water_samples[].laboratory.received_temperature` **Micrometer (`um`)** — filter pore size _(since v2.3)_: - `water_samples[].results[].filtration.pore_size` **Unit by parameter code** _(since v2.3)_ — `water_samples[].results[].value`, `detection_limit`, `quantification_limit`, `value_precision` and `purge.readings[].value` are in the canonical unit of the result's parameter (see water-quality.md § Units). **Degree (`deg`)** — angles and geographic coordinates: - `fractures[].azimuth`, `fractures[].dip` - `location.lat`, `location.lng`, `location.properties.lat_precision`, `location.properties.lng_precision` **Unitless** — dimensionless or domain-specific values: - `aquifer_analysis[].storativity` (dimensionless) - `aquifer_analysis[].jacob_b`, `jacob_c` (composite units; treated as the units required to make the standard Jacob equation balance with stored `flow_rate` and `length`) - `aquifer_analysis[].well_efficiency_pct` (percentage, 0–100) - `aquifer_analysis[].specific_capacity` (derived ratio in m³/h per m, i.e. `m2/h`) - Lithology `color` (CSS hex string) - `pump_installations[].stages`, `electrical.phases` (counts) - `permits[].monthly_schedule[].month`, `days`; `permits[].conditions[].occurrences` (counts) - `operating_regime[].days_per_week` (count, 1–7) ### Water quality units _(since v2.3)_ Water quality results take their unit from the parameter code; only `x-` vocabulary codes declare `unit` (UCUM). The unit-by-code rule and regional unit normalization are specified in [water-quality.md](water-quality.md) § Units. ### Level sign convention Depths are measured from ground level (0) and increase downward. Water levels above ground level are therefore **negative**. This applies to `static_level`, `LevelReading.depth`, and `aquifer_analysis[].static_level` / `dynamic_level`. A negative static level is how a flowing artesian condition is recorded (see § `well_type`). This convention was implicit in v2.0 and is made explicit in v2.1 without changing semantics. It does not apply to `pump_installations[].intake_depth`, which is an equipment depth and is always positive. ### Diameter and the as-built convention Diameters in casing, screens, surface case, and hole fill are stored as the **as-built outer diameter in millimeters**, not as nominal manufacturer designations. A casing sold as "6 inch" with a real outer diameter of 168.3 mm is recorded as `168.3`, not `152.4` (the literal conversion of 6 inches to mm). Nominal-to-actual mapping for common pipe and screen sizes is a market convention handled by applications — typically by the converter or input form — not by the file format. ### Screen slot `well_screen[].screen_slot` is in millimeters like every other diameter field. US-market slot-number designations (e.g. "20 slot" = 0.020 inch = 0.508 mm) are an input convenience handled by applications, not encoded in the file. The v1 field name `screen_slot_mm` was a legacy artifact and is renamed to `screen_slot` in v2; the value is in millimeters before and after. ### Rationale An interchange format with configurable units multiplies the number of valid encodings of the same well, complicates equality and comparison, and introduces a class of bugs where the declared units do not match the stored values. The `.well` format is designed to be a canonical machine representation: one well, one encoding. Display units, input units, and locale-specific notation are concerns of the applications that read and write `.well` files, not of the format itself. This separation matches the design of GeoJSON (WGS84 only), RFC 3339 (UTC offsets always present), and most modern interchange formats. Producers receiving non-SI source data (imperial drilling reports, US gpm flow measurements, inch casing designations, slot-number screens) MUST convert to SI before writing. Consumers presenting the data to users in non-SI units MUST convert on read. Conversion utilities for the common transformations are provided in `@welldot/core` under the `units` module — m ↔ ft, mm ↔ in, m³/h ↔ L/s, m³/h ↔ US gpm, m²/s ↔ m²/d, DD ↔ DMS, slot-number ↔ mm, cv/hp ↔ kW, L / ft³ / US gal ↔ m³, µg/L ↔ mg/L, mS/m ↔ µS/cm, and similar. --- ## Water quality parameter vocabulary _(since v2.3)_ The `welldot` parameter vocabulary (98 codes), its rules, the CAS equivalences and the treatment of limit sets are specified in [water-quality.md](water-quality.md) § Parameter vocabulary. --- ## `profiles` An optional array of HTTPS URLs declaring formal extensions to the spec that the file conforms to. Each URL MUST resolve to a JSON Schema document. ```json "profiles": [ "https://welldot.org/profiles/brazil-ana/v1/schema.json", "https://inspire.ec.europa.eu/schemas/gwhyd/4.0/profile.json" ] ``` ### Profile schema requirements - Each URL MUST be an HTTPS URL resolving to a JSON Schema document (`Content-Type: application/schema+json` or `application/json`). - The schema SHOULD extend the canonical `.well` v2 schema via `allOf` + `$ref`, but this is not required. - The schema MAY link to human documentation via its `description` field or a custom `x-documentation` field pointing to a Markdown or PDF URL. - Profiles MAY impose additional required fields, constrain vocabulary values, or mandate the use of specific `x-` extensions for a regulatory context. ### Example: a regulatory water quality profile _(since v2.3)_ See [water-quality.md](water-quality.md) § Regulatory profiles. ### Profile validation - If a profile URL is unreachable, returns a non-JSON document, or fails to parse as JSON Schema, the profile is silently ignored. Parsers MUST NOT reject files whose profile URLs are unavailable. - A profile-aware parser SHOULD validate the file against each reachable profile schema and surface failures as **warnings**, not errors. Conformance is asserted by the author, not enforced by the spec. - Profile URLs SHOULD be versioned via path segments or schema-internal `$id` to ensure stability. --- ## Extensibility ### Custom fields — `x-` prefix Any implementer may add custom fields to any object by prefixing the field name with `x-`. This convention is established by OpenAPI 3.x and JSON Schema. Parsers MUST preserve and ignore `x-` fields they do not recognize. Sub-properties within an `x-` object do not need to be prefixed. For application extensions, `x--` is RECOMMENDED (e.g. `x-welldot-custom`), so tools that both use `x-custom` do not collide. _(since v2.4)_ ```json { "id": "b2c3d4e5-...", "type": "constant_rate", "datetime": "2006-03-14T08:00:00-03:00", "x-siagas": { "registro": "SP-0042819", "processo_ana": "02000.001234/2006-78" } } ``` Fields without the `x-` prefix that are not defined in this spec are technically non-conformant. Parsers MUST NOT reject such files but SHOULD emit a warning. ### Custom top-level blocks — `x-` prefix required Top-level blocks not defined in this spec MUST use the `x-` prefix. For application extensions, `x--` is RECOMMENDED (e.g. `x-welldot-custom`), so tools that both use `x-custom` do not collide. _(since v2.4)_ Parsers MUST preserve and ignore unrecognized top-level keys. ```json { "version": "2.4", "x-gas_monitoring": [ ... ], "x-geophysical_logs": [ ... ] } ``` ### Custom vocabulary values — `x-` prefix Open vocabulary fields in this spec (`type` in `hydrodynamic_events`, `category` in `history_logs`, `vocabulary` in `texture` and in water quality `parameter` (as `x-…` vocabularies), `method` in `aquifer_analysis`, `well_type`, and any other field documented as accepting any string; not closed vocabularies such as `history_logs[].status`) SHOULD use the `x-` prefix when the value is non-canonical — i.e. not listed in the recommended vocabulary of this spec. Parsers MUST treat `x-` prefixed vocabulary values as valid. Promotion from `x-` form to canonical form follows the Deprecation Policy (see § Deprecation Policy — Vocabulary value promotion). ```json { "type": "x-slug_test" } { "category": "x-site_visit" } { "method": "x-theis_recovery" } ``` --- ## Offline packaging _(since v2.4)_ A `.well` file references its attachments by absolute URL, and HTTPS links may expire. For regulators and long-term archiving, a `.well` file MAY be delivered as an offline package: a ZIP archive with the extension `.wellz`. - The `.well` file sits at the archive root, and the attachments sit at relative paths inside the archive. - In this mode an `Attachment.uri` MAY be a relative path to the file inside the archive, and `sha256` is required. - Outside a package, `uri` stays an absolute URL. A relative `uri` in a bare `.well` file is not valid. The package is a delivery wrapper, not a second format: the `.well` file inside it is an ordinary `.well` file. Validators and parsers read the `.well` alone, so the schema keeps `uri` as a URL; resolving relative paths and checking `sha256` is the job of the tool that opens the package. --- ## Cross-reference and Uniqueness Rules The `.well` format uses string IDs to link records across arrays. This section is the single normative source for ID semantics. ### Uniqueness scope Each ID-bearing array maintains its **own uniqueness scope**. The ID namespaces are independent: - `hydrodynamic_events[].id` — unique within `hydrodynamic_events` - `aquifer_analysis[].id` — unique within `aquifer_analysis` - `history_logs[].id` — unique within `history_logs` - `pump_installations[].id` — unique within `pump_installations` _(since v2.3)_ - `permits[].id` — unique within `permits` _(since v2.3)_ - `permits[].conditions[].id` — unique within the `conditions` array of one permit _(since v2.3)_ - `permits[].conditions[].fulfillments[].id` — unique within the `fulfillments` array of one condition _(since v2.3)_ - `permits[].history[].id` — unique within the `history` array of one permit _(since v2.3)_ - `permits[].monthly_schedule[].month` and `permits[].volume_limits[].period` — unique within one permit _(since v2.3)_ - `meters[].id` — unique within `meters` _(since v2.3)_ - `production[].id` — unique within `production` _(since v2.3)_ - `operating_regime[].id` — unique within `operating_regime` _(since v2.3)_. `operating_regime[].effective_from` is not unique: duplicates are a warning _(since v2.4)_ - `water_samples[].id` — unique within `water_samples` _(since v2.3)_ - `attachments[].id` — unique within its owning array: the root `attachments`, or the `attachments` array of one record (`history_logs`, `permits`, `permits[].history`, `permits[].conditions[].fulfillments`, `pump_installations`, `meters`, `hydrodynamic_events`, `aquifer_analysis`, `water_samples`). The same id may appear under different records. - `well_id[]` — uniqueness is on the composite key `(authority, id)`, not on `id` alone An ID value MAY repeat across different arrays without conflict (e.g. an event and an analysis MAY both use the ID `"001"`), though distinct values are recommended for clarity. ### Cross-array references The following fields hold references to IDs in other arrays: | Reference field | Points to | Resolution scope | | ----------------------------------------------------- | -------------------------- | ---------------- | | `aquifer_analysis[].source_event_ids[]` | `hydrodynamic_events[].id` | Same file only | | `aquifer_analysis[].static_level_source_id` | `hydrodynamic_events[].id` | Same file only | | `hydrodynamic_events[].corrects` | `hydrodynamic_events[].id` | Same file only | | `production[].corrects` | `production[].id` | Same file only | | `production[].meter_id` | `meters[].id` | Same file only | | `permits[].supersedes` | `permits[].id` | Same file only | | `history_logs[].pump_installation_id` | `pump_installations[].id` | Same file only | | `history_logs[].meter_id` | `meters[].id` | Same file only | | `history_logs[].hydrodynamic_event_ids[]` | `hydrodynamic_events[].id` | Same file only | | `history_logs[].sample_ids[]` | `water_samples[].id` | Same file only | | `permits[].conditions[].fulfillments[].event_id` | `hydrodynamic_events[].id` | Same file only | | `permits[].conditions[].fulfillments[].sample_id` | `water_samples[].id` | Same file only | | `water_samples[].corrects` | `water_samples[].id` | Same file only | | `water_samples[].parent_sample_id` | `water_samples[].id` | Same file only | | `water_samples[].static_level_event_id` | `hydrodynamic_events[].id` | Same file only | | `water_samples[].sampling_point.pump_installation_id` | `pump_installations[].id` | Same file only | Root `attachments` is not a registry: records never reference root attachments by id. Nor is it an aggregate: it holds only general files about the well as a whole (e.g. the drilling report), never copies of the attachments of `history_logs`, `permits` (including their `history` entries and condition `fulfillments`), `pump_installations`, `meters`, `hydrodynamic_events`, `aquifer_analysis` or `water_samples` entries. A document that concerns several records is repeated on each; `sha256` guarantees both copies point to the same file. Cross-file references are not supported in v2. ### Dangling references A **dangling reference** is a reference field whose value does not match any ID in the target array. - Dangling references MUST emit a warning naming the referring field, the referring object's ID, and the missing target ID. - Dangling references MUST NOT cause file rejection. Events may be legitimately deleted from history (regulatory retention rules, GDPR-style erasure) leaving analyses that reference them stranded. - Consumers MUST preserve unresolved references when re-exporting and MUST NOT remove them silently. Removing a reference is always a deliberate user action, never a side effect of import or export. _(since v2.4)_ ### Repeated records _(since v2.4)_ Collective permits and campaign blanks apply to several wells, so they are repeated in each well's file. - Copies MUST share the same `id` and SHOULD have identical content. - Consumers MAY merge copies by `id`. - A fulfillment's `event_id` or `sample_id` resolves only in the file of the well that produced it. In the other copies it is an expected unresolved reference and emits no warning. ### Duplicate IDs within a scope A **duplicate ID** occurs when two records in the same uniqueness scope share the same ID (or, for `well_id`, the same `(authority, id)` pair). Duplicate IDs are **malformed but recoverable**: - Parsers MUST NOT silently accept duplicate IDs. The file is considered malformed. - Parsers MUST NOT silently rewrite duplicate IDs. Automatic renaming would change the object graph in a way that downstream references cannot detect. - Interactive applications MUST surface the duplication to the user with sufficient context to decide: - The IDs in conflict - A summary of each conflicting record (datetime, type, key fields) - The choices: **merge** (keep one, discard the other), **rename** (assign a new ID to one of them, updating any references in the same file), or **abort load**. - Non-interactive parsers (batch importers, server-side validators) MUST reject the file with a clear error rather than guess. - Any user-confirmed resolution MUST be logged as a `history_logs` entry with `category: "event"` describing what was changed. The objective is deterministic load behavior: a `.well` file MUST produce the same object graph across all conformant parsers under default settings. User-mediated repair is the only path that can deviate from this, and it must be explicit. ### `id` field convention All `id` fields are `string` type. UUID v4 is recommended for new records. Sequential strings (e.g. `"evt-001"`) are valid for manually authored files. Numeric IDs from external systems are converted per the rule documented in § well_id (Numeric ID conversion). --- ## Precision and Uncertainty Optional one-sigma precision qualifiers are defined for the most consequential measurement fields. Parsers that do not recognize them ignore them per the foreign-members rule. The precision field name is the measurement field name suffixed with `_precision`. The unit of the precision value is the unit of the measurement it qualifies. ### Supported precision fields | Measurement | Precision field | Defined on | | --------------- | ------------------------- | ------------------------------------- | | `lat` | `lat_precision` | `LocationProperties` | | `lng` | `lng_precision` | `LocationProperties` | | `elevation` | `elevation_precision` | `LocationProperties` | | `static_level` | `static_level_precision` | events, `AquiferAnalysis` | | `dynamic_level` | `dynamic_level_precision` | `AquiferAnalysis` | | `depth` | `depth_precision` | `LevelReading`, `Fracture` | | `rate` | `rate_precision` | `PumpingStep` | | `flow_rate` | `flow_rate_precision` | `AquiferAnalysis` | | `depth` | `depth_precision` | `SamplingPoint` _(since v2.3)_ | | `value` | `value_precision` | water quality `Result` _(since v2.3)_ | All precision fields are optional. When omitted, the measurement value carries no documented precision. Precision is the one-sigma standard deviation in the same unit as the measurement. Laboratory reports usually give an expanded uncertainty U with a coverage factor k (typically k = 2). Converters store `value_precision = U / k`, never U itself. ```json { "depth": 44.8, "depth_precision": 0.01 } ``` --- _`.well` Format Specification v2.4_ # `.well` File Format Specification — Version 2.4: Interoperability **See also:** [overview.md](./overview.md) · [format-reference.md](./format-reference.md) · [object-schemas.md](./object-schemas.md) · [water-quality.md](./water-quality.md) ## Overview This part of the `.well` v2.4 specification covers how `.well` connects to other standards and tools: the reserved JSON-LD `@context` and its publication plan, how the format maps onto the OGC GroundWaterML 2 (GWML2) model, and validating files against the published JSON Schema. ## Table of contents - [JSON-LD Context (Reserved)](#json-ld-context-reserved) - [v2.0 parser requirements](#v20-parser-requirements) - [Recommended usage](#recommended-usage) - [Context publication plan](#context-publication-plan) - [Versioning and stability](#versioning-and-stability) - [Optional fallback](#optional-fallback) - [Relationship to GWML2](#relationship-to-gwml2) - [Authoritative references](#authoritative-references) - [Scope correspondence](#scope-correspondence) - [Conceptual alignment](#conceptual-alignment) - [Class mapping (informative)](#class-mapping-informative) - [Notes on the mapping](#notes-on-the-mapping) - [Profiles for regulatory adoption](#profiles-for-regulatory-adoption) - [Schema Validation](#schema-validation) --- ## JSON-LD Context (Reserved) The `@context` top-level field is reserved for JSON-LD interoperability with semantic web tooling and external standards such as GWML2. See § Relationship to GWML2. ### v2.0 parser requirements - The `@context` field is **optional**. - v2.0 parsers MUST preserve the `@context` field on round-trip, per the foreign-members rule. - v2.0 parsers MUST NOT reject files that include or omit the field. - v2.0 parsers are NOT required to interpret the field as JSON-LD. No semantic processing is mandated by this version of the spec. ### Recommended usage A `.well` file MAY include `"@context": "https://welldot.org/context/v2.jsonld"` to assert that the file's field names map to the canonical `.well` v2 JSON-LD context. Tools that understand JSON-LD can then transform the document into RDF for use with SPARQL, triple stores, or GWML2-conformant pipelines. ### Context publication plan The canonical context is staged across six phases (0–5). Each phase publishes a real, fetchable document at the canonical URL — at no phase is the URL a placeholder. **Phase 0 — Reservation (v2.0 ratification)** A minimal valid JSON-LD 1.1 document is published at `https://welldot.org/context/v2.jsonld` containing only the format-identity terms: ```json { "@context": { "@version": 1.1, "well": "https://welldot.org/vocab/v2#", "version": "well:version", "name": "well:name", "well_type": "well:well_type" } } ``` The document advertises itself as a draft via an `x-welldot-status: reserved` HTTP header and an in-band `@version` marker. This phase reserves the URL and proves it resolves; it does not yet enable meaningful RDF processing. **Phase 1 — Identity & location (v2.0 + 1 month)** Adds mappings for `well_id`, `well_id.authority`, `well_id.id`, `well_id.primary`, `location`, `location.lat`, `location.lng`, `location.elevation`, and `construction_date`. Aliases `location.lat`/`lng` to `geo:lat`/`geo:long` from the W3C Geo vocabulary; aliases `construction_date` to `schema:dateCreated` from Schema.org. After Phase 1, a `.well` document round-trips to a minimal valid RDF graph representing the well's identity and position. **Phase 2 — Constructive blocks (v2.0 + 3 months)** Adds mappings for `bore_hole`, `well_case`, `well_screen`, `reduction`, `surface_case`, `hole_fill`, `cement_pad` and their fields. Where direct GWML2 alignment is verified against the published XSDs, terms alias to the GWML2-WellConstruction namespace; otherwise terms live under the `welldot:vocab/v2#` namespace with explanatory `rdfs:comment` annotations. **Phase 3 — Geological & hydrogeological (v2.0 + 6 months)** Adds mappings for `lithology`, `fractures`, `caves`, `texture`, `texture.code`, `texture.vocabulary`. Aliases `lithology` to `gwml:GW_GeologyLogCoverage` once the corresponding GWML2 class name is verified against the OGC schema documents. **Phase 4 — Hydrodynamic & analysis (v2.1)** Adds mappings for `well_purpose`, `centralizers`, `hydrodynamic_events`, `aquifer_analysis`, `history_logs`, and supporting types (`PumpingStep`, `LevelReading`, `RecoveryPhase`, `Attachment`). Maps event readings to O&M's `OM_Observation` pattern and time-series to TimeseriesML where alignment is verified. This phase ships alongside the v2.1 spec revision. **Phase 5 — Operational blocks (v2.3)** Adds mappings for `attachments`, `Attachment.document_type`, `pump_installations`, `PumpInstallation` and `PumpElectrical` fields (including `installed_by` and `removed_by`), `permits` (`Permit`, `VolumeLimit`, `MonthlyGrant`, `PermitCondition`, `ConditionFulfillment`, `PermitHistoryEntry`, and the administrative `status`), `meters` (`Meter`, including `manufacturer`, `model`, `installed_by`, `removed_by` and `attachments`), `production` (`MeterReading`, `EstimatedVolume`, with `production[].corrects`), `operating_regime` (`OperatingRegime`), the `maintenance` and `status_change` log fields, and `hydrodynamic_events[].corrects`. Permit dates map to `xsd:date` and condition durations to `xsd:duration`; `supersedes` is a succession relation between two instruments, distinct from `corrects`. `corrects` maps to a retraction relation between two observation or ledger records; installation, reading and regime instants map to `xsd:dateTime`; `installed_by` and `removed_by` map to the agent of the installation or removal activity (`prov:wasAssociatedWith`), kept as a plain literal; attachments alias to `schema:MediaObject` (`uri` → `schema:contentUrl`, `media_type` → `schema:encodingFormat`, `sha256` → `schema:sha256`). Units bind to QUDT (`unit:KiloW`, `unit:V`, `unit:MilliM2`, `unit:M3` for meter readings and volumes, `unit:M3-PER-HR` for regime flow rates, `unit:HR` for daily operating time, `unit:MilliM` for meter nominal diameter). This phase ships alongside the v2.3 spec revision. The v2.3 water quality terms ship in the same phase: `water_samples` (`WaterSample`, `SamplingPoint`, `Purge`, `PurgeReading`, `Laboratory`, `Result`, `Parameter`, `Filtration`, `Validation`) and `history_logs[].sample_ids`. Collection, receipt, analysis and validation instants map to `xsd:dateTime`; a `_resolution: "day"` sibling maps the value to `xsd:date` semantics. Each `welldot` parameter code gets a term URI under `https://welldot.org/vocab/water-quality/` (versioned with the published vocabulary), aligned where verified to the corresponding observable property in QUDT, ChEBI or the SOSA/SSN observable-property pattern. A `cas` parameter maps to the CAS Common Chemistry URI of its registry number, for example `{ "code": "71-43-2", "vocabulary": "cas" }` → `https://commonchemistry.cas.org/detail?cas_rn=71-43-2`; a `welldot` code with a published CAS equivalence carries an `owl:sameAs`-style link to the same URI. `x-` vocabularies are left unmapped. Units bind to QUDT (`unit:MilliGM-PER-L`, `unit:MicroS-PER-CentiM`, `unit:DEG_C`, `unit:MilliV`, `unit:BQ-PER-L`, `unit:MicroM`); turbidity, color and microbiological count units without a QUDT term keep their UCUM code as a literal. ### Versioning and stability - The Phase-0 document is replaced in-place as phases ship, but term URIs under `welldot:vocab/v2#` are append-only and never re-purposed within v2. - If a term needs to change meaning, it gets a new URI in `welldot:vocab/v3#`. v2 consumers continue to resolve the old term. - Each phase increments an internal `version` marker on the context document; client tools can detect which phase they're consuming. - A `CHANGELOG.md` is published alongside the context at `https://welldot.org/context/v2.changelog.md` documenting every term added. ### Optional fallback If publishing infrastructure fails, files may inline the context object directly under `@context` instead of referencing the URL. This is supported by JSON-LD 1.1 and parsers MUST accept both forms. --- ## Relationship to GWML2 **GroundWaterML 2** (GWML2) is the OGC international standard for groundwater data exchange. It was developed and ratified by an international consortium that includes the Geological Survey of Canada (GSC), U.S. Geological Survey (USGS), CSIRO and the Bureau of Meteorology (Australia), BRGM (France), Federation University Australia, and Salzburg University, with contributing input from the British Geological Survey, the EU INSPIRE Joint Research Centre, the Polish and German geological surveys, and UNESCO's International Groundwater Resources Assessment Centre. Two versions of the standard are currently in use: - **OGC 16-032r2** — _WaterML 2: Part 4 — GroundWaterML 2 (GWML2)_, version 2.2, ratified in 2016 and published in 2017 - **OGC 19-013** — GWML2 version 2.2.1, ratified in 2019 and published in 2021, which updates 16-032r2 by aligning with GeoSciML 4.1 and Observations & Measurements 2.0 GWML2 defines its model in three stages — a conceptual UML schema, a logical UML schema specialized for OGC standards, and an XML/GML encoding — and explicitly anticipates future JSON or RDF encodings. The standard is organized into six modular packages: GWML2-Main, GWML2-Constituent, GWML2-Flow, GWML2-Well, GWML2-WellConstruction, and GWML2-AquiferTest. `.well` is a JSON-native format that scopes down to the static record plus operational history of a single well. It is not a competing conceptual model — it is an opinionated encoding for a subset of the GWML2 domain, designed for tooling and developer adoption. The optional JSON-LD `@context` (see § JSON-LD Context) is the technical bridge that allows a `.well` document to also be interpreted as a GWML2-aligned RDF graph. ### Authoritative references | Reference | Location | | ----------------------------------- | ----------------------------------------------------- | | GWML2 v2.2 specification (16-032r2) | `https://docs.ogc.org/is/16-032r2/16-032r2.html` | | GWML2 v2.2.1 specification (19-013) | `https://docs.ogc.org/is/19-013/19-013.html` | | GWML2 XML schemas (XSD) | `http://schemas.opengis.net/gwml/2.2/` | | OGC Hydrology DWG GWML2 wiki | `https://external.ogc.org/twiki_public/HydrologyDWG/` | ### Scope correspondence | GWML2 Module | Namespace | `.well` coverage | | ---------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | GWML2-Main | `http://www.opengis.net/gwml-main/2.2` | Out of scope (aquifers, fluid bodies, hydrogeological units as standalone features) | | GWML2-Constituent | `http://www.opengis.net/gwml-constituent/2.2` | Partially covered (`water_samples`, v2.3): measured constituents and properties of sampled water | | GWML2-Flow | `http://www.opengis.net/gwml-flow/2.2` | Out of scope (recharge, discharge, inter-flow) | | GWML2-Well | `http://www.opengis.net/gwml-well/2.2` | Covered: top-level well identity, `location`, `well_id`, well-as-sampling-feature semantics | | GWML2-WellConstruction | `http://www.opengis.net/gwml-wellconstruction/2.2` | Covered: `bore_hole`, `well_case`, `well_screen`, `reduction`, `surface_case`, `hole_fill`, `cement_pad` | | GWML2-AquiferTest | `http://www.opengis.net/gwml-aquifertest/2.2` | Covered: `hydrodynamic_events`, `aquifer_analysis` | ### Conceptual alignment A `.well` document corresponds conceptually to a GWML2 **`GW_Well`** feature, which itself specializes O&M's `SF_SamplingCurve` — wells in GWML2 are modeled as sampling features along whose length observations are made. Boreholes (`Borehole` from GWML2-WellConstruction) are physical engineering artifacts distinct from wells in GWML2; in `.well` v2, both are collapsed into the constructive blocks of a single record since the format describes one composite well. A `.well` `Lithology` array corresponds to a GWML2 **`GW_GeologyLogCoverage`**, which is a realization of `CV_DiscreteCoverage` over depth intervals. Each lithology interval references a `GW_HydrogeologicalUnit` — GWML2's specialization of GeoSciML's `GeologicUnit` — via the `geologic_unit` field; `.well` does not model the hydrogeological unit itself as a standalone feature. ### Class mapping (informative) The mapping below is **informative, not normative**. Class names in italics are verified against published GWML2 documentation (the OGC 16-032r2 specification, the _Hydrogeology Journal_ and _Open Geospatial Data, Software and Standards_ papers, and the OGC Hydrology DWG wiki). Class names marked with `?` are **approximate** — they describe the GWML2 module a `.well` construct belongs to but the exact class name was not verified at the time of writing this draft. Implementers building converters should verify these against the authoritative XSDs at `http://schemas.opengis.net/gwml/2.2/` before relying on them. | `.well` construct | GWML2 module | Class / property | | -------------------------------- | ---------------------- | --------------------------------------------------------------------- | | Well (top-level) | GWML2-Well | _`GW_Well`_ (specializes `O&M:SF_SamplingCurve`) | | `well_id[]` | GWML2-Well | `GW_Well` identifier property (?) | | `location` | GWML2-Well | `GW_Well` shape / position property (?) | | `bore_hole[]` | GWML2-WellConstruction | `Borehole` (?) | | `well_case[]` | GWML2-WellConstruction | `Casing` (?) | | `surface_case[]` | GWML2-WellConstruction | `Casing` with surface depth range (?) | | `well_screen[]` | GWML2-WellConstruction | `Screen` (?) | | `reduction[]` | GWML2-WellConstruction | No direct equivalent — casing transition | | `hole_fill[]` | GWML2-WellConstruction | Annular fill / gravel pack / seal (?) | | `cement_pad` | GWML2-WellConstruction | No direct equivalent — wellhead furniture | | `centralizers[]` | GWML2-WellConstruction | No verified equivalent — casing accessory (?) | | `well_purpose[]` | GWML2-Well | `GW_Well` purpose / use property (?) | | `lithology[]` | GWML2-Well | _`GW_GeologyLogCoverage`_ (realizes `CV_DiscreteCoverage`) | | `lithology[].geologic_unit` | GWML2-Main | _`GW_HydrogeologicalUnit`_ (specializes GeoSciML `GeologicUnit`) | | `fractures[]` | GWML2-Well | Fracture log interval (?) | | `caves[]` | GWML2-Well | Void / cavity log interval (?) | | `hydrodynamic_events[]` | GWML2-AquiferTest | `GW_AquiferTest` (?) plus `OM_Observation` series via TimeseriesML | | `aquifer_analysis[]` | GWML2-AquiferTest | Aquifer test result properties (?) | | `history_logs[]` | n/a | No direct equivalent — operational record outside GWML2 scope | | `pump_installations[]` | GWML2-WellConstruction | Pump / wellhead equipment (?) — pending check against the XSD | | `permits[]` | n/a | No direct equivalent — operational record outside GWML2 scope | | `meters[]` | n/a | No direct equivalent — operational record outside GWML2 scope | | `production[]` | n/a | No direct equivalent — operational record outside GWML2 scope | | `operating_regime[]` | n/a | No direct equivalent — operational record outside GWML2 scope | | `attachments[]` (root) | n/a | No direct equivalent — documents referenced by URL | | `hydrodynamic_events[].corrects` | n/a | No direct equivalent — ledger correction outside GWML2 scope | | `water_samples[]` | GWML2-Constituent | `SF_Specimen` (?) — the sample taken from the `GW_Well` | | `water_samples[].results[]` | GWML2-Constituent | `OM_Measurement` (?) on the specimen, observed property = `parameter` | | `water_samples[].corrects` | n/a | No direct equivalent — ledger correction outside GWML2 scope | A complete, normative mapping will be published as a separate document once the GWML2 XSDs have been read directly and each verified class name and property path has been confirmed. Until then, this table is a structural guide and not a conversion specification. ### Notes on the mapping - **Wells vs. boreholes in GWML2.** GWML2 distinguishes a `GW_Well` (the construction with hydrogeological purpose) from a `Borehole` (the physical hole). A single `.well` document conflates both since it describes one composite installation. A `.well → GWML2` converter must emit both features and link them; a `GWML2 → .well` converter must merge them. - **Aquifer tests in GWML2.** GWML2-AquiferTest uses TimeseriesML (OGC 15-042r2) for time-series readings and O&M's `OM_Observation` pattern for individual measurements. The `.well` event types (`spot_measurement`, `constant_rate`, `step_drawdown`, `airlift`, `recovery_only` and, since v2.4, `logger_series`) are pragmatic field-record categories with no direct GWML2 counterpart; a converter must decompose each into one or more `GW_AquiferTest` features with appropriate `OM_Observation` results. A `logger_series` is not a test: a converter maps it to a TimeseriesML series of `OM_Observation` results on the well, without a `GW_AquiferTest`. - **Hydrogeological units.** `.well` stores `lithology[].geologic_unit` and `aquifer_unit` as free-text strings. In GWML2 these resolve to references to `GW_HydrogeologicalUnit` features, which in real GWML2 deployments are typically catalogued in national registries (e.g. French BDLISA, Canadian GIN). A `.well → GWML2` converter SHOULD resolve these strings to authoritative unit URIs when a national vocabulary is available; absent that, it emits inline `GW_HydrogeologicalUnit` features with `gml:name` set to the string value. - **Water samples as specimens.** A `water_samples` entry corresponds to an O&M `SF_Specimen` taken from the well (the sampled feature), with `datetime` as the sampling time and `sampling_point` as the sampling location along the well. Each result is an `OM_Measurement` whose observed property is the parameter (a CAS or `welldot` term URI), whose result is the value in the code's canonical unit, and whose procedure is `method`. GWML2-Constituent describes the constituents of a fluid body rather than individual analyses, so a converter maps `.well` results to O&M observations on the specimen and derives constituent statements only when needed. Censoring (`qualifier`), detection and quantification limits, `lab_flags` and `validation` have no direct GWML2 counterpart and travel as result quality metadata or `x-` extensions. Field QA/QC links (`parent_sample_id`), purge data and chain of custody are outside GWML2 scope. Limits and exceedances are never part of the file and have no mapping. - **History logs are outside GWML2 scope.** GWML2 models the well as a static feature and its observations as a time-series. Maintenance interventions, incidents, status changes (`status_change`), permit condition fulfillment, and operational events are not represented. The same holds for the v2.3 operational blocks (`permits`, `meters`, `production`, `operating_regime`): metered production is an abstraction record, not an aquifer observation. A converter SHOULD preserve `.well` `history_logs` in a GeoSciML observation event sequence or as an `x-` extension on the GWML2 side, but full lossless round-trip is not guaranteed. ### Profiles for regulatory adoption National regulators with existing data models (Brazil SIAGAS, French BSS / BDLISA, EU INSPIRE hydrogeology) can publish `.well` profiles that constrain the format to their required fields and vocabularies. Profile schemas may extend the canonical `.well` v2 schema with regulator-specific requirements (e.g. mandatory `well_id` entries for the relevant national authority, restricted enums for casing materials, required precision fields) without breaking interoperability with the core spec. --- ## Schema Validation The canonical JSON Schema for `.well` v2 is published at: ``` https://welldot.org/schema/v2/well.schema.json ``` The schema conforms to **JSON Schema 2020-12** (`https://json-schema.org/draft/2020-12/schema`). Implementers SHOULD use a JSON Schema validator that supports this draft (Ajv 8+, python-jsonschema 4+, NetworkNT json-schema-validator). Earlier-draft validators may work for most fields but are not officially supported. Files MAY include a `$schema` field referencing this URL: ```json { "$schema": "https://welldot.org/schema/v2/well.schema.json", "version": "2.4", ... } ``` The schema is generated from the Zod validators in `@welldot/core` to prevent drift between runtime validation and published schema. The generated file lives at [`spec/schema/v2/well.schema.json`](../schema/v2/well.schema.json) in the repository root, is published at `https://welldot.org/schema/v2/well.schema.json` and ships in `@welldot/core` as `@welldot/core/well.schema.json`. Profile schemas SHOULD extend this canonical schema via `allOf` + `$ref`. --- _`.well` Format Specification v2.4_ # `.well` File Format Specification — Version 2.4: Object Schemas **See also:** [overview.md](./overview.md) · [format-reference.md](./format-reference.md) · [interoperability.md](./interoperability.md) · [water-quality.md](./water-quality.md) ## Overview This part of the `.well` v2.4 specification defines the records inside each top-level block: the common types (`Attachment`, date durations), the construction and geology objects, the `hydrodynamic_events` ledger and its supporting objects, `aquifer_analysis`, `history_logs`, and the operational blocks added in v2.3 (`pump_installations`, `permits`, `meters`, `production`, `operating_regime`). It ends with a complete example file. Water samples are specified in [water-quality.md](./water-quality.md); units and cross-reference rules in [format-reference.md](./format-reference.md). ## Table of contents - [Common Types _(since v2.3)_](#common-types-since-v23) - [`Attachment`](#attachment) - [Date duration](#date-duration) - [Object Schemas](#object-schemas) - [`BoreHole` — `bore_hole[]`](#borehole--bore_hole) - [`WellCase` — `well_case[]`](#wellcase--well_case) - [`Reduction` — `reduction[]`](#reduction--reduction) - [`WellScreen` — `well_screen[]`](#wellscreen--well_screen) - [`SurfaceCase` — `surface_case[]`](#surfacecase--surface_case) - [`HoleFill` — `hole_fill[]`](#holefill--hole_fill) - [`Centralizer` — `centralizers[]` _(since v2.1)_](#centralizer--centralizers-since-v21) - [`CementPad` — `cement_pad`](#cementpad--cement_pad) - [`Lithology` — `lithology[]`](#lithology--lithology) - [`Texture`](#texture) - [`Fracture` — `fractures[]`](#fracture--fractures) - [`Cave` — `caves[]`](#cave--caves) - [`hydrodynamic_events[]`](#hydrodynamic_events) - [Event types](#event-types) - [Common fields (all event types)](#common-fields-all-event-types) - [Corrections _(since v2.3)_](#corrections-since-v23) - [`spot_measurement`](#spot_measurement) - [`constant_rate`](#constant_rate) - [`step_drawdown`](#step_drawdown) - [`airlift`](#airlift) - [`recovery_only`](#recovery_only) - [`logger_series` _(since v2.4)_](#logger_series-since-v24) - [Supporting Object Schemas](#supporting-object-schemas) - [`PumpingStep`](#pumpingstep) - [`LevelReading`](#levelreading) - [`RecoveryPhase`](#recoveryphase) - [`aquifer_analysis[]`](#aquifer_analysis) - [`method` — Recommended values](#method--recommended-values) - [Derived Parameters — Computation Reference](#derived-parameters--computation-reference) - [Querying Current State](#querying-current-state) - [`history_logs[]`](#history_logs) - [Categories](#categories) - [`HistoryLogEntry`](#historylogentry) - [`updated_at` semantics](#updated_at-semantics) - [Data links _(since v2.3)_](#data-links-since-v23) - [Category-specific fields _(since v2.3)_](#category-specific-fields-since-v23) - [`pump_installations[]` _(since v2.3)_](#pump_installations-since-v23) - [`PumpInstallation`](#pumpinstallation) - [`PumpElectrical`](#pumpelectrical) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection) - [Derived values](#derived-values) - [Rendering](#rendering) - [`permits[]` _(since v2.3)_](#permits-since-v23) - [`Permit`](#permit) - [`PermitHistoryEntry`](#permithistoryentry) - [`VolumeLimit`](#volumelimit) - [`MonthlyGrant`](#monthlygrant) - [`PermitCondition`](#permitcondition) - [Deadline generation (normative)](#deadline-generation-normative) - [Condition fulfillment](#condition-fulfillment) - [Permit status (derived)](#permit-status-derived) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection-1) - [`meters[]` _(since v2.3)_](#meters-since-v23) - [`Meter`](#meter) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection-2) - [`production[]` _(since v2.3)_](#production-since-v23) - [Common fields](#common-fields) - [`meter_reading`](#meter_reading) - [`estimated_volume`](#estimated_volume) - [Volume derivation (normative)](#volume-derivation-normative) - [Precedence](#precedence) - [Derived values](#derived-values-1) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection-3) - [`operating_regime[]` _(since v2.3)_](#operating_regime-since-v23) - [`OperatingRegime`](#operatingregime) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection-4) - [`water_samples[]` _(since v2.3)_](#water_samples-since-v23) - [Complete Example](#complete-example) --- ## Common Types _(since v2.3)_ ### `Attachment` The `.well` format is not a file container. Attachments are referenced by URL. Since v2.3, `Attachment` is a common type used in several places. | Field | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------------------ | | `id` | string | yes | Unique within its owning `attachments` array. UUID v4 recommended. | | `uri` | string | yes | Full HTTPS URL. A relative path is valid only inside an offline package _(since v2.4)_. | | `media_type` | string | yes | MIME type (e.g. `image/jpeg`, `application/pdf`). | | `document_type` | string | no | What the document is. See format-reference.md § `Attachment.document_type`. _(since v2.3)_ | | `filename` | string | no | Original filename for display. | | `description` | string | no | Caption or content description. | | `sha256` | string | no | SHA-256 hash of the file in lowercase hex. Consumers SHOULD validate after download. | #### Where attachments are allowed | Location | Purpose | | --------------------------------------------------- | --------------------------------------------------------------------------- | | Root `attachments[]` | Documents about the well as a whole, not tied to any record. _(since v2.3)_ | | `history_logs[].attachments` | Supporting documents or photos of a log entry. | | `permits[].attachments` | The legal document (portaria, certificate). _(since v2.3)_ | | `permits[].history[].attachments` | Notices, receipts, protocols of an administrative step. _(since v2.3)_ | | `permits[].conditions[].fulfillments[].attachments` | Proof of submission (`condition_evidence`). _(since v2.3)_ | | `pump_installations[].attachments` | Pump curves, invoices, photos. _(since v2.3)_ | | `meters[].attachments` | Installation photos, invoices, certificates. _(since v2.3)_ | | `hydrodynamic_events[].attachments` | Field sheets, logger exports. _(since v2.3)_ | | `aquifer_analysis[].attachments` | Interpretation reports. _(since v2.3)_ | | `water_samples[].attachments` | Laboratory reports (`lab_report`), field sheets, photos. _(since v2.3)_ | An attachment belongs to the record it documents. A document that concerns several records is repeated on each; the repetition is one URI and one hash, and the hash guarantees both copies point to the same file. The root array is not a registry, and records do not reference root attachments by id. Nor is it an aggregate: it holds only general files about the well as a whole (e.g. the drilling report), never copies of the attachments of `history_logs`, `permits` (including their `history` entries and condition `fulfillments`), `pump_installations`, `meters`, `hydrodynamic_events`, `aquifer_analysis` or `water_samples` entries. `Attachment.id` is unique within its owning array: the root `attachments[]`, or the `attachments` array of one record. The same id may appear under different records. #### URL resolvability The `uri` field is a reference, not a guarantee. URLs may become unreachable over time; consumers MUST handle fetch failures gracefully and MUST NOT treat them as file-format errors. When `sha256` is present, consumers that successfully fetch the attachment MUST validate the hash and MUST reject content that fails validation. Producers SHOULD include `sha256` for any attachment intended for long-term preservation. ### Date duration Relative deadlines and recurrences (`permits[].conditions[].due_after`, `recurrence`) are ISO 8601 durations restricted to date components: `PnY`, `PnM`, `PnW`, `PnD` and their combinations, such as `P1Y6M`. At least one component is required. Time components (`T…`) are malformed. Durations are added to calendar dates, never to instants; see format-reference.md § Datetime Conventions. --- ## Object Schemas ### `BoreHole` — `bore_hole[]` A drilled interval of the borehole. Multiple entries describe a telescoping borehole. | Field | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diameter` | number | yes | Borehole diameter in millimeters. | | `drilling_method` | string | no | Drilling method. Open vocabulary: see [format-reference.md](format-reference.md) § `bore_hole[].drilling_method`. | ```json { "from": 0, "to": 80, "diameter": 250, "drilling_method": "rotary" } ``` --- ### `WellCase` — `well_case[]` Steel or plastic casing installed inside the borehole. | Field | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Casing material. Open vocabulary: see [format-reference.md](format-reference.md) § Construction materials. | | `diameter` | number | yes | Casing outer diameter in millimeters. | --- ### `Reduction` — `reduction[]` A transition piece connecting two casing or screen sections of different diameters. | Field | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diam_from` | number | yes | Diameter at top in millimeters. | | `diam_to` | number | yes | Diameter at bottom in millimeters. | | `type` | string | yes | Reducer material. Open vocabulary: see [format-reference.md](format-reference.md) § Construction materials. | --- ### `WellScreen` — `well_screen[]` Slotted or wire-wound screen section. | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Screen material. Open vocabulary: see [format-reference.md](format-reference.md) § Construction materials. The screen design goes in `slot_type`. | | `diameter` | number | yes | Screen outer diameter in millimeters. | | `screen_slot` | number | yes | Slot opening size in millimeters. | | `slot_type` | string | no | Screen design _(since v2.3)_. Open vocabulary: see [format-reference.md](format-reference.md) § `well_screen[].slot_type`. | > **v1 migration:** v1's `screen_slot_mm` field is renamed to `screen_slot` in v2. The value is in millimeters before and after — no conversion needed. See § Units for the rule that screen slot is always in millimeters regardless of any application's display preference. --- ### `SurfaceCase` — `surface_case[]` Outer protective casing installed near the surface. | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diameter` | number | yes | Casing outer diameter in millimeters. | --- ### `HoleFill` — `hole_fill[]` Material placed in the annular space between casing and borehole wall. | Field | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Either `gravel_pack` or `seal`. | | `diameter` | number | yes | Outer diameter of the filled annulus in millimeters. | | `description` | string | yes | Material description (e.g. grain size, material). | --- ### `Centralizer` — `centralizers[]` _(since v2.1)_ Centralizers clamped to a casing or screen string to keep it concentric in the borehole, so the gravel pack and annular seal have uniform thickness. Each entry describes a depth interval over which centralizers are installed at a regular spacing, which is how drilling reports usually give them. | Field | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters (depth of the first centralizer). | | `to` | number | yes | End depth in meters (depth of the last centralizer). `from === to` denotes a single centralizer. | | `spacing` | number | no | Spacing between consecutive centralizers in meters. Must be positive. Omit when the spacing is unknown. | | `type` | string | yes | Centralizer type. Open vocabulary: see [format-reference.md](format-reference.md) § `centralizers[].type`. | | `diameter` | number | no | As-built outer diameter in millimeters. | | `description` | string | no | Free-text description (e.g. material, manufacturer). | - The array is **optional**. v2.0 documents without it remain valid. - Individual positions are derived from `from`, `to`, and `spacing` (`from`, `from + spacing`, … up to `to`). The count is derived and MUST NOT be stored. - Each entry SHOULD fall within a `well_case` or `well_screen` interval. Validators MUST NOT reject documents that violate this. - Centralizers do not contribute to the calculated well depth. --- ### `CementPad` — `cement_pad` Concrete wellhead pad. **All dimensions are in meters**. | Field | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | yes | Pad material. Open vocabulary: see [format-reference.md](format-reference.md) § `cement_pad.type`; other free text (e.g. a shape such as `circular`) remains valid. | | `width` | number | yes | Width in meters. | | `thickness` | number | yes | Thickness in meters. | | `length` | number | yes | Length in meters. | > Note: A typical residential cement pad has dimensions on the order of `1.0` meter. Applications presenting these values in non-metric units (e.g. feet for US users) must convert `cement_pad` dimensions along with all other length fields. --- ### `Lithology` — `lithology[]` Geological description of a depth interval. | Field | Type | Required | Description | | --------------- | --------- | -------- | -------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `description` | string | yes | Free-text geological description. | | `color` | string | yes | Representative color as a CSS hex value. | | `texture` | `Texture` | yes | Lithology pattern reference. See below. | | `geologic_unit` | string | yes | Stratigraphic or geologic unit name. | | `aquifer_unit` | string | yes | Aquifer unit name like "Aquifero Pirabas" or "Massachusetts Aquifer" | ### `Texture` | Field | Type | Required | Description | | ------------ | ---------------- | -------- | ------------------------------------------------------------------------------------------------------ | | `code` | string \| number | yes | The texture code within the declared vocabulary. | | `vocabulary` | string | no | Either a short canonical token or an HTTPS URI identifying the vocabulary. Default: `fgdc`. See below. | #### `vocabulary` values The `vocabulary` field accepts two forms: **Short canonical tokens** — well-known vocabularies maintained by the welldot project or by widely-adopted standards bodies. The v2.0 canonical set is: | Token | Vocabulary | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `fgdc` | FGDC Digital Cartographic Standard for Geologic Map Symbolization (FGDC-STD-013-2006), Chapter 25 — Lithology. Codes are integers. | | `cgi` | Commission for the Management and Application of Geoscience Information (CGI) Simple Lithology vocabulary. Codes are URIs or short tokens per CGI. | | `custom` | Application-defined vocabulary with no interoperability guarantee. Codes are opaque strings. Use of `custom` is discouraged when a documented alternative exists; prefer publishing a vocabulary URI. | **HTTPS URIs** — when a regulator, geological survey, or institution maintains its own lithology vocabulary, the URI of its definition document is a valid `vocabulary` value. The URI SHOULD resolve to a machine-readable vocabulary description (SKOS, JSON-LD, or a documented JSON list of `{code, label}` entries), though parsers do not fetch it. ```json { "vocabulary": "fgdc", "code": 607 } { "vocabulary": "cgi", "code": "sandstone" } { "vocabulary": "https://brgm.fr/vocab/lithology/v1", "code": "GRES" } { "vocabulary": "https://welldot.org/vocab/lithology-extended/v1", "code": "LATERITA" } ``` Profiles SHOULD constrain the `vocabulary` field to a specific URI or short token relevant to their regulatory context. > **v1 migration:** A v1 file's bare `fgdc_texture: ""` field is treated as `texture: { code: "", vocabulary: "fgdc" }`. ```json { "from": 0, "to": 20, "description": "Areia fina amarelada", "color": "#f5deb3", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Quaternário", "aquifer_unit": "freático" } ``` --- ### `Fracture` — `fractures[]` A discrete fracture or fracture zone. | Field | Type | Required | Description | | ----------------- | ------- | -------- | -------------------------------------------------------------------- | | `depth` | number | yes | Depth of fracture in meters. | | `water_intake` | boolean | yes | Whether the fracture produces water. | | `description` | string | yes | Free-text description. | | `swarm` | boolean | yes | Whether this fracture belongs to a swarm. | | `azimuth` | number | yes | Azimuth in degrees from geographic north (0–360). | | `dip` | number | yes | Dip angle in degrees from horizontal (0–90). | | `depth_precision` | number | no | One-sigma precision of `depth` in meters. Default handled by editors | --- ### `Cave` — `caves[]` A cavity or void zone. | Field | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `water_intake` | boolean | yes | Whether the cave produces water. | | `description` | string | yes | Free-text description. | --- ## `hydrodynamic_events[]` A chronological ledger of all hydrodynamic observations; it grows by appending. Events are ordered by `datetime` ascending (UTC-normalized instants); parsers MUST NOT assume order and SHOULD sort by `datetime` when querying. When two events share the same instant, an optional `sequence` integer breaks the tie. > **Note — interpretive contracts, not pumping methods.** Each `type` value identifies a _category of field record_ with a distinct semantic contract, not a pumping technique or equipment class. Equipment belongs in the `equipment` field; test-analysis method belongs in `method` on `aquifer_analysis`. The contract for each type specifies what data it reliably supplies, how many steps it should carry, whether recovery is permitted, and whether its event ID is valid input to `source_event_ids` on an `aquifer_analysis` entry. ### Event types | `type` | Português (BR) | What it records | `steps` constraint | `recovery` | Valid in `source_event_ids` | | ------------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------ | ---------- | -------------------------------- | | `spot_measurement` | Medição pontual | Static water level during a routine visit; optionally an informal brief pump observation. Not a controlled test. | SHOULD be 0 or 1 | Optional | As `static_level_source_id` only | | `constant_rate` | Teste de vazão constante | Controlled pump test at exactly one fixed rate. | SHOULD be exactly 1 | Optional | Yes | | `step_drawdown` | Teste de vazão escalonada | Controlled pump test at two or more successive rates in ascending order. | SHOULD be ≥ 2, ascending | Optional | Yes | | `airlift` | Air-lift | Historical fact that air-lift development was performed and produced an estimated yield. | SHOULD be ≥ 1 | Optional | No — see § airlift | | `recovery_only` | Apenas recuperação | Recovery after a pumping event whose drawdown data was not recorded. | None | Required | Yes | | `logger_series` | Série de datalogger | Continuous level series recorded by an installed sensor. Not a controlled test nor a static level. _(since v2.4)_ | None | No | No — see § logger_series | Each known `type` MUST satisfy its own schema: a malformed `constant_rate`, for example, is invalid rather than read as an unknown type. The step counts in the table are recommendations: `steps` MUST be an array of valid `PumpingStep` entries, but another count is valid. _(since v2.4)_ The `type` field accepts any string. Non-canonical values SHOULD use the `x-` prefix. ### Common fields (all event types) Only `id`, `type`, and `datetime` are required. All others are optional for all types. | Field | Type | Required | Description | | ------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `id` | string | yes | Unique within `hydrodynamic_events`. UUID v4 recommended. | | `type` | string | yes | Event type. | | `datetime` | string | yes | RFC 3339 datetime with mandatory UTC offset (e.g. `"2006-03-14T08:00:00-03:00"`). See § Datetime Conventions. | | `sequence` | integer | no | Tiebreaker for events sharing the same instant. Lower values sort first. | | `operator` | string | no | Person or company conducting the measurement or test. | | `equipment` | string | no | Equipment description. | | `notes` | string | no | Free-text observations. | | `corrects` | string | no | `hydrodynamic_events[].id` of the event this one retracts. See below. _(since v2.3)_ | | `attachments` | `Attachment[]` | no | Field sheets, logger exports. See § Attachment. _(since v2.3)_ | ### Corrections _(since v2.3)_ `hydrodynamic_events` is a ledger: events MAY be edited in place, but editing is NOT RECOMMENDED after the event has been exported. `corrects` remains the recommended mechanism for measurement corrections (a re-measurement): the correction is a new event whose `corrects` holds the id of the retracted event. - A retracted event stays in the file but is excluded from every derivation (current static level, analyses, charts). - Chains are allowed (C corrects B, which corrected A); only the last uncorrected entry counts. - A `corrects` cycle is malformed and emits a warning. A `corrects` value that does not resolve is a dangling reference. - An `aquifer_analysis` whose `source_event_ids` or `static_level_source_id` points to a retracted event emits a warning naming both ids. --- ### `spot_measurement` | Field | Type | Required | Description | | ------------------------ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `static_level` | number | yes | Depth to water surface from ground level, in meters. Negative when the level is above ground (flowing artesian). See § Level sign convention. | | `static_level_precision` | number | no | One-sigma precision of `static_level`. | | `measurement_method` | string | no | Open vocabulary: see [format-reference.md](format-reference.md) § `hydrodynamic_events[].measurement_method`. | | `steps` | `PumpingStep[]` | no | SHOULD carry at most one step; another count is valid. For an informal brief pump observation during the visit — not a controlled test. Use `constant_rate` for a formal test. | | `recovery` | `RecoveryPhase` | no | Recovery after the optional pumping step. | --- ### `constant_rate` | Field | Type | Required | Description | | ------------------------ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `static_level` | number | no | Pre-test static level in meters. Omit if not measured. | | `static_level_precision` | number | no | One-sigma precision of `static_level`. | | `steps` | `PumpingStep[]` | no | SHOULD carry exactly one entry; another count is valid. A second step would make this a `step_drawdown`. Omit if only recovery data is available. | | `recovery` | `RecoveryPhase` | no | Recovery measurements after pump shutdown. | --- ### `step_drawdown` Jacob's `B` and `C` are derived — store them in `aquifer_analysis`, never here. | Field | Type | Required | Description | | ------------------------ | --------------- | -------- | --------------------------------------------------------------------------------------- | | `static_level` | number | no | Pre-test static level in meters. | | `static_level_precision` | number | no | One-sigma precision of `static_level`. | | `steps` | `PumpingStep[]` | yes | SHOULD carry two or more steps in ascending order of flow rate; another count is valid. | | `recovery` | `RecoveryPhase` | no | Recovery after the final step. | --- ### `airlift` This type exists to record the historical fact that air-lift development was performed and produced an estimated yield. It is **not** suitable input for `aquifer_analysis`: tools producing `aquifer_analysis` entries MUST refuse to accept `airlift` event IDs in `source_event_ids` and MUST emit a warning if one is supplied. Static level is not reliably measurable during air-lift and SHOULD be omitted. | Field | Type | Required | Description | | ---------- | --------------- | -------- | --------------------------------------------------------------------------------------------------- | | `steps` | `PumpingStep[]` | yes | SHOULD carry one or more steps; another count is valid. Each step may carry time-series `readings`. | | `recovery` | `RecoveryPhase` | no | Recovery measurements after air-lift shutdown, if recorded. | --- ### `recovery_only` | Field | Type | Required | Description | | ------------------ | --------------- | -------- | ------------------------------------------------ | | `pumping_rate` | number | no | Estimated preceding flow rate in m³/h. | | `pumping_duration` | number | no | Estimated preceding pumping duration in minutes. | | `recovery` | `RecoveryPhase` | yes | Recovery time-series. | --- ### `logger_series` _(since v2.4)_ A continuous level series recorded by an installed sensor: a pressure transducer with a datalogger, as used in monitoring networks and in mining (dewatering, slope piezometers, permit conditions). It is neither a controlled test nor a static level. | Contract | Value | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `steps` | None. `steps` MUST NOT be present. | | `recovery` | No. `recovery` MUST NOT be present. | | Valid in `source_event_ids` | No. | | Valid as `static_level_source_id` | No. | | Inherited common fields | `equipment`, `operator`, `attachments` (the raw logger export, with `sha256`), `corrects`, and the `history_logs[].hydrodynamic_event_ids` link. | | Field | Type | Required | Description | | ------------------------- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `measurement_method` | string | no | Open vocabulary: see [format-reference.md](format-reference.md) § `hydrodynamic_events[].measurement_method`. | | `sensor_depth` | number | no | Sensor depth in meters from ground level. A level below it indicates an invalid reading. | | `interval` | number | no | Nominal interval between readings, in minutes. Greater than zero. | | `barometric_compensation` | string | no | Closed: `barologger` (Barologger / Barologger), `vented` (Vented cable / Cabo ventilado) or `none` (None / Nenhuma). | | `aggregation` | string | no | Closed: `none` (Raw readings / Leituras brutas), `hourly` (Hourly / Horária) or `daily` (Daily / Diária). When `hourly` or `daily`, every reading MUST carry `depth_min` and `depth_max`. | | `readings` | `LevelReading[]` | yes | One or more readings. Each reading MUST carry `datetime` and MUST NOT carry `elapsed`. See § LevelReading. | A series MAY grow with new readings instead of becoming one event per download. > **Note — pump state is not stored per reading.** A series has no per-reading `pumping` flag. In a well that pumps, the series mixes static and dynamic levels; interpretation crosses the series with `production` and `pump_installations`. Validation (warnings, never rejection): - An `aquifer_analysis` whose `source_event_ids` or `static_level_source_id` points to a `logger_series` emits a warning naming both ids. - A reading whose `depth` is greater than `sensor_depth` (the level is below the sensor) emits a warning naming the event. ```json { "id": "8f2c1e4a-3b6d-4f0e-9a7c-5d1b2e3f4a5b", "type": "logger_series", "datetime": "2026-07-01T00:00:00-03:00", "equipment": "Solinst Levelogger 5 LT, SN 2081234", "measurement_method": "pressure_transducer", "sensor_depth": 42.0, "interval": 15, "barometric_compensation": "barologger", "aggregation": "none", "readings": [ { "datetime": "2026-07-01T00:00:00-03:00", "depth": 18.42, "pressure": 233.9 }, { "datetime": "2026-07-01T00:15:00-03:00", "depth": 18.44, "pressure": 233.7 } ] } ``` --- ## Supporting Object Schemas ### `PumpingStep` | Field | Type | Required | Description | | ---------------- | ---------------- | -------- | ----------------------------------------------------------------- | | `rate` | number | yes | Pumping rate in m³/h. | | `rate_precision` | number | no | One-sigma precision of `rate`. | | `duration` | number | no | Duration in minutes. | | `readings` | `LevelReading[]` | no | Time-series during this step. A single terminal reading is valid. | ### `LevelReading` | Field | Type | Required | Description | | ----------------- | ------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `elapsed` | number | yes, except in `logger_series` | Time since step start (or pump shutdown for recovery) in minutes. MUST NOT be present in `logger_series` readings. | | `datetime` | string | in `logger_series` only | RFC 3339 instant of the reading, with UTC offset. Required in `logger_series`; optional elsewhere. _(since v2.4)_ | | `depth` | number | yes | Depth to water surface from ground level in meters. In an aggregated `logger_series`, the representative value of the interval. | | `depth_precision` | number | no | One-sigma precision of `depth`. | | `pressure` | number | no | Pressure transducer reading in kPa. | | `depth_min` | number | aggregated `logger_series` only | Shallowest depth in the interval, in meters. Required when `aggregation` is `hourly` or `daily`; used only there. _(since v2.4)_ | | `depth_max` | number | aggregated `logger_series` only | Deepest depth in the interval, in meters. Required when `aggregation` is `hourly` or `daily`; used only there. _(since v2.4)_ | ### `RecoveryPhase` | Field | Type | Required | Description | | ---------- | ---------------- | -------- | ------------------------------------------------------------------ | | `readings` | `LevelReading[]` | yes | Time-series after pump shutdown. `elapsed` measured from shutdown. | --- ## `aquifer_analysis[]` An array of interpreted aquifer parameter sets. Multiple entries may coexist, representing different methods or analysts. | Field | Type | Required | Description | | ------------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | yes | Unique within `aquifer_analysis`. | | `datetime` | string | yes | RFC 3339 datetime with mandatory UTC offset. | | `analyst` | string | no | Name of the hydrogeologist. | | `source_event_ids` | string[] | yes | IDs of source `hydrodynamic_events` from this same file. See § Cross-reference and Uniqueness Rules. | | `method` | string | no | See vocabulary below. | | `static_level` | number | no | Static level used as reference, in meters. | | `static_level_precision` | number | no | One-sigma precision of `static_level`. | | `static_level_source_id` | string | no | ID of the event from which `static_level` was taken. | | `dynamic_level` | number | no | Stabilized dynamic level in meters. | | `dynamic_level_precision` | number | no | One-sigma precision of `dynamic_level`. | | `flow_rate` | number | no | Flow rate associated with `dynamic_level`, in m³/h. | | `flow_rate_precision` | number | no | One-sigma precision of `flow_rate`. | | `max_flow_rate` | number | no | Maximum recommended sustained extraction rate in m³/h, as judged by the analyst from the source events. This is an interpretive recommendation, not the test rate. Distinct from `flow_rate`, which is the rate actually applied during the test that produced `dynamic_level`. | | `max_flow_rate_precision` | number | no | One-sigma precision of `max_flow_rate` in m³/h. | | `max_flow_rate_basis` | string | no | Free-text justification: safety factor applied, regulatory framework referenced (e.g. `"80% of test rate per ANA practice"`, `"limited by available drawdown to top of screen"`), or assumptions about long-term recharge. | | `specific_capacity` | number | no | `Q/s` in m³/h per m (i.e. m²/h). | | `transmissivity` | number | no | Aquifer transmissivity `T` in m²/s. | | `storativity` | number | no | Dimensionless storativity `S`. | | `hydraulic_conductivity` | number | no | Hydraulic conductivity `K` in m/s. Requires `aquifer_thickness`. | | `aquifer_thickness` | number | no | Saturated aquifer thickness in meters. | | `jacob_b` | number | no | Formation loss coefficient from Jacob's equation. | | `jacob_c` | number | no | Well loss coefficient from Jacob's equation. | | `well_efficiency_pct` | number | no | Well efficiency as a percentage. | | `attachments` | `Attachment[]` | no | Interpretation reports. See § Attachment. _(since v2.3)_ | | `notes` | string | no | Methodology notes, assumptions, data quality remarks. | ### `method` — Recommended values `cooper_jacob`, `theis`, `neuman`, `hantush`, `birsoy_summers`, `eden_hazel`, `visual_inspection`; labels in [format-reference.md](format-reference.md) § `aquifer_analysis[].method`. Non-canonical values SHOULD use the `x-` prefix. --- ## Derived Parameters — Computation Reference The following values are **never stored**. Applications compute them on demand. | Parameter | Formula | Inputs | | ---------------------------- | -------------------------------------------- | ---------------------------------------------- | | Drawdown `s` at time `t` | `depth(t) − static_level` | `LevelReading.depth`, event `static_level` | | Residual drawdown (recovery) | `depth(t) − static_level` | `RecoveryPhase.readings`, event `static_level` | | Specific capacity `Q/s` | `flow_rate / (dynamic_level − static_level)` | `AquiferAnalysis` fields | | Unit drawdown `s/Q` | `(dynamic_level − static_level) / flow_rate` | `AquiferAnalysis` fields | | Formation loss | `jacob_b × Q` | `AquiferAnalysis.jacob_b`, `flow_rate` | | Well loss | `jacob_c × Q²` | `AquiferAnalysis.jacob_c`, `flow_rate` | | Hydraulic conductivity `K` | `transmissivity / aquifer_thickness` | `AquiferAnalysis` fields | --- ## Querying Current State **Current static level (NE):** Exclude retracted events (§ Corrections), filter the remaining events where `static_level` is present, sort by `datetime` descending (UTC-normalized), take the first result. **Current specific capacity:** Find the most recent `aquifer_analysis` with `specific_capacity` present. **Current transmissivity:** Find the most recent `aquifer_analysis` with `transmissivity` present. **Recommended maximum flow:** Read the most recent `aquifer_analysis[].max_flow_rate`, with `max_flow_rate_basis` and `max_flow_rate_precision`. **Current pump:** The `pump_installations` entry without `removed_at`. See § `pump_installations[]`. **Current meter(s):** The `meters` entries without `removed_at`. More than one open meter is valid (secondary meters) but emits a warning. See § `meters[]`. **Regime in force:** The `operating_regime` entry with the latest `effective_from` not after the instant of interest. Before the first entry, the regime is unknown. See § `operating_regime[]`. **Current well status:** The `status` of the `history_logs` entry of category `status_change` with the latest `datetime`. With no `status_change` entry, the status is **unknown** — applications MUST NOT assume `active`. See § `history_logs[]` — `status_change`. --- ## `history_logs[]` A mutable chronological record of physical interventions, inspections, and incidents. Entries may be edited or removed to correct field errors. Each entry carries two distinct timestamps: - **`datetime`** — when the logged event actually occurred in the field. - **`updated_at`** — when this log entry was most recently created or edited in the record system. This timestamp tracks the data lineage of the entry itself, not the event it describes. These timestamps may differ substantially: a maintenance intervention `datetime` of 2018-06-12 may be paired with an `updated_at` of 2024-03-15 if the entry was added retroactively or corrected. ### Categories | `category` | Portuguese (BR) | Description | | --------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `maintenance` | Manutenção | Physical intervention or service task: pump replacement, casing repair, cleaning, redevelopment, meter calibration. Carries the category-specific fields below _(since v2.3)_. | | `inspection` | Inspeção | Site visit without physical alteration: visual survey, camera inspection, sample collection. | | `incident` | Incidente | Unplanned event: partial collapse, contamination, prolonged drought, vandalism. | | `event` | Evento | Generic milestone: construction completion, commissioning, change of use, ownership transfer, rehabilitation, data-integrity repair. | | `status_change` | Mudança de situação | _(since v2.3)_ The well's operating status changed (e.g. active → inactive). Carries the category-specific fields below. | Vocabulary is open. Non-canonical values SHOULD use the `x-` prefix. ### `HistoryLogEntry` | Field | Type | Required | Description | | ------------------------ | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | yes | Unique within `history_logs`. | | `datetime` | string | yes | RFC 3339 datetime with mandatory UTC offset. When the logged event occurred. | | `updated_at` | string | no | RFC 3339 datetime with mandatory UTC offset. When this entry was most recently created or edited. When omitted, treated as equal to `datetime`. Producers SHOULD set this field whenever they create or modify an entry. | | `category` | string | yes | Event category. | | `description` | string | yes | Free-text account: work performed, findings, or incident narrative. | | `author` | string | no | Person or company responsible for the record. | | `severity` | string | no | Open vocabulary: see [format-reference.md](format-reference.md) § `history_logs[].severity`. | | `attachments` | `Attachment[]` | no | Supporting documents or photos. See § Attachment. | | `hydrodynamic_event_ids` | string[] | no | _(since v2.3)_ `hydrodynamic_events[].id`s holding the data produced or observed by this entry. Valid on every category. See § Data links. | | `sample_ids` | string[] | no | _(since v2.3)_ `water_samples[].id`s collected or concerned by this entry. Valid on every category. See § Data links. | ### `updated_at` semantics - `updated_at` is usually later than `datetime`. It MAY be earlier when an entry is pre-dated (retroactive entry of a historical event); both orderings are valid and neither emits a warning. - When a producer modifies the `description`, `category`, `severity`, `author`, or `attachments` of an existing entry, it SHOULD update `updated_at` to the current instant. - When a producer modifies only the `id` (e.g. during duplicate-resolution), it SHOULD NOT update `updated_at`, since the substantive content of the entry has not changed. - The `updated_at` field is not retroactive: parsers reading a v1 or early-v2.0 file without `updated_at` MUST NOT synthesize one. `Attachment` is a common type since v2.3; see § Common Types — `Attachment`. ### Data links _(since v2.3)_ A log entry records that something was done or observed; measured values belong in their own block and are referenced by id, never copied into the log. `hydrodynamic_event_ids` and `sample_ids` link an entry of any category to that data: a `level_measurement` maintenance points to its `spot_measurement` event, a `pump_test` to its `constant_rate` or `step_drawdown` event, a `water_sampling` task or an `inspection` visit to the samples it collected (a sample, its field duplicate and a blank are three ids), and an `incident` to the sample that revealed a contamination. The log keeps what the data block does not: crew, cost, invoices and the narrative of the task. Both fields are arrays of distinct ids from this same file. An id that does not resolve emits a warning. A link to a ledger entry that was later retracted (`corrects`) is not rewritten; consumers resolve it to the entry in force when needed. ### Category-specific fields _(since v2.3)_ Category-specific fields MUST be absent on entries of other categories; their presence emits a warning. They are optional in the schema: a missing required category field (e.g. `maintenance_type` on a `maintenance` entry) surfaces as a warning, never a rejection. Permit condition fulfillment is not a log category: it is recorded on the condition itself (see § `permits[]` — Condition fulfillment). #### `maintenance` | Field | Type | Required | Description | | ---------------------- | ------ | -------- | ------------------------------------------------------------- | | `maintenance_type` | string | yes | See format-reference.md § `history_logs[].maintenance_type`. | | `pump_installation_id` | string | no | `pump_installations[].id` the task concerns. | | `meter_id` | string | no | `meters[].id` the task concerns (e.g. a `meter_calibration`). | The data a task produced is linked with `hydrodynamic_event_ids` / `sample_ids` (see § Data links), never copied into the log. #### `status_change` | Field | Type | Required | Description | | -------- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- | | `status` | string (enum) | yes | One of the five values below (closed vocabulary; any other value is rejected). See format-reference.md § `history_logs[].status`. | | Value | Portuguese (BR) | Semantics | | ---------------- | ---------------------- | --------------------------------------- | | `active` | Ativo / em operação | Normal operation. | | `maintenance` | Em manutenção | Temporarily out of service for work. | | `inactive` | Paralisado | Out of service, recoverable. | | `decommissioned` | Desativado / tamponado | Permanently closed and properly sealed. | | `abandoned` | Abandonado | Left without proper sealing. | The current status is the `status` of the `status_change` entry with the latest `datetime`. With no such entry, the status is unknown. `status` is a closed vocabulary: the `x-` extension mechanism does not apply, and a file carrying any other value is rejected. Any non-`status_change` entry dated after a `decommissioned` or `abandoned` status emits a warning. A stopped well is recorded with a `status_change`, never with an `operating_regime` entry whose `flow_rate` is `0`. --- ## `pump_installations[]` _(since v2.3)_ An installation block: each entry is one installation of a pump in the well, present from `installed_at` until `removed_at`. Entries are edited in place, and `updated_at` records the last edit. The current pump and its submergence are derived, never stored. - `removed_at` earlier than or equal to `installed_at` is malformed and emits a warning. - `installed_by` and `removed_by` name who did the work (person or company, free text). `removed_by` on an entry without `removed_at` carries no meaning and consumers ignore it. - Each entry is one installation, not one piece of equipment. A unit pulled and reinstalled gets a new entry with the same `serial`. - Overlapping open installations emit a warning, not an error. Standby pumps exist. ### `PumpInstallation` | Field | Type | Required | Unit | Description | | ----------------- | ---------------- | -------- | ---- | --------------------------------------------------------------------------- | | `id` | string | yes | | Unique within `pump_installations`. UUID v4 recommended. | | `installed_at` | string (instant) | yes | | RFC 3339 instant when the pump entered service in this well. | | `removed_at` | string (instant) | no | | When it left. Absent means currently installed. | | `installed_by` | string | no | | Person or company that installed the pump. | | `removed_by` | string | no | | Person or company that removed the pump. Only meaningful with `removed_at`. | | `type` | string | yes | | See format-reference.md § `pump_installations[].type`. | | `power_source` | string | no | | `grid`, `solar`, `diesel`, `hybrid`, `x-…` | | `manufacturer` | string | no | | | | `model` | string | no | | | | `serial` | string | no | | Links reinstallations of the same unit. | | `intake_depth` | number | no | m | Depth of the pump intake (crivo), from ground level. | | `rated_flow_rate` | number | no | m³/h | Nameplate duty-point flow. | | `rated_head` | number | no | m | Nameplate duty-point head. | | `rated_power` | number | no | kW | Motor power. | | `stages` | integer | no | | Number of stages. | | `riser_diameter` | number | no | mm | Riser pipe (edutor), as-built outer diameter. | | `riser_material` | string | no | | Open vocabulary: see format-reference.md § Construction materials. | | `check_valve` | boolean | no | | Whether a check valve is installed. | | `electrical` | `PumpElectrical` | no | | See below. | | `notes` | string | no | | | | `updated_at` | string (instant) | no | | Last edit of this record. | | `attachments` | `Attachment[]` | no | | Pump curves, invoices, photos. See § Attachment. | ### `PumpElectrical` All fields optional. | Field | Type | Unit | | --------------- | ------- | ---------- | | `voltage` | number | V | | `phases` | integer | `1` or `3` | | `cable_section` | number | mm² | | `cable_length` | number | m | ### Validation (warnings, never rejection) - `intake_depth` greater than `well_depth`, or below the bottom of the deepest `bore_hole` interval. - `intake_depth` inside a `well_screen` interval. - More than one entry without `removed_at`. - `removed_at` earlier than or equal to `installed_at`. ### Derived values - **Current pump:** the entry without `removed_at`; if all are removed, none. - **Submergence:** `intake_depth − dynamic_level`, using the most recent applicable reading during pumping. A negative value means the intake is above the water level. - **Time in service per unit:** sum of installation durations sharing a `serial`. ### Rendering Renderers draw the current pump ending at `intake_depth` and its riser from the surface. Earlier installations are not drawn by default. --- ## `permits[]` _(since v2.3)_ A mutable record block: each entry transcribes one legal instrument governing abstraction from the well (outorga, dispensa, cadastro), from its request on. Entries are edited in place, and `updated_at` records the last edit. The attached document is authoritative. The administrative `status` set by the authority is stored; the validity status and condition deadlines are always derived, never stored. ### `Permit` | Field | Type | Required | Unit | Description | | ---------------------- | ---------------------- | -------- | ---- | ----------------------------------------------------------------------------- | | `id` | string | yes | | Unique within `permits`. UUID v4 recommended. | | `type` | string | yes | | See format-reference.md § `permits[].type`. | | `authority` | string | yes | | Issuing body, e.g. `ANA`, `SEMAS-PA`. Same semantics as `well_id.authority`. | | `identifier` | string | no | | Identifier of the granted instrument (portaria, license code), verbatim. | | `request_identifier` | string | no | | Identifier of the request or administrative process (protocolo), verbatim. | | `status` | string (enum) | no | | Administrative status. Closed vocabulary, see below. Absent means `granted`. | | `issued_at` | string (date) | no | | Issue date. | | `valid_from` | string (date) | no | | Absent means valid from `issued_at`. | | `valid_until` | string (date) | no | | Absent means no fixed expiry. | | `renewal_requested_at` | string (date) | no | | Date a renewal request was filed. | | `water_use` | string[] | no | | See format-reference.md § `permits[].water_use`. | | `flow_rate` | number | no | m³/h | Maximum granted flow. | | `daily_operating_time` | number | no | h | Maximum granted daily operating time. Above 24 is read as 24, with a warning. | | `volume_limits` | `VolumeLimit[]` | no | | Only volumes stated in the document. | | `monthly_schedule` | `MonthlyGrant[]` | no | | Month-by-month grant. | | `conditions` | `PermitCondition[]` | no | | Obligations (condicionantes). | | `history` | `PermitHistoryEntry[]` | no | | Administrative steps: filing, process, notifications, fees. | | `supersedes` | string | no | | `permits[].id` of the instrument this one legally replaces. | | `notes` | string | no | | | | `updated_at` | string (instant) | no | | Last edit of this record. | | `attachments` | `Attachment[]` | no | | The legal document. See § Attachment. | `water_use` describes what the abstracted water is for. It is distinct from the top-level `well_purpose`, which describes the well's role. A renewal is not a permit type: it is a new permit of the same type whose `supersedes` points to the previous one. `supersedes` expresses legal succession between two valid instruments and is not a correction mechanism. Identifiers are free strings preserved verbatim, because agencies use diverse formats (numbers, letters, slashes). A permit that has only been requested has no `identifier` yet: it is identified by `request_identifier`. At least one of the two SHOULD be present. #### Administrative status `status` records what the authority decided, which dates alone cannot express. It is a closed vocabulary: the `x-` extension mechanism does not apply, and a file carrying any other value is rejected. | Value | Portuguese (BR) | Semantics | | ----------- | --------------- | --------------------------------------------------------------- | | `requested` | Requerida | Filed and awaiting a decision. No grant yet. | | `granted` | Concedida | Issued. The validity status is derived from the dates. Default. | | `suspended` | Suspensa | Temporarily halted by the authority. | | `revoked` | Revogada | Cancelled by the authority. | | `denied` | Indeferida | The request was refused. | | `withdrawn` | Cancelada | The request or permit was given up by the holder. | The change itself (notice of suspension, decision) is recorded as a `history` step, with the document attached. ### `PermitHistoryEntry` | Field | Type | Required | Description | | ------------- | ---------------- | -------- | ----------------------------------------------------------------------------------- | | `id` | string | yes | Unique within the permit's `history`. UUID v4 recommended. | | `date` | string (date) | yes | When the step happened. | | `type` | string | no | See format-reference.md § `permits[].history[].type`. | | `description` | string | yes | What happened: protocol, dispatch, notice, fee, inspection, decision. | | `done` | boolean | no | For actionable steps (fee paid, notification answered). Absent means informational. | | `due_date` | string (date) | no | Date by which an actionable step must be done. | | `updated_at` | string (instant) | no | Last edit of this record. | | `attachments` | `Attachment[]` | no | Notices, receipts, protocols. See § Attachment. | A step whose `due_date` has passed and that is not `done` is past due. Applications SHOULD surface it. ### `VolumeLimit` | Field | Type | Required | Unit | Description | | -------- | ------ | -------- | ---- | --------------------------------------------------------------- | | `period` | string | yes | | `daily`, `monthly`, `annual`. Each period appears at most once. | | `volume` | number | yes | m³ | | Volumes implied by flow × time × days are derived and MUST NOT be stored here. ### `MonthlyGrant` | Field | Type | Required | Unit | Description | | ---------------------- | ------- | -------- | ---- | --------------------------------------- | | `month` | integer | yes | | 1–12, unique within the schedule. | | `flow_rate` | number | no | m³/h | | | `daily_operating_time` | number | no | h | Above 24 is read as 24, with a warning. | | `days` | integer | no | | Days of operation granted in the month. | When `monthly_schedule` is present, a month absent from it has no abstraction granted. The top-level `flow_rate` and `daily_operating_time` hold the maximum across months; a monthly value above them emits a warning. ### `PermitCondition` | Field | Type | Required | Description | | -------------- | ------------------------ | -------- | ---------------------------------------------------------------- | | `id` | string | yes | Unique within the permit's `conditions` array. | | `description` | string | yes | Text of the condition as written in the document. | | `category` | string | no | See format-reference.md § `permits[].conditions[].category`. | | `first_due` | string (date) | no | Explicit date of the first deadline. | | `due_after` | string (duration) | no | First deadline relative to the permit's start date, e.g. `P90D`. | | `recurrence` | string (duration) | no | Interval between deadlines, e.g. `P6M`. Absent means one-time. | | `last_due` | string (date) | no | No deadline is generated after this date. | | `occurrences` | integer | no | Maximum number of deadlines, counting the first. | | `responsible` | string | no | Person or team accountable for meeting the obligation. | | `fulfillments` | `ConditionFulfillment[]` | no | Deadlines met. See § Condition fulfillment. | `first_due` and `due_after` are mutually exclusive; a condition carrying both is malformed and `first_due` wins. A condition with neither is undated: it is valid and is displayed, but generates no deadlines. Durations follow § Common Types — Date duration. ### Deadline generation (normative) Applications generate a condition's deadlines as follows. 1. **Start date** of the permit: `valid_from`, else `issued_at`. Without either, `due_after` cannot be resolved and the condition is undated. 2. **Anchor**: `first_due`, else start date + `due_after`. 3. **Deadline n** (n = 0, 1, 2 …) = anchor + n × `recurrence`, always computed from the anchor, never from the previous deadline. Years and months are added first; when the resulting day does not exist in the target month, that month's last day is used. Weeks and days are then added. Without `recurrence`, only n = 0 exists. 4. **Stop** when any limit is reached: `occurrences` deadlines generated; the date passes `last_due`; or the date passes the permit's effective end. 5. **Effective end** of the permit: if another permit supersedes it, the day before the successor's start date; otherwise `valid_until`; while the status is `active_pending_renewal`, there is no end. With no end, applications generate deadlines up to a horizon of their choice. 6. **Administrative status**: a `requested`, `denied` or `withdrawn` permit generates no deadlines. A `suspended` or `revoked` permit generates none after today; those already due remain. Computing every deadline from the anchor prevents drift: a monthly obligation anchored on 31 January falls on 28 or 29 February and back on 31 March. | Document wording | Encoding | | -------------------------------------------- | ------------------------------------------------------ | | Install a meter within 90 days | `due_after: "P90D"` | | Semiannual report from issuance | `due_after: "P6M", recurrence: "P6M"` | | Semiannual report by 31 Jan and 31 Jul | `first_due: "2027-01-31", recurrence: "P6M"` | | Monthly level readings during the first year | `due_after: "P1M", recurrence: "P1M", occurrences: 12` | | Annual water analysis | `due_after: "P1Y", recurrence: "P1Y"` | | Request renewal 90 days before expiry | `first_due` set to that date by the author | ### Condition fulfillment Fulfillment is recorded in the condition's `fulfillments`. Each entry fulfills one deadline, identified by its `due_date`. #### `ConditionFulfillment` | Field | Type | Required | Description | | ------------- | ---------------- | -------- | ---------------------------------------------------------------------------------- | | `id` | string | yes | Unique within the condition's `fulfillments`. UUID v4 recommended. | | `datetime` | string (instant) | yes | When the obligation was fulfilled, e.g. when a report was filed. | | `due_date` | string (date) | no | The deadline fulfilled. Absent for undated conditions. | | `description` | string | no | | | `author` | string | no | Who fulfilled the obligation. | | `event_id` | string | no | `hydrodynamic_events[].id` that satisfied the obligation, when applicable. | | `sample_id` | string | no | `water_samples[].id` that satisfied the obligation, when applicable. | | `updated_at` | string (instant) | no | Last edit of this record. | | `attachments` | `Attachment[]` | no | Proof of submission, with `document_type: "condition_evidence"`. See § Attachment. | A water-quality `monitoring` condition is fulfilled by pointing to the sample with `sample_id`. Derived status of each deadline, evaluated on the local civil date at the well site: - **fulfilled** — a fulfillment has its `due_date`. **Fulfilled late** when that fulfillment's local date is after `due_date`. The local date is the `datetime` converted to `location.properties.timezone` _(since v2.4)_; without `timezone`, the date part of the string in the offset it carries. - **upcoming** — no match and today is on or before `due_date`. - **overdue** — no match and today is after `due_date`. An undated condition is fulfilled by an entry without `due_date`. A fulfillment whose `due_date` matches no generated deadline, a fulfillment without `due_date` on a dated condition, or an `event_id` / `sample_id` that does not resolve emits a warning. Conditions belong to their permit: when a permit is superseded, its deadlines stop at its effective end and the successor's conditions take over. ### Permit status (derived) Evaluated on the local civil date at the well site, in order: 1. A stored `status` other than `granted` → that value (`requested`, `suspended`, `revoked`, `denied`, `withdrawn`). 2. Another permit `supersedes` it → `superseded`. 3. Today is before its start date → `not_yet_valid`. 4. `valid_until` is absent, or today is on or before it → `active`. 5. `renewal_requested_at` is present and on or before `valid_until` → `active_pending_renewal`. 6. Otherwise → `expired`. The lead time a renewal request requires varies by jurisdiction; it is evaluated by applications or regulatory profiles, not by the format. ### Validation (warnings, never rejection) - `valid_until` earlier than the start date. - A `supersedes` cycle, or a `supersedes` reference that does not resolve. - Two granted, non-superseded permits of the same `type` with overlapping validity. - A `daily_operating_time` above 24, on the permit or in `monthly_schedule`. It is read as 24 by every derivation; the stored value is not rewritten. A negative value is a schema error. - A `monthly_schedule` value above the permit's top-level maximum. - A duplicate `month`, `volume_limits` period, condition `id`, `history` `id` or fulfillment `id` within one permit. - A condition with both `first_due` and `due_after`. - A permit with neither `identifier` nor `request_identifier`, or a granted permit without `identifier`. - A condition fulfillment that matches no deadline, or whose `event_id` / `sample_id` does not resolve. - Permits with condition fulfillments in a file without `location.properties.timezone` _(since v2.4)_. A collective grant covering several wells is repeated in each well's file until cross-file references arrive in v3. --- ## `meters[]` _(since v2.3)_ An installation block: each entry is one installation of a totalizer (hidrômetro) in the well, present from `installed_at` until `removed_at`. Entries are edited in place, and `updated_at` records the last edit. A meter holds device facts only: every register value, including the installation and removal readings, lives in `production`. The installation-pattern rules of § `pump_installations[]` apply; overlapping open meters emit a warning, not an error, because secondary meters exist. ### `Meter` | Field | Type | Required | Unit | Description | | ------------------ | ---------------- | -------- | ---- | ---------------------------------------------------------------------------- | | `id` | string | yes | | Unique within `meters`. UUID v4 recommended. | | `installed_at` | string (instant) | yes | | RFC 3339 instant when the meter entered service in this well. | | `removed_at` | string (instant) | no | | When it left. Absent means currently installed. | | `installed_by` | string | no | | Person or company that installed the meter. | | `removed_by` | string | no | | Person or company that removed the meter. Only meaningful with `removed_at`. | | `type` | string | no | | See format-reference.md § `meters[].type`. | | `manufacturer` | string | no | | | | `model` | string | no | | | | `serial` | string | no | | Links reinstallations of the same unit. | | `nominal_diameter` | number | no | mm | Nominal diameter (DN). | | `max_reading` | number | no | m³ | Register capacity, used to detect rollover. Greater than 0. | | `notes` | string | no | | | | `updated_at` | string (instant) | no | | Last edit of this record. | | `attachments` | `Attachment[]` | no | | Installation photos, invoices, certificates. See § Attachment. | The register values at installation and removal are recorded as ordinary `meter_reading` entries in `production`, with `datetime` equal to `installed_at` or `removed_at`. Meter calibrations are `history_logs` entries of category `maintenance` with `maintenance_type: "meter_calibration"` and a `meter_id`. ### Validation (warnings, never rejection) - `removed_at` earlier than or equal to `installed_at`. - More than one entry without `removed_at`. - A `production` reading for this meter outside its installation window. --- ## `production[]` _(since v2.3)_ A ledger: the record of water produced by the well, which grows by appending. Entries MAY be edited in place, but editing is NOT RECOMMENDED after the entry has been exported. `corrects` remains the recommended mechanism for measurement corrections: the correction is a new entry whose `corrects` holds the id of the retracted entry, following § `hydrodynamic_events[]` — Corrections (retracted entries stay in the file and are excluded from every derivation; chains are allowed; a cycle emits a warning). Entries are discriminated by `type`. Volumes are always derived, never stored as totals. ### Common fields | Field | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------- | | `id` | string | yes | Unique within `production`. UUID v4 recommended. | | `type` | string | yes | `meter_reading` or `estimated_volume`. Non-canonical types SHOULD use the `x-` prefix. | | `corrects` | string | no | `production[].id` of the entry this one retracts. | | `sequence` | integer | no | Tie-breaker for identical instants, lower first. | | `notes` | string | no | | An entry of a known `type` that lacks its required fields is rejected. Entries of unknown (`x-`) types are preserved with their extra members and are excluded from volume derivations. ### `meter_reading` | Field | Type | Required | Unit | Description | | ---------- | ---------------- | -------- | ---- | ---------------------------------------------------------------- | | `datetime` | string (instant) | yes | | RFC 3339 instant of the reading. | | `meter_id` | string | yes | | `meters[].id`. | | `reading` | number | yes | m³ | Register value. Meters reading in liters are converted on input. | | `source` | string | no | | `manual`, `telemetry`, `x-…` | A totalizer value is cumulative, so telemetry needs no aggregation field: storing fewer points loses no volume. The producing application decides the density written to the file. ### `estimated_volume` A volume produced in a period, estimated without a meter (e.g. before a meter was installed, or while it was out of service). | Field | Type | Required | Unit | Description | | -------------- | ---------------- | -------- | ---- | ---------------------------------------------------------------------------------------- | | `period_start` | string (instant) | yes | | Start of the declared period. | | `period_end` | string (instant) | yes | | End of the declared period. SHOULD be later than `period_start`; otherwise a warning. | | `volume` | number | yes | m³ | Volume produced in the period. | | `method` | string | no | | Basis of the estimate: `flow_time`, `energy`, `user_statement`, `x-…`. Descriptive only. | ### Volume derivation (normative) Corrected entries are ignored throughout. Readings of each meter are ordered by instant, then `sequence`. 1. For consecutive readings r₁, r₂ of the same meter, if r₂ ≥ r₁ the volume is r₂ − r₁. 2. If r₂ < r₁ and the meter has `max_reading`, the volume is (`max_reading` − r₁) + r₂ (rollover). 3. If r₂ < r₁ and there is no `max_reading`, the interval is unknown and a warning is emitted. It is never negative. 4. Volume is never computed across two different meters. When readings exist at the removal and installation instants, coverage is complete up to the removal and from the installation; the gap between them is unmetered and not counted. Without those readings, the volume between the two meters is unknown. 5. Production before a meter's first reading is unknown, not zero. ### Precedence - An `estimated_volume` counts only for time not covered by meter readings, prorated linearly by the uncovered fraction of its period. Overlap emits a warning, and the meter wins. `method` plays no part in this rule. - Volumes declared to a regulator (annual use declarations) are not production entries. They are the fulfillment of the permit's reporting condition (`permits[].conditions[].fulfillments`), with the declaration attached. ### Derived values Volume per period (day, month, year), prorated linearly by time across period boundaries; average flow over a period; compliance against `permits[].volume_limits` and `monthly_schedule`. None of these are stored. ### Validation (warnings, never rejection) - A reading outside its meter's installation window, or a `meter_id` that does not resolve. - A decreasing reading on a meter without `max_reading` (unknown interval). - An `estimated_volume` overlapping metered time. - An `estimated_volume` whose `period_end` is not later than `period_start`. - A `corrects` cycle, or a `corrects` reference that does not resolve. --- ## `operating_regime[]` _(since v2.3)_ A mutable record block: each entry declares how the well is intended to run from a given instant. Entries are edited in place, and `updated_at` records the last edit. A change of regime is a new entry; the entry with the latest `effective_from` not after a given instant is the regime in force. There is no end field. ### `OperatingRegime` | Field | Type | Required | Unit | Description | | ---------------------- | ---------------- | -------- | ---- | ------------------------------------------------------- | | `id` | string | yes | | Unique within `operating_regime`. | | `effective_from` | string (instant) | yes | | When the regime takes effect. Duplicates are a warning. | | `flow_rate` | number | no | m³/h | | | `daily_operating_time` | number | no | h | Above 24 is read as 24, with a warning. | | `days_per_week` | integer | no | | 1–7. | | `notes` | string | no | | | | `updated_at` | string (instant) | no | | Last edit of this record. | - An absent field means unknown, never zero. - A stopped well is recorded with a `history_logs` `status_change`, not a regime with `flow_rate: 0`. - A `daily_operating_time` above 24 is stored as written, read as 24 by every derivation, and emits a warning. A negative `daily_operating_time` is a schema error, as is a `days_per_week` outside 1–7 (or not an integer). ### Validation (warnings, never rejection) - A `daily_operating_time` above 24. - Two entries with the same `effective_from`. - Regime values above those of the permit active at `effective_from` (`flow_rate`, `daily_operating_time`). --- ## `water_samples[]` _(since v2.3)_ A ledger of water samples, each with its field and laboratory results. The full schema — `WaterSample`, `SamplingPoint`, `Purge`, `PurgeReading`, `Laboratory`, `Result`, `Parameter`, `Filtration`, `Validation`, result rules, validation and derived values — is specified in [water-quality.md](water-quality.md). --- ## Complete Example ```json { "$schema": "https://welldot.org/schema/v2/well.schema.json", "@context": "https://welldot.org/context/v2.jsonld", "version": "2.4", "well_type": "tubular", "well_purpose": ["production"], "name": "Poço PP-01", "well_driller": "Perfuradora XYZ", "construction_date": "2006-03-10", "obs": "Sem anomalias observadas durante a perfuração.", "well_id": [ { "authority": "SIAGAS", "id": "SP-0042819", "primary": true }, { "authority": "ANA", "id": "02000.001234/2006-78" } ], "location": { "lat": -1.4558, "lng": -48.5039, "elevation": 12.5, "properties": { "timezone": "America/Belem", "elevation_datum": "wgs84_ellipsoid", "crs": "EPSG:4326", "elevation_precision": 0.5 } }, "profiles": ["https://welldot.org/profiles/brazil-ana/v1/schema.json"], "bore_hole": [ { "from": 0, "to": 80, "diameter": 250, "drilling_method": "rotary" } ], "well_case": [ { "from": 0, "to": 60, "type": "carbon_steel", "diameter": 200 } ], "reduction": [], "well_screen": [ { "from": 60, "to": 80, "type": "stainless_steel", "diameter": 150, "screen_slot": 0.5, "slot_type": "continuous_slot" } ], "surface_case": [{ "from": 0, "to": 3, "diameter": 300 }], "hole_fill": [ { "from": 60, "to": 80, "type": "gravel_pack", "diameter": 250, "description": "Seixo 2-4mm" }, { "from": 3, "to": 60, "type": "seal", "diameter": 250, "description": "Cimento" } ], "centralizers": [ { "from": 6, "to": 54, "spacing": 12, "type": "spring_bow", "diameter": 245 } ], "cement_pad": { "type": "concrete", "width": 1.0, "thickness": 0.15, "length": 1.0 }, "lithology": [ { "from": 0, "to": 20, "description": "Areia fina amarelada", "color": "#f5deb3", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Quaternário", "aquifer_unit": "freático" }, { "from": 20, "to": 80, "description": "Granito fraturado cinza", "color": "#a9a9a9", "texture": { "code": 718, "vocabulary": "fgdc" }, "geologic_unit": "Embasamento Cristalino", "aquifer_unit": "fraturado" } ], "fractures": [ { "depth": 45.2, "depth_precision": 0.1, "water_intake": true, "description": "Fratura aberta", "swarm": false, "azimuth": 120, "dip": 35 } ], "caves": [], "hydrodynamic_events": [ { "id": "d4e5f6a7-b8c9-0123-defa-234567890123", "type": "airlift", "datetime": "2006-03-10T14:00:00-03:00", "operator": "Perfuradora XYZ", "steps": [ { "rate": 340.0, "duration": 30, "readings": [ { "elapsed": 30, "depth": 44.8, "depth_precision": 0.05 } ] } ], "notes": "Air-lift at end of drilling. Static level not reliable." }, { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "type": "constant_rate", "datetime": "2006-03-14T08:00:00-03:00", "operator": "Perfuradora XYZ", "equipment": "Submersible pump 15 CV", "static_level": 28.74, "static_level_precision": 0.01, "steps": [ { "rate": 340.0, "duration": 1440, "readings": [ { "elapsed": 1, "depth": 32.1 }, { "elapsed": 5, "depth": 38.4 }, { "elapsed": 30, "depth": 42.8 }, { "elapsed": 60, "depth": 43.9 }, { "elapsed": 1440, "depth": 44.8 } ] } ], "recovery": { "readings": [ { "elapsed": 5, "depth": 38.2 }, { "elapsed": 15, "depth": 33.6 }, { "elapsed": 30, "depth": 30.1 } ] }, "notes": "Test terminated at 24h. Recovery monitored for 30 min." }, { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "spot_measurement", "datetime": "2010-07-22T09:15:00-03:00", "static_level": 31.2, "measurement_method": "electric_probe", "notes": "Routine annual monitoring." }, { "id": "e5f6a7b8-c9d0-1234-efab-567890123456", "type": "spot_measurement", "datetime": "2026-09-15T09:10:00-03:00", "static_level": 33.4, "measurement_method": "electric_probe", "notes": "Level before water sampling." } ], "aquifer_analysis": [ { "id": "f6a7b8c9-d0e1-2345-fabc-456789012345", "datetime": "2006-03-15T16:00:00-03:00", "analyst": "Dr. Maria Silva", "source_event_ids": ["b2c3d4e5-f6a7-8901-bcde-f12345678901"], "method": "cooper_jacob", "static_level": 28.74, "static_level_source_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "dynamic_level": 44.8, "flow_rate": 340.0, "specific_capacity": 21.17, "transmissivity": 4.2e-4, "notes": "Cooper-Jacob straight-line fit, t=10min to t=1440min. S not determinable — no observation well." } ], "attachments": [ { "id": "c1d2e3f4-a5b6-7890-cdef-123456789abc", "uri": "https://files.wellmanager.example.com/wells/pp-01/relatorio-perfuracao.pdf", "media_type": "application/pdf", "document_type": "drilling_report", "filename": "relatorio-perfuracao.pdf" } ], "pump_installations": [ { "id": "e1f2a3b4-c5d6-7890-efab-234567890abc", "installed_at": "2006-03-20T10:00:00-03:00", "removed_at": "2018-06-12T09:30:00-03:00", "installed_by": "Perfurações Silva Ltda.", "removed_by": "Bombas Norte Serviços", "type": "submersible", "power_source": "grid", "rated_power": 11.0, "intake_depth": 52 }, { "id": "f2a3b4c5-d6e7-8901-fabc-345678901bcd", "installed_at": "2018-06-12T15:00:00-03:00", "updated_at": "2024-03-15T11:42:00-03:00", "type": "submersible", "power_source": "grid", "manufacturer": "Schneider", "model": "ME-25", "intake_depth": 54, "rated_power": 7.36, "riser_diameter": 114.3, "riser_material": "galvanized_steel", "check_valve": true, "electrical": { "voltage": 380, "phases": 3, "cable_section": 10, "cable_length": 70 }, "attachments": [ { "id": "b8c9d0e1-f2a3-4567-bcde-678901234567", "uri": "https://files.wellmanager.example.com/wells/pp-01/attachments/nota-fiscal-bomba-2018.pdf", "media_type": "application/pdf", "document_type": "invoice", "filename": "nota-fiscal-bomba-2018.pdf", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } ] } ], "permits": [ { "id": "9a8b7c6d-5e4f-3210-abcd-0123456789ab", "type": "abstraction_permit", "authority": "SEMAS-PA", "identifier": "1234/2025", "request_identifier": "2024/0099-PRT", "status": "granted", "issued_at": "2025-02-10", "valid_until": "2029-02-10", "water_use": ["human_supply"], "flow_rate": 340.0, "daily_operating_time": 20, "volume_limits": [{ "period": "annual", "volume": 2482000 }], "conditions": [ { "id": "c1", "description": "Instalar hidrômetro na saída do poço", "category": "equipment_installation", "due_after": "P90D", "responsible": "Equipe de operação", "fulfillments": [ { "id": "e2f3a4b5-c6d7-8901-efab-23456789abcd", "datetime": "2025-04-02T10:00:00-03:00", "due_date": "2025-05-11", "description": "Hidrômetro instalado e comunicado ao órgão.", "author": "Equipe de operação" } ] }, { "id": "c2", "description": "Relatório semestral de nível e vazão", "category": "reporting", "first_due": "2025-07-31", "recurrence": "P6M" }, { "id": "c3", "description": "Análise anual de qualidade da água", "category": "monitoring", "first_due": "2026-09-30", "recurrence": "P1Y", "fulfillments": [ { "id": "b5c6d7e8-f9a0-1234-bcde-56789abcdef0", "datetime": "2026-09-22T16:00:00-03:00", "due_date": "2026-09-30", "description": "Laudo LD-4471/26 protocolado no órgão gestor.", "sample_id": "ws-2026-09-a" } ] } ], "history": [ { "id": "h-01", "date": "2024-11-04", "type": "filing", "description": "Requerimento de outorga protocolado." }, { "id": "h-02", "date": "2024-12-02", "type": "fee", "description": "Taxa de análise do requerimento.", "due_date": "2024-12-20", "done": true }, { "id": "h-03", "date": "2025-02-10", "type": "decision", "description": "Portaria 1234/2025 publicada." } ], "attachments": [ { "id": "d4e5f6a7-b8c9-0123-defa-456789abcdef", "uri": "https://files.wellmanager.example.com/wells/pp-01/portaria-1234-2025.pdf", "media_type": "application/pdf", "document_type": "permit_document" } ] } ], "meters": [ { "id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "installed_at": "2025-04-02T09:00:00-03:00", "installed_by": "Hidro Serviços", "type": "electromagnetic", "manufacturer": "Incontrol", "model": "VMS Pro", "serial": "EM150-55821", "nominal_diameter": 150, "max_reading": 99999999 } ], "production": [ { "id": "p-0001", "type": "meter_reading", "datetime": "2025-04-02T09:00:00-03:00", "meter_id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "reading": 0, "source": "manual" }, { "id": "p-0002", "type": "meter_reading", "datetime": "2025-12-31T17:00:00-03:00", "meter_id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "reading": 1452300, "source": "telemetry" }, { "id": "p-0003", "type": "estimated_volume", "period_start": "2025-01-01T00:00:00-03:00", "period_end": "2025-04-02T09:00:00-03:00", "volume": 436800, "method": "flow_time", "notes": "Regime r-0001 (300 m³/h × 16 h/dia × 91 dias), antes do hidrômetro." }, { "id": "p-0004", "type": "meter_reading", "datetime": "2026-06-30T17:00:00-03:00", "meter_id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "reading": 2541800, "source": "manual" }, { "id": "p-0005", "type": "meter_reading", "datetime": "2026-06-30T17:00:00-03:00", "meter_id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "reading": 2451800, "source": "manual", "corrects": "p-0004", "notes": "Corrige erro de digitação na leitura p-0004." } ], "operating_regime": [ { "id": "r-0001", "effective_from": "2018-06-12T15:00:00-03:00", "flow_rate": 300, "daily_operating_time": 16, "days_per_week": 7 }, { "id": "r-0002", "effective_from": "2025-04-02T09:00:00-03:00", "flow_rate": 320, "daily_operating_time": 18, "days_per_week": 7, "updated_at": "2025-04-02T12:00:00-03:00" } ], "water_samples": [ { "id": "ws-2026-09-a", "datetime": "2026-09-15T09:30:00-03:00", "sample_type": "routine", "campaign": "2026-Q3", "sampling_method": "low_flow", "sampling_point": { "type": "in_well", "depth": 65, "device": "low_flow_pump" }, "purge": { "duration": 35, "volume": 0.0105, "flow_rate": 0.018, "stabilized": true, "readings": [ { "elapsed": 25, "parameter": { "code": "ph", "vocabulary": "welldot" }, "value": 4.9 }, { "elapsed": 30, "parameter": { "code": "ph", "vocabulary": "welldot" }, "value": 4.8 }, { "elapsed": 35, "parameter": { "code": "ph", "vocabulary": "welldot" }, "value": 4.8 } ] }, "static_level_event_id": "e5f6a7b8-c9d0-1234-efab-567890123456", "collected_by": "Field team A", "chain_of_custody": "CC-0918", "laboratory": { "name": "Lab X", "accreditation": "ISO/IEC 17025 #1234", "report_number": "LD-4471/26", "batch_id": "WO-88213", "sample_id": "88213-01", "received_at": "2026-09-15T18:20:00-03:00", "received_temperature": 4.1 }, "attachments": [ { "id": "a1", "uri": "https://files.wellmanager.example.com/wells/pp-01/LD-4471-26.pdf", "media_type": "application/pdf", "document_type": "lab_report" } ], "results": [ { "parameter": { "code": "ph", "vocabulary": "welldot" }, "value": 4.8, "measured_in": "field" }, { "parameter": { "code": "specific_conductance", "vocabulary": "welldot" }, "value": 1240, "measured_in": "field" }, { "parameter": { "code": "turbidity_fnu", "vocabulary": "welldot" }, "value": 3.1, "measured_in": "field" }, { "parameter": { "code": "sulfate", "vocabulary": "welldot" }, "value": 412, "method": "US EPA 300.0", "measured_in": "lab", "analyzed_at": "2026-09-17T00:00:00-03:00", "analyzed_at_resolution": "day", "validation": { "status": "validated", "validated_by": "QA reviewer" } }, { "parameter": { "code": "iron", "vocabulary": "welldot" }, "value": 18.6, "fraction": "dissolved", "filtration": { "pore_size": 0.45, "location": "field" }, "method": "US EPA 200.8", "measured_in": "lab" }, { "parameter": { "code": "cyanide_wad_as_cn", "vocabulary": "welldot" }, "qualifier": "estimated", "value": 0.004, "detection_limit": 0.002, "quantification_limit": 0.005, "measured_in": "lab", "lab_flags": ["J"], "validation": { "status": "qualified", "qualifier": "J" } }, { "parameter": { "code": "71-43-2", "vocabulary": "cas" }, "qualifier": "<", "value": 0.001, "measured_in": "lab" }, { "parameter": { "code": "e_coli", "vocabulary": "welldot" }, "presence": false, "measured_in": "lab", "analyzed_at": "2026-09-15T19:10:00-03:00" } ] }, { "id": "ws-2026-09-b", "datetime": "2026-09-15T09:35:00-03:00", "sample_type": "field_duplicate", "parent_sample_id": "ws-2026-09-a", "campaign": "2026-Q3", "sampling_point": { "type": "in_well", "depth": 65, "device": "low_flow_pump" }, "results": [ { "parameter": { "code": "sulfate", "vocabulary": "welldot" }, "value": 398, "measured_in": "lab" } ] } ], "history_logs": [ { "id": "d0e1f2a3-b4c5-6789-defa-890123456789", "datetime": "2006-03-10T00:00:00-03:00", "updated_at": "2006-03-10T00:00:00-03:00", "category": "event", "description": "Well construction completed. Commissioned for public supply by COSANPA. Initial flow rate 340 m³/h via air-lift.", "author": "Prefeitura Municipal de Belém" }, { "id": "c3d4e5f6-a7b8-9012-cdef-3456789abcde", "datetime": "2006-03-20T10:00:00-03:00", "category": "status_change", "description": "Poço entra em operação após instalação da bomba.", "status": "active" }, { "id": "a7b8c9d0-e1f2-3456-abcd-567890123456", "datetime": "2018-06-12T09:30:00-03:00", "updated_at": "2024-03-15T11:42:00-03:00", "category": "maintenance", "maintenance_type": "pump_service", "pump_installation_id": "f2a3b4c5-d6e7-8901-fabc-345678901bcd", "description": "Original 15 CV submersible pump replaced after motor burnout. New pump: Schneider ME-25 10 CV. (Description corrected 2024-03-15: model was ME-25, not ME-22 as originally logged.)", "author": "Manutenção Rápida Ltda.", "severity": "high", "attachments": [ { "id": "b8c9d0e1-f2a3-4567-bcde-678901234567", "uri": "https://files.wellmanager.example.com/wells/pp-01/attachments/nota-fiscal-bomba-2018.pdf", "media_type": "application/pdf", "document_type": "invoice", "filename": "nota-fiscal-bomba-2018.pdf", "description": "Purchase invoice for replacement pump", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } ] }, { "id": "f3a4b5c6-d7e8-9012-fabc-3456789abcde", "datetime": "2026-04-10T14:00:00-03:00", "category": "maintenance", "maintenance_type": "meter_calibration", "meter_id": "7c8d9e0f-a1b2-4c3d-8e4f-5a6b7c8d9e0f", "description": "Aferição do medidor eletromagnético; erro dentro da tolerância.", "author": "Metrologia Norte Ltda." }, { "id": "a4b5c6d7-e8f9-0123-abcd-456789abcdef", "datetime": "2026-09-15T09:30:00-03:00", "category": "maintenance", "maintenance_type": "water_sampling", "sample_ids": ["ws-2026-09-a", "ws-2026-09-b"], "hydrodynamic_event_ids": ["e5f6a7b8-c9d0-1234-efab-567890123456"], "description": "Coleta de baixa vazão com duplicata de campo; nível medido antes da coleta.", "author": "Field team A" } ] } ``` --- _`.well` Format Specification v2.4_ # `.well` File Format Specification — Version 2.4: Water Quality **See also:** [overview.md](./overview.md) · [format-reference.md](./format-reference.md) · [object-schemas.md](./object-schemas.md) · [interoperability.md](./interoperability.md) ## Overview 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. ## Table of contents - [`water_samples[]`](#water_samples) - [`WaterSample`](#watersample) - [`SamplingPoint`](#samplingpoint) - [`Purge`](#purge) - [`PurgeReading`](#purgereading) - [`Laboratory`](#laboratory) - [`Result`](#result) - [`Parameter`](#parameter) - [`Filtration`](#filtration) - [`Validation`](#validation) - [Result rules](#result-rules) - [Malformed (rejected)](#malformed-rejected) - [Validation (warnings, never rejection)](#validation-warnings-never-rejection) - [Derived values](#derived-values) - [History log links](#history-log-links) - [Recommended values](#recommended-values) - [`water_samples[].sample_type` — Recommended values _(since v2.3)_](#water_samplessample_type--recommended-values-since-v23) - [`water_samples[].sampling_method` — Recommended values _(since v2.3)_](#water_samplessampling_method--recommended-values-since-v23) - [`water_samples[].sampling_point.type` — Recommended values _(since v2.3)_](#water_samplessampling_pointtype--recommended-values-since-v23) - [`water_samples[].sampling_point.device` — Recommended values _(since v2.3)_](#water_samplessampling_pointdevice--recommended-values-since-v23) - [Units](#units) - [Parameter vocabulary](#parameter-vocabulary) - [Vocabulary rules](#vocabulary-rules) - [Core vocabulary](#core-vocabulary) - [Limit sets are not part of the file](#limit-sets-are-not-part-of-the-file) - [Regulatory profiles](#regulatory-profiles) --- ## `water_samples[]` 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 MAY be edited in place, but editing is NOT RECOMMENDED after the entry has been exported. A revalidation of data already recorded is an edit of `Result.validation` in place. A corrected laboratory report is best written as 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). 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. 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). ### `WaterSample` | Field | Type | Required | Unit | Description | | ----------------------- | ---------------- | ----------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | yes | | Unique within `water_samples`. UUID v4 recommended. | | `datetime` | string (instant) | yes | | RFC 3339 instant of collection. | | `sample_type` | string | yes | | `routine`, `field_duplicate`, `split_sample`, `field_blank`, `trip_blank`, `equipment_blank`, `x-…`. See format-reference.md § `water_samples[].sample_type`. | | `parent_sample_id` | string | conditional | | `water_samples[].id` of the original sample. Expected for `field_duplicate` and `split_sample` (absence emits a warning). | | `sequence` | integer | no | | Tie-breaker among samples at the same instant, lower first. Orders the samples of a vertical profile. | | `campaign` | string | no | | Free identifier of the sampling campaign. | | `sampling_method` | string | no | | How the well was purged: `low_flow`, `volumetric_purge`, `no_purge`, `pump_discharge`, `x-…`. | | `sampling_point` | `SamplingPoint` | no | | Where the water was taken. See below. | | `purge` | `Purge` | no | | Purge before collection. See below. | | `static_level_event_id` | string | no | | `hydrodynamic_events[].id` with the water level measured at collection. | | `collected_by` | string | no | | Person or team that collected the sample. | | `preservation` | string | no | | Preservation and packaging, free text. | | `chain_of_custody` | string | no | | Chain of custody number. | | `laboratory` | `Laboratory` | no | | See below. | | `corrects` | string | no | | `water_samples[].id` of the sample this one retracts. | | `notes` | string | no | | | | `attachments` | `Attachment[]` | no | | The laboratory report with `document_type: "lab_report"`. See § Attachment. | | `results` | `Result[]` | yes | | At least one. Field and laboratory results in one list, distinguished by `measured_in`. | ### `SamplingPoint` | Field | Type | Required | Unit | Description | | ---------------------- | ------ | -------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | yes | | `pump_discharge`, `wellhead_tap`, `in_well`, `x-…`. See format-reference.md § `water_samples[].sampling_point.type`. | | `depth` | number | no | m | Point depth of the sampler intake, from ground level. Mutually exclusive with `from`/`to`. | | `depth_precision` | number | no | m | One-sigma precision of `depth`. | | `from` | number | no | m | Top of the isolated interval. | | `to` | number | no | m | Bottom of the isolated interval. | | `device` | string | no | | What collected the water: `bailer`, `discrete_depth_sampler`, `passive_diffusion_bag`, `grab_sleeve`, `low_flow_pump`, `packer_pump`, `x-…`. | | `pump_installation_id` | string | no | | `pump_installations[].id`, when the sample is taken at the production pump. | A sample's depth is a point, an interval, derived from the production pump, or unknown. All depths are measured from ground level. | Situation | `type` | Geometry | Example | | ----------------- | ---------------- | ------------------------------------------------------------- | ---------------------------------- | | Production pump | `pump_discharge` | `pump_installation_id`; depth derived from its `intake_depth` | Supply well | | Wellhead tap | `wellhead_tap` | None | Routine potability | | Point sampler | `in_well` | `depth` | Bailer, HydraSleeve, diffusion bag | | Low-flow | `in_well` | `depth` of the sampling pump intake | ASTM D6771, ISO 5667-11 | | Isolated interval | `in_well` | `from` and `to` | Packer, multilevel well | | Unknown | `in_well` | None | Historical record without depth | - `depth` together with `from` or `to` is malformed. - With `pump_installation_id`, the depth is not repeated on the sampling point. - Field measurements taken from the top of casing are converted to ground level by the producer, by the same rule as water levels. - A vertical profile or a multilevel well is several samples at different depths, with the same `campaign` and `sequence` for ordering. ### `Purge` All fields optional. | Field | Type | Unit | Description | | ------------ | ---------------- | ---- | --------------------------------------------------- | | `duration` | number | min | Purge duration. | | `volume` | number | m³ | Purged volume. | | `flow_rate` | number | m³/h | Purge flow rate. | | `stabilized` | boolean | | Field parameters were stabilized before collection. | | `readings` | `PurgeReading[]` | | Stabilization readings taken during the purge. | ### `PurgeReading` | Field | Type | Required | Unit | Description | | ----------- | ----------- | -------- | --------- | ---------------------------------------- | | `elapsed` | number | yes | min | Time since the purge started. | | `parameter` | `Parameter` | yes | | `{ code, vocabulary }`, as in `results`. | | `value` | number | yes | canonical | Value in the parameter's canonical unit. | `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"`. ### `Laboratory` | Field | Type | Required | Unit | Description | | ------------------------ | ---------------- | -------- | ---- | ----------------------------------------------------------------------------------------- | | `name` | string | yes | | | | `accreditation` | string | no | | ISO/IEC 17025 (or equivalent) accreditation identifier. | | `report_number` | string | no | | Report number, preserved as issued. | | `batch_id` | string | no | | Laboratory batch or work order. | | `sample_id` | string | no | | The laboratory's own sample identifier. | | `received_at` | string (instant) | no | | When the laboratory received the sample. | | `received_at_resolution` | string | no | | `day` when only the date is known. See format-reference.md § Instants known only by date. | | `received_temperature` | number | no | °C | Sample temperature on receipt. | ### `Result` | Field | Type | Required | Unit | Description | | ------------------------ | ---------------- | --------- | --------- | ----------------------------------------------------------------------------- | | `parameter` | `Parameter` | yes | | What was measured. See below. | | `value` | number | one form | canonical | Numeric result. | | `presence` | boolean | one form | | Presence (`true`) or absence (`false`). | | `text` | string | one form | | Qualitative result (odor, taste). | | `qualifier` | string (enum) | no | | `<`, `>`, `not_detected`, `estimated`. Censoring and estimation only. | | `unit` | string | `x-` only | | UCUM unit. Required for `x-` vocabularies, malformed for `welldot` and `cas`. | | `detection_limit` | number | no | canonical | Detection limit (LD / LOD). | | `quantification_limit` | number | no | canonical | Quantification limit (LQ / LOQ). | | `value_precision` | number | no | canonical | One-sigma uncertainty. Expanded uncertainty from a report is divided by k. | | `fraction` | string (enum) | no | | `total`, `dissolved`, `suspended`. | | `filtration` | `Filtration` | no | | Filtration applied before analysis. Expected with `fraction: "dissolved"`. | | `measured_in` | string (enum) | no | | `field` or `lab`. | | `method` | string | no | | Analytical method, e.g. `ISO 7027`, `US EPA 200.8`, `SMEWW 4500-NO3 B`. | | `analyzed_at` | string (instant) | no | | When the result was analyzed. | | `analyzed_at_resolution` | string | no | | `day` when only the date is known. | | `lab_flags` | string[] | no | | Laboratory flags as issued, uninterpreted. | | `validation` | `Validation` | no | | Data validation by a reviewer. See below. | | `notes` | string | no | | | "Canonical" is the unit of the parameter in the vocabulary, or `unit` for `x-` codes. See § Units. ### `Parameter` | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------------------------------------------------- | | `code` | string | yes | Code in the vocabulary: a `welldot` code, a CAS Registry Number (`71-43-2`) or a custom code. | | `vocabulary` | string | yes | `welldot`, `cas` or `x-…`. Any other value is malformed. | The `welldot` codes, their units, value forms and CAS equivalences are listed in § Parameter vocabulary. ### `Filtration` All fields optional. | Field | Type | Unit | Description | | ----------- | ------------- | ---- | ------------------------------ | | `pore_size` | number | µm | Filter pore size, e.g. `0.45`. | | `location` | string (enum) | | `field` or `lab`. | ### `Validation` | Field | Type | Required | Description | | -------------- | ---------------- | -------- | -------------------------------------------------------------- | | `status` | string (enum) | yes | `unvalidated`, `validated`, `qualified`, `rejected`. | | `qualifier` | string | no | Code of the validation guideline applied, e.g. `J`, `UJ`, `R`. | | `guideline` | string | no | Guideline used, free text. | | `validated_by` | string | no | | | `validated_at` | string (instant) | no | | An absent `validation` is equivalent to `status: "unvalidated"`. ### Result rules Each result has exactly one value form. Three annotation layers are kept apart: the censoring qualifier, the laboratory flags and the reviewer's validation. | Form | Field | Used for | `qualifier` allowed | | ------------ | ---------- | ------------------------------------------------ | -------------------------------------- | | Numeric | `value` | Almost every parameter | `<`, `>`, `estimated` | | Presence | `presence` | Presence codes (`total_coliforms`, `e_coli`) | None | | Text | `text` | Odor, taste and other qualitative results | None | | Not detected | none | Below the detection limit with no reported value | `not_detected`, with `detection_limit` | | Layer | Field | Set by | Example | | ---------------- | ------------ | ----------------------------------- | --------------------------------------------- | | Censoring | `qualifier` | Normalized on import | `<` 0.001 | | Laboratory flags | `lab_flags` | The laboratory, preserved as issued | `B` (blank contamination), `H` (holding time) | | Validation | `validation` | The data reviewer | `qualified` with `J` | - `<` with `value` records the value the laboratory reported, usually the quantification or detection limit. It is neither zero nor absent. - `>` covers upper bounds, such as counts above the method range. - `estimated` marks values between the detection and quantification limits. - `not_detected` without `detection_limit` emits a warning, because the result has no scale. - `value_precision` is one sigma. Reports give an expanded uncertainty (usually k = 2); converters divide it by the coverage factor. - `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. - `lab_flags` has no vocabulary: codes vary by laboratory and country, and interpreting them is the job of validation. - A `rejected` result is excluded from every derivation and from comparisons with limits. `qualified` results are kept. - Revalidating the data of a recorded sample creates a new sample with `corrects`, like any ledger correction. - 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. - A `welldot` code and its CAS equivalent are the same parameter for duplicate detection and derivations. ### Malformed (rejected) - `results` empty or absent. - 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`. - `unit` on a `welldot` or `cas` parameter; an `x-` parameter without `unit`; a `vocabulary` other than `welldot`, `cas` or `x-…`. - `sampling_point.depth` together with `from` or `to`. - An `_resolution` field with a value other than `day`; an instant without an offset. - 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. ### Validation (warnings, never rejection) - `field_duplicate` or `split_sample` without `parent_sample_id`. - `fraction: "dissolved"` without `filtration`. - A sample depth outside every `well_screen` interval (stagnant casing water). - A sample depth below `well_depth`. - A sample depth above the static level of `static_level_event_id`. - `presence` on a numeric code, or `value` on a presence code (value form different from the vocabulary's). - `not_detected` without `detection_limit`. - The same parameter, `fraction` and `measured_in` repeated in one sample. - A `welldot` code unknown to the vocabulary version. - `analyzed_at` or `laboratory.received_at` earlier than the collection `datetime`. - A reference that does not resolve: `corrects`, `parent_sample_id`, `static_level_event_id`, `sampling_point.pump_installation_id`. - A `corrects` cycle. ### Derived values Derived values are computed by `@welldot/utils` and never stored. Retracted samples and `rejected` results are excluded from all of them. - 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. - Ion balance, converting mg/L to meq/L with the molar mass and charge of each code. - Relative percent difference (RPD) between a duplicate or split and its original sample, per parameter. - Blank contamination: parameters detected in `field_blank`, `trip_blank` or `equipment_blank` samples of the same `campaign`. - Holding time, from `datetime` to `analyzed_at`, at the available resolution. - Receipt temperature compliance. - Purge stabilization from `purge.readings`. - Acid drainage indicators (net alkalinity, sulfate/chloride ratio). - Piper and Stiff coordinates and the hydrochemical facies. - 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). ### History log links 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. --- ## Recommended values Non-canonical values of these open vocabularies SHOULD use the `x-` prefix. ### `water_samples[].sample_type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Description | | ----------------- | ------------------------ | ------------------------------------------------------------------------------------- | | `routine` | Amostra de rotina | Regular sample of the well water. | | `field_duplicate` | Duplicata de campo | Second sample collected at the same time and point. Requires `parent_sample_id`. | | `split_sample` | Amostra dividida (split) | One sample split between laboratories (interlaboratory). Requires `parent_sample_id`. | | `field_blank` | Branco de campo | Analyte-free water exposed to field conditions. | | `trip_blank` | Branco de transporte | Analyte-free water that travels with the samples, unopened. | | `equipment_blank` | Branco de equipamento | Analyte-free water passed through the sampling equipment after decontamination. | Non-canonical values SHOULD use the `x-` prefix. Laboratory internal QC (method blanks, matrix spikes) is out of scope. ### `water_samples[].sampling_method` — Recommended values _(since v2.3)_ `sampling_method` describes the purge; `sampling_point.device` describes what collected the water. Portuguese terms follow ABNT NBR 15847 (métodos de purga). | Value | Portuguese (BR) | Description | | ------------------ | --------------------------------- | --------------------------------------------------------------------------------------- | | `low_flow` | Purga de baixa vazão (micropurga) | Low-flow purging with stabilization of field parameters (e.g. ASTM D6771, ISO 5667-11). | | `volumetric_purge` | Purga de volume determinado | Removal of a set number of well volumes before sampling. | | `no_purge` | Amostragem sem purga | Passive or no-purge sampling. | | `pump_discharge` | Bomba de produção em operação | Collected from the running production pump. | Non-canonical values SHOULD use the `x-` prefix. ### `water_samples[].sampling_point.type` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | Description | | ---------------- | -------------------------- | --------------------------------------------------------------------------- | | `pump_discharge` | Saída da bomba de produção | Production pump discharge; depth derived from `pump_installation_id`. | | `wellhead_tap` | Torneira do cavalete | Tap at the wellhead; no depth. | | `in_well` | Dentro do poço | Sampler inside the well, at `depth`, over `from`/`to`, or at unknown depth. | Non-canonical values SHOULD use the `x-` prefix. ### `water_samples[].sampling_point.device` — Recommended values _(since v2.3)_ | Value | Portuguese (BR) | | ------------------------ | ------------------------------------------ | | `bailer` | Bailer (amostrador de retenção) | | `discrete_depth_sampler` | Amostrador pontual de profundidade | | `passive_diffusion_bag` | Bolsa de difusão passiva | | `grab_sleeve` | Amostrador passivo tipo luva (HydraSleeve) | | `low_flow_pump` | Bomba de baixa vazão | | `packer_pump` | Bomba com obturador (packer) | Non-canonical values SHOULD use the `x-` prefix. --- ## Units A water quality result never declares a unit for a known parameter: the unit comes from the parameter code. 1. **Substances** (vocabulary `welldot` or `cas`) are always in mg/L. µg/L, ng/L and g/L are converted on import. 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. 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. 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. Regional unit names are normalized on import: | Reported unit | Normalized to | | -------------------- | ------------------------------------------------- | | µg/L, ng/L, g/L | mg/L | | mS/m | µS/cm (×10) | | mS/cm | µS/cm (×1000) | | uT, FTU | generic `turbidity` code | | NTU, FNU, FAU | `turbidity_ntu`, `turbidity_fnu`, `turbidity_fau` | | mg Pt-Co/L, PCU, TCU | uH (numerically equal) | 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. --- ## Parameter vocabulary A parameter is identified by an object `{ code, vocabulary }`, like `texture`: ```json { "code": "nitrate_as_n", "vocabulary": "welldot" } { "code": "71-43-2", "vocabulary": "cas" } { "code": "glyphosate_ampa", "vocabulary": "x-lab-codes" } ``` `vocabulary` is `welldot`, `cas` or an `x-…` vocabulary; any other value is malformed. ### Vocabulary rules 1. **Substances** (`welldot` or `cas`) are always in mg/L. Conversions such as µg/L → mg/L happen on import. 2. **Parameters that are not concentrations** have their unit fixed by the `welldot` vocabulary (pH, conductivity, temperature, redox potential, turbidity, color, microbiology, radioactivity). 3. **`x-` codes** require `unit`, in UCUM. This is the only exception to the spec's unit rule. 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. 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. 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`). 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. 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. ### Core vocabulary 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. | Group | Code | Label (EN) | Portuguese (BR) | Unit | Form | CAS | | ---------------- | --------------------------------- | ----------------------------------------------------- | ------------------------------------------- | ---------- | -------- | ---------- | | Physical / field | `temperature` | Temperature | Temperatura | °C | value | | | Physical / field | `ph` | pH | pH | — | value | | | Physical / field | `specific_conductance` | Specific conductance at 25 °C | Condutividade elétrica a 25 °C | µS/cm | value | | | Physical / field | `conductivity_uncompensated` | Electrical conductivity, uncompensated | Condutividade sem compensação | µS/cm | value | | | Physical / field | `dissolved_oxygen` | Dissolved oxygen | Oxigênio dissolvido | mg/L | value | | | Physical / field | `orp` | Oxidation-reduction potential vs. reference electrode | Potencial redox vs. eletrodo de referência | mV | value | | | Physical / field | `eh` | Redox potential vs. SHE | Potencial redox corrigido vs. EPH | mV | value | | | Physical / field | `turbidity` | Turbidity, formazin, method unspecified | Turbidez, formazina sem método identificado | FTU | value | | | Physical / field | `turbidity_ntu` | Turbidity, nephelometric white light | Turbidez nefelométrica, luz branca | NTU | value | | | Physical / field | `turbidity_fnu` | Turbidity, nephelometric infrared (ISO 7027) | Turbidez nefelométrica, infravermelho | FNU | value | | | Physical / field | `turbidity_fau` | Turbidity, attenuation (ISO 7027) | Turbidez por atenuação | FAU | value | | | Physical / field | `apparent_color` | Apparent color | Cor aparente | uH | value | | | Physical / field | `true_color` | True color | Cor verdadeira | uH | value | | | Physical / field | `odor` | Odor | Odor | — | text | | | Physical / field | `taste` | Taste | Gosto | — | text | | | Physical / field | `total_dissolved_solids` | Total dissolved solids | Sólidos totais dissolvidos | mg/L | value | | | Physical / field | `total_suspended_solids` | Total suspended solids | Sólidos suspensos totais | mg/L | value | | | Physical / field | `total_solids` | Total solids | Sólidos totais | mg/L | value | | | Physical / field | `free_co2` | Free carbon dioxide | Gás carbônico livre | mg/L | value | | | Aggregate | `alkalinity_total_as_caco3` | Total alkalinity as CaCO₃ | Alcalinidade total como CaCO₃ | mg/L | value | | | Aggregate | `alkalinity_bicarbonate_as_caco3` | Bicarbonate alkalinity as CaCO₃ | Alcalinidade de bicarbonatos como CaCO₃ | mg/L | value | | | Aggregate | `alkalinity_carbonate_as_caco3` | Carbonate alkalinity as CaCO₃ | Alcalinidade de carbonatos como CaCO₃ | mg/L | value | | | Aggregate | `alkalinity_hydroxide_as_caco3` | Hydroxide alkalinity as CaCO₃ | Alcalinidade de hidróxidos como CaCO₃ | mg/L | value | | | Aggregate | `acidity_total_as_caco3` | Total acidity as CaCO₃ | Acidez total como CaCO₃ | mg/L | value | | | Aggregate | `hardness_total_as_caco3` | Total hardness as CaCO₃ | Dureza total como CaCO₃ | mg/L | value | | | Aggregate | `hardness_calcium_as_caco3` | Calcium hardness as CaCO₃ | Dureza de cálcio como CaCO₃ | mg/L | value | | | Major ion | `calcium` | Calcium | Cálcio | mg/L | value | 7440-70-2 | | Major ion | `magnesium` | Magnesium | Magnésio | mg/L | value | 7439-95-4 | | Major ion | `sodium` | Sodium | Sódio | mg/L | value | 7440-23-5 | | Major ion | `potassium` | Potassium | Potássio | mg/L | value | 7440-09-7 | | Major ion | `bicarbonate` | Bicarbonate as HCO₃⁻ | Bicarbonato como HCO₃⁻ | mg/L | value | 71-52-3 | | Major ion | `carbonate` | Carbonate as CO₃²⁻ | Carbonato como CO₃²⁻ | mg/L | value | 3812-32-6 | | Major ion | `chloride` | Chloride | Cloreto | mg/L | value | 16887-00-6 | | Major ion | `sulfate` | Sulfate | Sulfato | mg/L | value | 14808-79-8 | | Major ion | `fluoride` | Fluoride | Fluoreto | mg/L | value | 16984-48-8 | | Major ion | `silica_as_sio2` | Silica as SiO₂ | Sílica como SiO₂ | mg/L | value | 7631-86-9 | | Nutrient | `nitrate_as_n` | Nitrate as N | Nitrato como N | mg/L | value | | | Nutrient | `nitrate_as_no3` | Nitrate as NO₃⁻ | Nitrato como NO₃⁻ | mg/L | value | 14797-55-8 | | Nutrient | `nitrite_as_n` | Nitrite as N | Nitrito como N | mg/L | value | | | Nutrient | `nitrite_as_no2` | Nitrite as NO₂⁻ | Nitrito como NO₂⁻ | mg/L | value | 14797-65-0 | | Nutrient | `ammonia_as_n` | Ammonia nitrogen as N | Nitrogênio amoniacal como N | mg/L | value | | | Nutrient | `ammonia_as_nh3` | Ammonia as NH₃ | Amônia como NH₃ | mg/L | value | 7664-41-7 | | Nutrient | `kjeldahl_nitrogen_as_n` | Total Kjeldahl nitrogen as N | Nitrogênio Kjeldahl total como N | mg/L | value | | | Nutrient | `phosphorus_total_as_p` | Total phosphorus as P | Fósforo total como P | mg/L | value | | | Nutrient | `orthophosphate_as_po4` | Orthophosphate as PO₄³⁻ | Ortofosfato como PO₄³⁻ | mg/L | value | | | Organic | `total_organic_carbon` | Total organic carbon | Carbono orgânico total | mg/L | value | | | Organic | `cod` | Chemical oxygen demand | Demanda química de oxigênio (DQO) | mg/L | value | | | Organic | `bod5` | Biochemical oxygen demand, 5-day | Demanda bioquímica de oxigênio (DBO₅) | mg/L | value | | | Organic | `total_petroleum_hydrocarbons` | Total petroleum hydrocarbons (range per method) | Hidrocarbonetos totais de petróleo | mg/L | value | | | Organic | `oil_and_grease` | Oil and grease | Óleos e graxas | mg/L | value | | | Disinfection | `free_chlorine` | Free chlorine residual | Cloro residual livre | mg/L | value | | | Disinfection | `total_chlorine` | Total chlorine residual | Cloro residual total | mg/L | value | | | Mining and redox | `ferrous_iron` | Ferrous iron (Fe²⁺) | Ferro ferroso (Fe²⁺) | mg/L | value | | | Mining and redox | `chromium_hexavalent` | Hexavalent chromium | Cromo hexavalente | mg/L | value | 18540-29-9 | | Mining and redox | `cyanide_total_as_cn` | Total cyanide as CN⁻ | Cianeto total como CN⁻ | mg/L | value | | | Mining and redox | `cyanide_wad_as_cn` | Weak acid dissociable cyanide as CN⁻ | Cianeto WAD como CN⁻ | mg/L | value | | | Mining and redox | `cyanide_free_as_cn` | Free cyanide as CN⁻ | Cianeto livre como CN⁻ | mg/L | value | | | Mining and redox | `thiocyanate` | Thiocyanate | Tiocianato | mg/L | value | 302-04-5 | | Mining and redox | `sulfide_total_as_s` | Total sulfide as S | Sulfeto total como S | mg/L | value | | | Metal / trace | `iron` | Iron | Ferro | mg/L | value | 7439-89-6 | | Metal / trace | `manganese` | Manganese | Manganês | mg/L | value | 7439-96-5 | | Metal / trace | `aluminum` | Aluminium | Alumínio | mg/L | value | 7429-90-5 | | Metal / trace | `antimony` | Antimony | Antimônio | mg/L | value | 7440-36-0 | | Metal / trace | `arsenic` | Arsenic | Arsênio | mg/L | value | 7440-38-2 | | Metal / trace | `barium` | Barium | Bário | mg/L | value | 7440-39-3 | | Metal / trace | `beryllium` | Beryllium | Berílio | mg/L | value | 7440-41-7 | | Metal / trace | `boron` | Boron | Boro | mg/L | value | 7440-42-8 | | Metal / trace | `cadmium` | Cadmium | Cádmio | mg/L | value | 7440-43-9 | | Metal / trace | `chromium` | Chromium, total | Cromo total | mg/L | value | 7440-47-3 | | Metal / trace | `cobalt` | Cobalt | Cobalto | mg/L | value | 7440-48-4 | | Metal / trace | `copper` | Copper | Cobre | mg/L | value | 7440-50-8 | | Metal / trace | `lead` | Lead | Chumbo | mg/L | value | 7439-92-1 | | Metal / trace | `lithium` | Lithium | Lítio | mg/L | value | 7439-93-2 | | Metal / trace | `mercury` | Mercury | Mercúrio | mg/L | value | 7439-97-6 | | Metal / trace | `molybdenum` | Molybdenum | Molibdênio | mg/L | value | 7439-98-7 | | Metal / trace | `nickel` | Nickel | Níquel | mg/L | value | 7440-02-0 | | Metal / trace | `selenium` | Selenium | Selênio | mg/L | value | 7782-49-2 | | Metal / trace | `silver` | Silver | Prata | mg/L | value | 7440-22-4 | | Metal / trace | `strontium` | Strontium | Estrôncio | mg/L | value | 7440-24-6 | | Metal / trace | `thallium` | Thallium | Tálio | mg/L | value | 7440-28-0 | | Metal / trace | `uranium` | Uranium | Urânio | mg/L | value | 7440-61-1 | | Metal / trace | `vanadium` | Vanadium | Vanádio | mg/L | value | 7440-62-2 | | Metal / trace | `zinc` | Zinc | Zinco | mg/L | value | 7440-66-6 | | Microbiology | `total_coliforms` | Total coliforms in 100 mL | Coliformes totais em 100 mL | — | presence | | | Microbiology | `total_coliforms_mpn` | Total coliforms, count | Coliformes totais, contagem | MPN/100 mL | value | | | Microbiology | `total_coliforms_cfu` | Total coliforms, count | Coliformes totais, contagem | CFU/100 mL | value | | | Microbiology | `e_coli` | _E. coli_ in 100 mL | _E. coli_ em 100 mL | — | presence | | | Microbiology | `e_coli_mpn` | _E. coli_, count | _E. coli_, contagem | MPN/100 mL | value | | | Microbiology | `e_coli_cfu` | _E. coli_, count | _E. coli_, contagem | CFU/100 mL | value | | | Microbiology | `thermotolerant_coliforms` | Thermotolerant coliforms in 100 mL | Coliformes termotolerantes em 100 mL | — | presence | | | Microbiology | `thermotolerant_coliforms_mpn` | Thermotolerant coliforms, count | Coliformes termotolerantes, contagem | MPN/100 mL | value | | | Microbiology | `thermotolerant_coliforms_cfu` | Thermotolerant coliforms, count | Coliformes termotolerantes, contagem | CFU/100 mL | value | | | Microbiology | `heterotrophic_plate_count` | Heterotrophic plate count | Bactérias heterotróficas | CFU/mL | value | | | Radioactivity | `gross_alpha` | Gross alpha activity | Atividade alfa total | Bq/L | value | | | Radioactivity | `gross_beta` | Gross beta activity | Atividade beta total | Bq/L | value | | | Radioactivity | `radium_226` | Radium-226 | Rádio-226 | Bq/L | value | 13982-63-3 | | Radioactivity | `radium_228` | Radium-228 | Rádio-228 | Bq/L | value | 15262-20-1 | | Radioactivity | `radon_222` | Radon-222 | Radônio-222 | Bq/L | value | 14859-67-7 | 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). ### Limit sets are not part of the file 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. --- ## Regulatory profiles 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`: - 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`); - monthly `e_coli` and `total_coliforms` results (presence form) and half-yearly results for the full list; - `laboratory.accreditation` on every sample and an attachment with `document_type: "lab_report"`. ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.org/profiles/drinking-water/v1/schema.json", "allOf": [{ "$ref": "https://welldot.org/schema/v2/well.schema.json" }], "required": ["water_samples"], "properties": { "water_samples": { "items": { "required": ["laboratory", "attachments"], "properties": { "laboratory": { "required": ["name", "accreditation"] } } } } } } ``` 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. # `.well` File Format Specification **Version:** 1.0 **Extension:** `.well` **Encoding:** UTF-8 **Base format:** JSON ## Overview This is the specification of version 1 of the `.well` format: one JSON file that holds the static record of one water well, its identity, construction (borehole, casing, screens, fills, cement pad) and geology (lithology, fractures, caves). v1 is stable and kept for reference. New files use version 2, described in [../v2/overview.md](../v2/overview.md); `deserializeWell()` in `@welldot/core` reads v1 files and normalizes them to v2. ## Table of contents - [Purpose](#purpose) - [Design Principles](#design-principles) - [File Structure](#file-structure) - [Top-level Fields](#top-level-fields) - [`well_type` — Recommended values](#well_type--recommended-values) - [Object Schemas](#object-schemas) - [`BoreHole` — `bore_hole[]`](#borehole--bore_hole) - [`WellCase` — `well_case[]`](#wellcase--well_case) - [`Reduction` — `reduction[]`](#reduction--reduction) - [`WellScreen` — `well_screen[]`](#wellscreen--well_screen) - [`SurfaceCase` — `surface_case[]`](#surfacecase--surface_case) - [`HoleFill` — `hole_fill[]`](#holefill--hole_fill) - [`CementPad` — `cement_pad`](#cementpad--cement_pad) - [`Lithology` — `lithology[]`](#lithology--lithology) - [`Fracture` — `fractures[]`](#fracture--fractures) - [`Cave` — `caves[]`](#cave--caves) - [Complete Example](#complete-example) - [Versioning](#versioning) - [MIME Type](#mime-type) --- ## Purpose The `.well` format is an open standard for representing water well data. It is designed to serve three distinct but complementary use cases: **Visualization.** The format contains sufficient constructive and geological detail to produce accurate technical well profile drawings — including borehole geometry, casing strings, screens, gravel packs, lithological columns, fractures, and caves — at true depth scale. **Registration.** The format captures the administrative and physical identity of a well, providing a complete static record suitable for submission to regulatory bodies, inclusion in national well registries, and long-term archival. **Research.** The structured geological and constructive data enables cross-well analysis, aquifer characterization, and hydrogeological research. Field vocabulary choices (FGDC texture codes, geologic units, aquifer units) are aligned with standards used in scientific literature. Dynamic hydrological measurements — static level, dynamic level, pump tests, water quality — are outside the scope of v1 and are reserved for a future `hydro` extension block. --- ## Design Principles - **Self-describing** — full field names are used throughout; a `.well` file can be read and understood without consulting the spec - **Versioned** — includes a `version` field for forward compatibility; parsers must reject unrecognized versions - **Self-contained** — a single file encodes the complete static record of one well - **SI units** — all depths are in meters, all diameters in millimeters; applications are responsible for unit conversion on display - **Geographic north** — all azimuth values are referenced to geographic north - **Ground level as zero** — all depth values are measured from ground level (0); elevation above sea level is stored separately in the `elevation` field --- ## File Structure A `.well` file is a JSON object with the following top-level structure: ```json { "version": 1, "well_type": "tubular", "name": "Poço PP-01", "well_driller": "Perfuradora XYZ", "construction_date": "2024-03-15", "lat": -1.4558, "lng": -48.5039, "elevation": 12.5, "obs": "Observações livres", "bore_hole": [ ... ], "well_case": [ ... ], "reduction": [ ... ], "well_screen": [ ... ], "surface_case": [ ... ], "hole_fill": [ ... ], "cement_pad": { ... }, "lithology": [ ... ], "fractures": [ ... ], "caves": [ ... ] } ``` --- ## Top-level Fields | Field | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------- | | `version` | integer | yes | Format version. Must be `1` for this spec. | | `well_type` | string | yes | Classification of the well. See vocabulary below. | | `name` | string | no | Well name or local identifier. | | `well_driller` | string | no | Name of the drilling company or individual. | | `construction_date` | string | no | ISO 8601 date of well completion (YYYY-MM-DD). | | `lat` | number | no | Latitude in decimal degrees (WGS84). | | `lng` | number | no | Longitude in decimal degrees (WGS84). | | `elevation` | number | no | Ground elevation above sea level in meters. | | `obs` | string | no | Free-text observations about the well. | ### `well_type` — Recommended values | Value | Description | | ---------------------- | ---------------------------------------------- | | `tubular` | Conventional rotary or percussion drilled well | | `artesian` | Free-flowing artesian well | | `hand_dug` | Manually excavated well or cistern | | `horizontal` | Horizontal or sub-horizontal well | | `infiltration_gallery` | Horizontal subsurface infiltration gallery | The `well_type` field accepts any string. The values above are the recommended vocabulary for interoperability. Tools should treat unrecognized values gracefully. --- ## Object Schemas ### `BoreHole` — `bore_hole[]` Represents a drilled interval of the borehole. Multiple entries describe a telescoping borehole with different diameters at different depth ranges. | Field | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diameter` | number | yes | Borehole diameter in millimeters. | | `drilling_method` | string | no | Method used (e.g. `rotary`, `percussion`, `cable_tool`). | ```json { "from": 0, "to": 80, "diameter": 250, "drilling_method": "rotary" } ``` --- ### `WellCase` — `well_case[]` Steel or plastic casing installed inside the borehole. | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Casing material. Recommended: `steel`, `pvc`, `hdpe`, `fiberglass`. | | `diameter` | number | yes | Casing outer diameter in millimeters. | ```json { "from": 0, "to": 60, "type": "steel", "diameter": 200 } ``` --- ### `Reduction` — `reduction[]` A transition piece connecting two casing or screen sections of different diameters. | Field | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diam_from` | number | yes | Diameter at the top of the reducer in mm. | | `diam_to` | number | yes | Diameter at the bottom of the reducer in mm. | | `type` | string | yes | Reducer type (e.g. `conical`, `stepped`). | ```json { "from": 58, "to": 60, "diam_from": 200, "diam_to": 150, "type": "conical" } ``` --- ### `WellScreen` — `well_screen[]` Slotted or wire-wound screen section that allows water to enter the well. | Field | Type | Required | Description | | ---------------- | ------ | -------- | --------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Screen type. Recommended: `wire_wound`, `bridge_slot`, `louvered`, `pvc_slotted`. | | `diameter` | number | yes | Screen outer diameter in millimeters. | | `screen_slot_mm` | number | yes | Slot opening size in millimeters. | ```json { "from": 60, "to": 80, "type": "wire_wound", "diameter": 150, "screen_slot_mm": 0.5 } ``` --- ### `SurfaceCase` — `surface_case[]` Outer protective casing installed near the surface, typically to prevent surface contamination. | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `diameter` | number | yes | Casing outer diameter in millimeters. | ```json { "from": 0, "to": 3, "diameter": 300 } ``` --- ### `HoleFill` — `hole_fill[]` Material placed in the annular space between the casing and the borehole wall. | Field | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `type` | string | yes | Either `gravel_pack` or `seal`. | | `diameter` | number | yes | Outer diameter of the filled annulus in mm. | | `description` | string | yes | Material description (e.g. grain size, material). | ```json { "from": 60, "to": 80, "type": "gravel_pack", "diameter": 250, "description": "Seixo 2-4mm" } ``` --- ### `CementPad` — `cement_pad` The concrete pad installed at ground level (depth 0) around the wellhead. Dimensions are in meters. | Field | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------- | | `type` | string | yes | Pad shape (e.g. `square`, `rectangular`, `circular`). | | `width` | number | yes | Width in meters. | | `thickness` | number | yes | Thickness in meters. | | `length` | number | yes | Length in meters. | ```json { "type": "square", "width": 1.0, "thickness": 0.15, "length": 1.0 } ``` --- ### `Lithology` — `lithology[]` Geological description of a depth interval. | Field | Type | Required | Description | | --------------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `description` | string | yes | Free-text geological description. | | `color` | string | yes | Representative color as a CSS hex value. | | `fgdc_texture` | string | yes | Texture code per the FGDC Digital Cartographic Standard for Geologic Map Symbolization. | | `geologic_unit` | string | yes | Name of the stratigraphic or geologic unit. | | `aquifer_unit` | string | yes | Aquifer classification (e.g. `freático`, `confinado`, `fraturado`). | ```json { "from": 0, "to": 20, "description": "Areia fina amarelada", "color": "#f5deb3", "fgdc_texture": "sand", "geologic_unit": "Quaternário", "aquifer_unit": "freático" } ``` --- ### `Fracture` — `fractures[]` A discrete fracture or fracture zone intersected by the borehole. | Field | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------------- | | `depth` | number | yes | Depth of the fracture in meters. | | `water_intake` | boolean | yes | Whether the fracture produces water. | | `description` | string | yes | Free-text description. | | `swarm` | boolean | yes | Whether this fracture belongs to a swarm (closely spaced set). | | `azimuth` | number | yes | Azimuth in degrees from geographic north (0–360). | | `dip` | number | yes | Dip angle in degrees from horizontal (0–90). | ```json { "depth": 45.2, "water_intake": true, "description": "Fratura aberta", "swarm": false, "azimuth": 120, "dip": 35 } ``` --- ### `Cave` — `caves[]` A cavity or void zone intersected by the borehole. | Field | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------- | | `from` | number | yes | Start depth in meters. | | `to` | number | yes | End depth in meters. | | `water_intake` | boolean | yes | Whether the cave produces water. | | `description` | string | yes | Free-text description. | ```json { "from": 50, "to": 52, "water_intake": false, "description": "Caverna seca" } ``` --- ## Complete Example ```json { "version": 1, "well_type": "tubular", "name": "Poço PP-01", "well_driller": "Perfuradora XYZ", "construction_date": "2024-03-15", "lat": -1.4558, "lng": -48.5039, "elevation": 12.5, "obs": "Sem anomalias observadas durante a perfuração.", "bore_hole": [ { "from": 0, "to": 80, "diameter": 250, "drilling_method": "rotary" } ], "well_case": [{ "from": 0, "to": 60, "type": "steel", "diameter": 200 }], "reduction": [ { "from": 58, "to": 60, "diam_from": 200, "diam_to": 150, "type": "conical" } ], "well_screen": [ { "from": 60, "to": 80, "type": "wire_wound", "diameter": 150, "screen_slot_mm": 0.5 } ], "surface_case": [{ "from": 0, "to": 3, "diameter": 300 }], "hole_fill": [ { "from": 60, "to": 80, "type": "gravel_pack", "diameter": 250, "description": "Seixo 2-4mm" }, { "from": 3, "to": 60, "type": "seal", "diameter": 250, "description": "Cimento" } ], "cement_pad": { "type": "square", "width": 1.0, "thickness": 0.15, "length": 1.0 }, "lithology": [ { "from": 0, "to": 20, "description": "Areia fina amarelada", "color": "#f5deb3", "fgdc_texture": "sand", "geologic_unit": "Quaternário", "aquifer_unit": "freático" }, { "from": 20, "to": 80, "description": "Granito fraturado cinza", "color": "#a9a9a9", "fgdc_texture": "granite", "geologic_unit": "Embasamento Cristalino", "aquifer_unit": "fraturado" } ], "fractures": [ { "depth": 45.2, "water_intake": true, "description": "Fratura aberta", "swarm": false, "azimuth": 120, "dip": 35 }, { "depth": 62.0, "water_intake": false, "description": "Fratura de cisalhamento", "swarm": true, "azimuth": 90, "dip": 70 } ], "caves": [ { "from": 50, "to": 52, "water_intake": false, "description": "Caverna seca" } ] } ``` --- ## Versioning The `version` field must always be present and is an integer starting at `1`. Parsers must reject files with an unrecognized `version` value and should warn when `version` is absent. | Version | Description | | ------- | ----------------------------- | | `1` | Initial release of the format | A `hydro` block for dynamic hydrological data (static level, dynamic level, pump tests, water quality) is reserved for v2 and must not appear in v1 files. --- ## MIME Type ``` application/vnd.well+json ``` --- _Specification v1.0 — `.well` format_ # FGDC Texture Patterns Reference ## Overview This document lists all texture patterns available in `FGDC_TEXTURES_OPTIONS` from `@welldot/core`. These codes follow the **FGDC (Federal Geographic Data Committee)** digital cartographic standard for geologic map symbolization. Use a `code` value in the `texture` field of a `Lithology` record to assign a pattern to a depth interval. ```typescript import { FGDC_TEXTURES_OPTIONS } from '@welldot/core'; // Find a texture by code const limestone = FGDC_TEXTURES_OPTIONS.find(t => t.code === 627); // { code: 627, label: 'Limestone' } // Filter to only textures with a rendered image const available = FGDC_TEXTURES_OPTIONS.filter(t => !t.pending); ``` > **Texture availability:** Some entries carry `pending: true`, meaning the SVG pattern image has not been implemented yet. Textures without this flag are fully rendered and ready to use. The flag is transitional and will be removed once all patterns are complete. > > Currently **120 of 282 textures** are available: all of Series 600 (84) and Series 700 (33), plus codes 120, 123, and 132 from Series 100. The patterns are grouped by FGDC series below. Each table lists the code and label of every pattern, and the summary at the end gives the available and pending counts per series. ## Table of contents - [Series 100 — Surficial Patterns](#series-100--surficial-patterns) - [Series 200 — Sedimentary Patterns](#series-200--sedimentary-patterns) - [Series 300 — Igneous Patterns](#series-300--igneous-patterns) - [Series 400 — Miscellaneous and Metamorphic Patterns](#series-400--miscellaneous-and-metamorphic-patterns) - [Series 500 — Glacial and Periglacial Patterns](#series-500--glacial-and-periglacial-patterns) - [Series 600 — Sedimentary Lithology Patterns](#series-600--sedimentary-lithology-patterns) - [Series 700 — Metamorphic and Igneous Lithology Patterns](#series-700--metamorphic-and-igneous-lithology-patterns) - [Summary](#summary) --- ## Series 100 — Surficial Patterns | Code | Label | Status | | ---- | -------------------------- | --------- | | 101 | Unconsolidated material | pending | | 102 | Alluvium | pending | | 103 | Terrace deposit | pending | | 104 | Floodplain deposit | pending | | 105 | Fan or pediment deposit | pending | | 106 | Eolian sand | pending | | 107 | Silt or loess | pending | | 108 | Clay deposit | pending | | 109 | Colluvium | pending | | 110 | Gravel deposit | pending | | 111 | Residual deposit | pending | | 112 | Playa deposit | pending | | 113 | Lacustrine deposit | pending | | 114 | Marine deposit | pending | | 115 | Beach or bar deposit | pending | | 116 | Organic deposit or peat | pending | | 117 | Swamp or marsh deposit | pending | | 118 | Soil | pending | | 119 | Fill or artificial deposit | pending | | 120 | Surficial pattern 120 | available | | 121 | Surficial pattern 121 | pending | | 122 | Surficial pattern 122 | pending | | 123 | Surficial pattern 123 | available | | 124 | Surficial pattern 124 | pending | | 125 | Surficial pattern 125 | pending | | 126 | Surficial pattern 126 | pending | | 127 | Surficial pattern 127 | pending | | 128 | Surficial pattern 128 | pending | | 129 | Surficial pattern 129 | pending | | 130 | Surficial pattern 130 | pending | | 131 | Surficial pattern 131 | pending | | 132 | Surficial pattern 132 | available | | 133 | Surficial pattern 133 | pending | | 134 | Surficial pattern 134 | pending | | 135 | Surficial pattern 135 | pending | | 136 | Surficial pattern 136 | pending | | 137 | Surficial pattern 137 | pending | --- ## Series 200 — Sedimentary Patterns > All entries in this series are **pending** (no SVG images implemented yet). | Code | Label | | ---- | ----------------------- | | 201 | Sedimentary pattern 201 | | 202 | Sedimentary pattern 202 | | 203 | Sedimentary pattern 203 | | 204 | Sedimentary pattern 204 | | 205 | Sedimentary pattern 205 | | 206 | Sedimentary pattern 206 | | 207 | Sedimentary pattern 207 | | 208 | Sedimentary pattern 208 | | 209 | Sedimentary pattern 209 | | 210 | Sedimentary pattern 210 | | 211 | Sedimentary pattern 211 | | 212 | Sedimentary pattern 212 | | 213 | Sedimentary pattern 213 | | 214 | Sedimentary pattern 214 | | 215 | Sedimentary pattern 215 | | 216 | Sedimentary pattern 216 | | 217 | Sedimentary pattern 217 | | 218 | Sedimentary pattern 218 | | 219 | Sedimentary pattern 219 | | 220 | Sedimentary pattern 220 | | 221 | Sedimentary pattern 221 | | 222 | Sedimentary pattern 222 | | 223 | Sedimentary pattern 223 | | 224 | Sedimentary pattern 224 | | 225 | Sedimentary pattern 225 | | 226 | Sedimentary pattern 226 | | 227 | Sedimentary pattern 227 | | 228 | Sedimentary pattern 228 | | 229 | Sedimentary pattern 229 | | 230 | Sedimentary pattern 230 | | 231 | Sedimentary pattern 231 | | 232 | Sedimentary pattern 232 | | 233 | Sedimentary pattern 233 | --- ## Series 300 — Igneous Patterns > All entries in this series are **pending** (no SVG images implemented yet). | Code | Label | | ---- | ------------------- | | 301 | Igneous pattern 301 | | 302 | Igneous pattern 302 | | 303 | Igneous pattern 303 | | 304 | Igneous pattern 304 | | 305 | Igneous pattern 305 | | 306 | Igneous pattern 306 | | 307 | Igneous pattern 307 | | 308 | Igneous pattern 308 | | 309 | Igneous pattern 309 | | 310 | Igneous pattern 310 | | 311 | Igneous pattern 311 | | 312 | Igneous pattern 312 | | 313 | Igneous pattern 313 | | 314 | Igneous pattern 314 | | 315 | Igneous pattern 315 | | 316 | Igneous pattern 316 | | 317 | Igneous pattern 317 | | 318 | Igneous pattern 318 | | 319 | Igneous pattern 319 | | 320 | Igneous pattern 320 | | 321 | Igneous pattern 321 | | 322 | Igneous pattern 322 | | 323 | Igneous pattern 323 | | 324 | Igneous pattern 324 | | 325 | Igneous pattern 325 | | 326 | Igneous pattern 326 | | 327 | Igneous pattern 327 | | 328 | Igneous pattern 328 | | 329 | Igneous pattern 329 | | 330 | Igneous pattern 330 | | 331 | Igneous pattern 331 | --- ## Series 400 — Miscellaneous and Metamorphic Patterns > All entries in this series are **pending** (no SVG images implemented yet). | Code | Label | | ---- | ------------------------- | | 401 | Miscellaneous pattern 401 | | 402 | Miscellaneous pattern 402 | | 403 | Miscellaneous pattern 403 | | 404 | Miscellaneous pattern 404 | | 405 | Miscellaneous pattern 405 | | 406 | Miscellaneous pattern 406 | | 407 | Miscellaneous pattern 407 | | 408 | Miscellaneous pattern 408 | | 409 | Miscellaneous pattern 409 | | 410 | Miscellaneous pattern 410 | | 411 | Miscellaneous pattern 411 | | 412 | Miscellaneous pattern 412 | | 413 | Miscellaneous pattern 413 | | 414 | Miscellaneous pattern 414 | | 415 | Miscellaneous pattern 415 | | 416 | Miscellaneous pattern 416 | | 417 | Miscellaneous pattern 417 | | 418 | Miscellaneous pattern 418 | | 419 | Miscellaneous pattern 419 | | 420 | Miscellaneous pattern 420 | | 421 | Miscellaneous pattern 421 | | 422 | Miscellaneous pattern 422 | | 423 | Miscellaneous pattern 423 | | 424 | Miscellaneous pattern 424 | | 425 | Miscellaneous pattern 425 | | 426 | Miscellaneous pattern 426 | | 427 | Miscellaneous pattern 427 | | 428 | Miscellaneous pattern 428 | | 429 | Miscellaneous pattern 429 | | 430 | Miscellaneous pattern 430 | | 431 | Miscellaneous pattern 431 | | 432 | Miscellaneous pattern 432 | | 433 | Miscellaneous pattern 433 | | 434 | Miscellaneous pattern 434 | | 435 | Miscellaneous pattern 435 | | 436 | Miscellaneous pattern 436 | --- ## Series 500 — Glacial and Periglacial Patterns > All entries in this series are **pending** (no SVG images implemented yet). | Code | Label | | ---- | ----------------------- | | 521 | Glacial pattern 521 | | 522 | Glacial pattern 522 | | 523 | Glacial pattern 523 | | 524 | Glacial pattern 524 | | 530 | Glacial pattern 530 | | 531 | Glacial pattern 531 | | 532 | Glacial pattern 532 | | 533 | Glacial pattern 533 | | 534 | Glacial pattern 534 | | 535 | Glacial pattern 535 | | 536 | Glacial pattern 536 | | 537 | Glacial pattern 537 | | 538 | Glacial pattern 538 | | 580 | Glacial pattern 580 | | 581 | Glacial pattern 581 | | 582 | Glacial pattern 582 | | 583 | Glacial pattern 583 | | 584 | Glacial pattern 584 | | 585 | Glacial pattern 585 | | 586 | Glacial pattern 586 | | 587 | Glacial pattern 587 | | 588 | Glacial pattern 588 | | 589 | Glacial pattern 589 | | 591 | Periglacial pattern 591 | | 592 | Periglacial pattern 592 | | 593 | Periglacial pattern 593 | | 594 | Periglacial pattern 594 | | 595 | Periglacial pattern 595 | --- ## Series 600 — Sedimentary Lithology Patterns These are the most commonly used codes for well lithology logging. They represent specific rock and sediment types encountered in boreholes. | Code | Label | | ---- | ---------------------------------------------------------------- | | 601 | Gravel or conglomerate (1st option) | | 602 | Gravel or conglomerate (2nd option) | | 603 | Crossbedded gravel or conglomerate | | 605 | Breccia (1st option) | | 606 | Breccia (2nd option) | | 607 | Massive sand or sandstone | | 608 | Bedded sand or sandstone | | 609 | Crossbedded sand or sandstone (1st option) | | 610 | Crossbedded sand or sandstone (2nd option) | | 611 | Ripple-bedded sand or sandstone | | 612 | Argillaceous or shaly sandstone | | 613 | Calcareous sandstone | | 614 | Dolomitic sandstone | | 616 | Silt, siltstone, or shaly silt | | 617 | Calcareous siltstone | | 618 | Dolomitic siltstone | | 619 | Sandy or silty shale | | 620 | Clay or clay shale | | 621 | Cherty shale | | 622 | Dolomitic shale | | 623 | Calcareous shale or marl | | 624 | Carbonaceous shale | | 625 | Oil shale | | 626 | Chalk | | 627 | Limestone | | 628 | Clastic limestone | | 629 | Fossiliferous clastic limestone | | 630 | Nodular or irregularly bedded limestone | | 631 | Limestone, irregular (burrow?) fillings of saccharoidal dolomite | | 632 | Crossbedded limestone | | 633 | Cherty crossbedded limestone | | 634 | Cherty and sandy crossbedded clastic limestone | | 635 | Oolitic limestone | | 636 | Sandy limestone | | 637 | Silty limestone | | 638 | Argillaceous or shaly limestone | | 639 | Cherty limestone (1st option) | | 640 | Cherty limestone (2nd option) | | 641 | Dolomitic limestone, limy dolostone, or limy dolomite | | 642 | Dolostone or dolomite | | 643 | Crossbedded dolostone or dolomite | | 644 | Oolitic dolostone or dolomite | | 645 | Sandy dolostone or dolomite | | 646 | Silty dolostone or dolomite | | 647 | Argillaceous or shaly dolostone or dolomite | | 648 | Cherty dolostone or dolomite | | 649 | Bedded chert (1st option) | | 650 | Bedded chert (2nd option) | | 651 | Fossiliferous bedded chert | | 652 | Fossiliferous rock | | 653 | Diatomaceous rock | | 654 | Subgraywacke | | 655 | Crossbedded subgraywacke | | 656 | Ripple-bedded subgraywacke | | 657 | Peat | | 658 | Coal | | 659 | Bony coal or impure coal | | 660 | Underclay | | 661 | Flint clay | | 662 | Bentonite | | 663 | Glauconite | | 664 | Limonite | | 665 | Siderite | | 666 | Phosphatic-nodular rock | | 667 | Gypsum | | 668 | Salt | | 669 | Interbedded sandstone and siltstone | | 670 | Interbedded sandstone and shale | | 671 | Interbedded ripple-bedded sandstone and shale | | 672 | Interbedded shale and silty limestone (shale dominant) | | 673 | Interbedded shale and limestone, shale dominant (1st option) | | 674 | Interbedded shale and limestone, shale dominant (2nd option) | | 675 | Interbedded calcareous shale and limestone (shale dominant) | | 676 | Interbedded silty limestone and shale | | 677 | Interbedded limestone and shale (1st option) | | 678 | Interbedded limestone and shale (2nd option) | | 679 | Interbedded limestone and shale (limestone dominant) | | 680 | Interbedded limestone and calcareous shale | | 681 | Till or diamicton (1st option) | | 682 | Till or diamicton (2nd option) | | 683 | Till or diamicton (3rd option) | | 684 | Loess (1st option) | | 685 | Loess (2nd option) | | 686 | Loess (3rd option) | --- ## Series 700 — Metamorphic and Igneous Lithology Patterns | Code | Label | | ---- | -------------------------------- | | 701 | Metamorphism | | 702 | Quartzite | | 703 | Slate | | 704 | Schistose or gneissoid granite | | 705 | Schist | | 706 | Contorted schist | | 707 | Schist and gneiss | | 708 | Gneiss | | 709 | Contorted gneiss | | 710 | Soapstone, talc, or serpentinite | | 711 | Tuffaceous rock | | 712 | Crystal tuff | | 713 | Devitrified tuff | | 714 | Volcanic breccia and tuff | | 715 | Volcanic breccia or agglomerate | | 716 | Zeolitic rock | | 717 | Basaltic flows | | 718 | Granite (1st option) | | 719 | Granite (2nd option) | | 720 | Banded igneous rock | | 721 | Igneous rock (1st option) | | 722 | Igneous rock (2nd option) | | 723 | Igneous rock (3rd option) | | 724 | Igneous rock (4th option) | | 725 | Igneous rock (5th option) | | 726 | Igneous rock (6th option) | | 727 | Igneous rock (7th option) | | 728 | Igneous rock (8th option) | | 729 | Porphyritic rock (1st option) | | 730 | Porphyritic rock (2nd option) | | 731 | Vitrophyre | | 732 | Quartz | | 733 | Ore | --- ## Summary | Series | Category | Total | Available | Pending | | --------- | ------------------------------- | ------- | --------- | ------- | | 100 | Surficial | 37 | 3 | 34 | | 200 | Sedimentary | 33 | 0 | 33 | | 300 | Igneous | 31 | 0 | 31 | | 400 | Miscellaneous / Metamorphic | 36 | 0 | 36 | | 500 | Glacial / Periglacial | 28 | 0 | 28 | | 600 | Sedimentary Lithology | 84 | 84 | 0 | | 700 | Metamorphic / Igneous Lithology | 33 | 33 | 0 | | **Total** | | **282** | **120** | **162** | --- _Source: FGDC Digital Cartographic Standard for Geologic Map Symbolization (FGDC-STD-013-2006), Chapter 25 — Lithology._ # Contributing > How to contribute to welldot, from reporting a problem with the docs to proposing a change to the .well format, and where each kind of change lives. Welldot is open source (Apache 2.0) and developed in the open on [GitHub](https://github.com/welldothq/welldot). Contributions of every size help: a typo fix, a clearer example, a missing vocabulary value, a converter for another registry. ## Where things live | You want to change | Where | Start with | | ------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | A rule of the `.well` format | `spec/` (plain Markdown) | An issue describing the need ([Extending the format](/format/guides/extending#propose-a-change-to-the-format)) | | A package's behavior or API | `packages/core`, `packages/utils`, `packages/render` | An issue or a pull request with tests and a changeset | | A docs page (guides, concepts, getting started) | `apps/docs/content/en/` | A pull request; use **Edit this page** at the side of any page | | The site itself (layout, components, MCP server) | `apps/docs/` | An issue first for anything large | | The conversion skill | `.agents/skills/welldot-converter/` | A pull request | The repository's [CONTRIBUTING.md](https://github.com/welldothq/welldot/blob/main/CONTRIBUTING.md) covers setup, branches, commits and releases. ## Changes that touch the docs A change to the spec or to a package's public API usually needs a docs change in the same pull request. [Update rules](/contributing/update-rules) says exactly what, and CI checks the parts that can be checked: examples must validate, links and spec anchors must resolve, and the site must build. ## Writing docs - [Writing docs](/contributing/writing-docs): the writing rules, how to add a page and how to run the site locally. - [Content components](/contributing/components): callouts, Do/Don't blocks, code groups, diagrams and live well previews, with their Markdown. ## Reporting a problem Found a page that is wrong, unclear or out of date? Open an [issue](https://github.com/welldothq/welldot/issues) with the page address and what you expected. For a mistake in the specification itself, say which section and quote the text. # Content components This page is the reference for writers. Every block below is shown as it renders, followed by the Markdown (MDC) that produces it. Prefer these over raw HTML. ## Callouts Four kinds, from the mildest to the strongest. Keep them short: one or two sentences. ::note A `.well` file is plain JSON. Any text editor can open it. :: ::tip Start from an example in `packages/core/examples` instead of an empty file. :: ::warning Depths are in metres from the ground surface. A negative `from` means above ground. :: ::caution Do not edit files in `spec/` to suit the site. The spec is canonical. :: ```mdc ::note A `.well` file is plain JSON. Any text editor can open it. :: ``` `::tip`, `::warning` and `::caution` take the same form. ## Do and Don't Pair a recommended form with the one to avoid. Each side takes text and an optional fenced block. ::do-dont #do Give depths as numbers, in metres. ```json { "from": 0, "to": 12.5 } ``` #dont Put units inside the value. ```json { "from": "0 m", "to": "12.5 m" } ``` :: ````mdc ::do-dont #do Give depths as numbers, in metres. ```json { "from": 0, "to": 12.5 } ``` #dont Put units inside the value. ```json { "from": "0 m", "to": "12.5 m" } ``` :: ```` ## Code blocks and code groups A fence takes a language and an optional `[filename]`. Group alternatives, such as package managers, with `::code-group`. The bracketed name becomes the tab label. ::code-group ```bash [pnpm] pnpm add @welldot/core ``` ```bash [npm] npm i @welldot/core ``` ```bash [yarn] yarn add @welldot/core ``` :: ````mdc ::code-group ```bash [pnpm] pnpm add @welldot/core ``` ```bash [npm] npm i @welldot/core ``` :: ```` ### Fence languages - `well`: a complete `.well` document. CI validates every `well` fence with `parseWell`, so it must be valid as written. It is highlighted as JSON. - `json`: a fragment, such as a single object or a field. Not validated. - `ts`, `bash`, `vue`, `yaml`, `md`: as usual. ```ts [read.ts] import { parseWell } from '@welldot/core'; const well = parseWell(await file.text()); console.log(well.bore_hole.length); ``` ## Tabs Use tabs for content that differs by context but not by language (code alternatives go in a code group). ::tabs :::tabs-item{label="Browser" icon="i-lucide-globe"} Open [welldot.org](https://welldot.org) and drop the file on the editor. ::: :::tabs-item{label="Node" icon="i-lucide-terminal"} Read the file and call `parseWell` from `@welldot/core`. ::: :: ```mdc ::tabs :::tabs-item{label="Browser" icon="i-lucide-globe"} Open welldot.org and drop the file on the editor. ::: :::tabs-item{label="Node" icon="i-lucide-terminal"} Read the file and call `parseWell` from `@welldot/core`. ::: :: ``` ## Steps Number a procedure by wrapping its `###` headings in `::steps`. ::steps ### Install the package Add `@welldot/core` to your project. ### Parse the file `parseWell` validates the JSON and upgrades a v1 file to v2. ### Use the data Every field is typed; see the format reference. :: ```mdc ::steps ### Install the package Add `@welldot/core` to your project. ### Parse the file `parseWell` validates the JSON and upgrades a v1 file to v2. :: ``` ## Diagrams A `mermaid` fence renders as a diagram that follows the light and dark theme. ```mermaid flowchart LR A[.well file] --> B{parseWell} B -->|valid| C[Well object] B -->|invalid| D[ZodError with paths] C --> E[WellRenderer] C --> F["@welldot/utils"] ``` ````mdc ```mermaid flowchart LR A[.well file] --> B{parseWell} B -->|valid| C[Well object] B -->|invalid| D[ZodError with paths] ``` ```` ## Live well preview Draws a profile with `@welldot/render`, next to its source. It runs in the browser only. ### From a bundled example `example` is the name of a file in `packages/core/examples`, without `.well`. ::well-preview{example="irece-ba-carste-bambui"} :: ```mdc ::well-preview{example="irece-ba-carste-bambui"} :: ``` ### From an inline document Put a complete document in a `well` fence inside the component. ::well-preview ```well { "version": "2.4", "name": "Demo well, sandstone aquifer", "well_type": "tubular", "bore_hole": [ { "from": 0, "to": 12, "diameter": 304.8 }, { "from": 12, "to": 60, "diameter": 254 } ], "surface_case": [{ "from": 0, "to": 12, "diameter": 254 }], "well_case": [{ "from": -0.5, "to": 36, "type": "pvc", "diameter": 152.4 }], "reduction": [], "well_screen": [ { "from": 36, "to": 54, "type": "pvc", "diameter": 152.4, "screen_slot": 0.75 } ], "hole_fill": [ { "from": 0, "to": 12, "type": "seal", "diameter": 254, "description": "Cement seal" }, { "from": 12, "to": 60, "type": "gravel_pack", "diameter": 254, "description": "Gravel pack, 2–4 mm" } ], "lithology": [ { "from": 0, "to": 8, "description": "Red clayey soil", "color": "#9c5a3c", "texture": { "code": 620, "vocabulary": "fgdc" }, "geologic_unit": "Cover", "aquifer_unit": "Aquitard" }, { "from": 8, "to": 30, "description": "Silty sandstone", "color": "#c9a27a", "texture": { "code": 612, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" }, { "from": 30, "to": 60, "description": "Medium-grained sandstone", "color": "#e0c48f", "texture": { "code": 607, "vocabulary": "fgdc" }, "geologic_unit": "Botucatu Formation", "aquifer_unit": "Guarani" } ], "fractures": [], "caves": [] } ``` :: ````mdc ::well-preview ```well { "version": "2.4", "name": "…", … } ``` :: ```` ### Invalid input When the source does not validate, the preview lists the errors with their paths instead of drawing. This snippet is incomplete on purpose, so it uses a `json` fence, which CI does not validate. ::well-preview ```json { "version": "2.4", "name": "Half-written well", "bore_hole": [{ "from": 0, "to": "sixty", "diameter": 254 }] } ``` :: # Update rules > Which code and spec changes need a docs change in the same pull request, what CI checks automatically, and the checklist to run at the end of every change to the spec or a published package. The docs stay correct only if they change together with what they describe. These rules apply to everyone: maintainers, outside contributors and AI assistants working in the repository. The same rules are written for Claude in [`apps/docs/CLAUDE.md`](https://github.com/welldothq/welldot/blob/main/apps/docs/CLAUDE.md), and the pull request template links here. ## What triggers a docs change | Change | Docs work, in the same pull request | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | Spec change (new field, enum value, rule, version, deprecation) | Edit the spec file in `spec/` first, then the guides and examples that depend on it. | | Public API change in a package | The TSDoc in the source (the API reference follows) and every guide that uses the API. | | Behavior change with no API change | The guide that describes the behavior. | | Release | Nothing by hand. Changelogs come from changesets. | | Internal refactor | Nothing. | | English page edited | Nothing in Portuguese. The CI warning lists stale PT pages; the on-page "behind" notice comes with the PT release. | When to edit each spec file, and what to update in it, is described in [`packages/core/CLAUDE.md`](https://github.com/welldothq/welldot/blob/main/packages/core/CLAUDE.md) ("Documentation requirements") and [`spec/README.md`](https://github.com/welldothq/welldot/blob/main/spec/README.md). Spec files stay plain GitHub-readable Markdown: no site components or syntax. ## Checks in CI The `docs — Check` workflow runs on pull requests that touch `apps/docs/`, `spec/`, `packages/` or `.changeset/`. | Check | Command | Effect | | ---------------------------------------------------------------------------------------- | --------------------------------------- | --------------------- | | Every `.well` snippet and example in the docs validates with `@welldot/core` | `pnpm --filter docs check:examples` | Blocking | | Internal links and anchors resolve, including links into spec headings | `pnpm --filter docs check:links` | Blocking | | The docs site builds | `pnpm --filter docs generate` | Blocking | | The core tests that compare the spec with the code (`vocab.test.ts`, `examples.test.ts`) | `pnpm --filter @welldot/core test` | Blocking (CI runs it) | | A changeset for a published package with no change under `apps/docs/` or `spec/` | `pnpm --filter docs check:touched` | Warning only | | Portuguese pages behind their English page (once they exist) | `pnpm --filter docs check:translations` | Warning only | A renamed spec heading therefore fails the build instead of silently breaking the guides that link to it. Many changes need no docs, which is why the changeset check only warns. ## End-of-change checklist Run through this at the end of any change that touches the spec or a published package: ::steps ### Did a rule change? Edit the spec file first, as plain Markdown. ### Which docs pages explain or depend on the change? Update them, linking to the spec instead of restating it. Search `apps/docs/content` for the field, function or rule name. ### Do the examples still validate? Run `pnpm --filter docs check:examples`, and the `@welldot/core` tests if examples in `packages/core/examples` changed. ### Did an English page change? Its Portuguese page, when there is one, shows up in the CI warning for stale translations. Nothing else is needed unless you are asked to translate. ### Is there a changeset? Add one for every published package you changed (`pnpm changeset`). The changelog page follows from it. :: ## In the pull request The pull request template has three boxes for this: - [ ] Spec updated first, if a rule changed - [ ] Docs pages that depend on the change updated (or none needed) - [ ] Examples validate (`check:examples`) and links resolve (`check:links`) # Writing docs > The writing rules for docs.welldot.org, how pages are organized and ordered, how to add a page, which pages are generated, and how to run and check the site locally. ## Who reads these docs - **Geologists and hydrogeologists**, often reading English as a second language. They need to know what each field means in the field. - **Developers** building tools, databases and editors on the format. They need code that works against the current packages. - **AI agents** working for either group, reading the same pages as Markdown. Write for all three: plain sentences, one idea each, no marketing language, no jargon a field geologist would not use unless it is explained. ## The rules 1. **Link to the spec, don't restate it.** The specification in `spec/` is the only place a rule is stated. A page that depends on a rule explains it in plain words and links to the spec heading, for example `[level sign convention](/format/spec/v2/format-reference#level-sign-convention)`. If a guide needs a rule the spec lacks, the spec gets the rule first. 2. **Full examples use `well` fences.** Every complete `.well` document is a ` ```well ` fence or a `::well-preview{example=""}` block, so CI validates it. Fragments and deliberately invalid snippets use `json` fences. 3. **Two entry points.** Geologist-facing pages (getting started, concepts) explain field meaning without code. Developer pages show code, checked against the current package APIs. 4. **Frontmatter.** Every page has a `title` and a `description`. The description is used in search results, `llms.txt` and SEO, so make it a full sentence that says what the page covers. 5. **No duplicate title.** The page header shows `title`. Omit a leading `# Heading` in the body, or make it identical to `title`. 6. **Sentence case** for titles and headings: "Getting started", not "Getting Started". 7. **Code that runs.** Run new snippets against the packages before publishing. State the output only when you have seen it. The [content components](/contributing/components) page lists the blocks you can use: callouts (`::note`, `::tip`, `::warning`, `::caution`), `::code-group`, `::tabs`, `::steps`, `::do-dont`, `mermaid` diagrams and `::well-preview`. ::warning `check:examples` only reads a `::well-preview{example="…"}` attribute written on one line. Keep the whole opening line together. :: ## Links and anchors - Link site pages by path: `/format/concepts/units-and-precision`. - Spec pages are at `/format/spec/v2/`, `/format/spec/reference/fgdc-textures` and `/format/spec/v1/well-format`. Heading anchors are the ones GitHub generates, so ``## `bore_hole[].drilling_method` `` becomes `#bore_holedrilling_method--recommended-values-since-v23`. Copy them from the rendered page or GitHub; do not guess. - Object pages: `/format/reference/objects/` (for example `bore-hole`). API pages: `//api//`. Changelogs: `//changelog`. - The JSON Schema is linked at `https://welldot.org/schema/v2/well.schema.json`, never copied. - Repository files are linked on GitHub: `https://github.com/welldothq/welldot/blob/main/`. `check:links` fails the build on a broken link or anchor, including links into the spec. ## Adding a page Pages live in `apps/docs/content/en/` and their path is their route. Numeric prefixes set the sidebar order and are dropped from the route: `format/2.concepts/3.vocabularies.md` is `/format/concepts/vocabularies`. ```text content/en/ index.md the landing (layout: landing) format/ the .well format section index.md section home 1.getting-started/ 2.concepts/ 3.guides/ 4.reference/ 5.spec/ 6.changelog.md core/ utils/ render/ one section per package index.md 1.getting-started/ 2.guides/ (api/ is generated) ai/ contributing/ ``` 1. Create the file in the right folder, with a numeric prefix for its position. 2. Add `title` and `description` frontmatter. `navigation.title` sets a shorter sidebar label. 3. Write the page following the rules above. 4. Run the checks below. A directory without an `index.md` appears in the sidebar as a group named after the folder. ## Generated pages Never edit these by hand; they are rebuilt on every `dev`, `build` and `generate` and are not committed: - `content/en//api/**`: the API reference, generated from the TSDoc in the package sources by TypeDoc. To change it, edit the TSDoc. - `content/en/format/reference/objects/**`: the object reference, generated from the JSON Schema. To change it, change the Zod schemas in `@welldot/core` (and the spec). The spec pages under `/format/spec` and the package changelogs are read straight from `spec/` and `packages/*/CHANGELOG.md`. ## Run the site locally The docs need Node 22.5 or later (they use Node's built-in SQLite). ```bash pnpm install pnpm turbo build --filter=docs^... # build the packages the docs read pnpm --filter docs dev # http://localhost:3001 ``` Before opening a pull request: ```bash pnpm --filter docs check:examples # every .well snippet validates pnpm --filter docs generate # the site builds pnpm --filter docs check:links # every link and anchor resolves ``` ## Translations This release is English only. Pages are organized so a Portuguese edition can be added under `content/pt/` later; an English edit never waits for a translation, and a translation that falls behind is flagged automatically. # @welldot/core > The TypeScript reference implementation of the .well format, with types, Zod validators, serialization, unit conversion, vocabularies and water quality data. `@welldot/core` is the reference implementation of the [`.well` format](/format). It gives TypeScript and JavaScript programs everything needed to read, check and write `.well` files, with no DOM and no network access. ::code-group ```bash [pnpm] pnpm add @welldot/core ``` ```bash [npm] npm install @welldot/core ``` ```bash [yarn] yarn add @welldot/core ``` :: ```ts import { parseWell, serializeWell } from '@welldot/core'; const well = parseWell(text); // validated and typed; v1 files are upgraded well.name = 'PT-01'; const out = serializeWell(well); // "version": "2.4", unknown members kept ``` ## What it contains | Area | Exports | Guide | | ------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | Types | `Well` and every object type (`BoreHole`, `Lithology`, `HydrodynamicEvent`, `Permit`, `WaterSample`, …) | [API reference](/core/api) | | Validation | `parseWell`, `WellSchema` and one Zod schema per object | [Parsing and validating](/core/guides/parsing-and-validating) | | Serialization | `serializeWell`, `deserializeWell`, `redactWell`, `isWellEmpty`, `hasWellData` | [Serializing and sharing](/core/guides/serializing-and-sharing) | | Units | Conversions between the canonical SI units and display units | [Unit conversion](/core/guides/unit-conversion) | | Vocabularies | Every recommended and closed vocabulary with English and Portuguese labels, and the FGDC textures | [Vocabularies and textures](/core/guides/vocabularies-and-textures) | | Water quality | The 98-parameter vocabulary and the WHO, EU and Brazilian limit sets | [Water quality vocabulary](/core/guides/water-quality-vocabulary) | | Versions | `WELL_FORMAT_VERSION`, version parsing and comparison, time zone checks | [Versions and time zones](/core/guides/versions-and-time-zones) | | Data | The JSON Schema (`@welldot/core/well.schema.json`) and 19 example wells (`@welldot/core/examples/*`) | [Example wells](/format/reference/examples) | ## Relationship to the spec The package follows the [specification](/format/spec) and is tested against it: the vocabulary constants are compared with the spec's tables, and the spec's complete example must parse. The [JSON Schema](/format/reference/json-schema) is generated from its Zod validators. The current release, 0.5.0, implements format revision 2.4. ## Next - [Installation](/core/getting-started/installation) - [First use](/core/getting-started/first-use): read, inspect, change and save a well. - [API reference](/core/api), generated from the source. - [Changelog](/core/changelog) # @welldot/core ## 0.6.0 ### Minor Changes - 147708f: The `.well` specification moved from `packages/core/docs/` to `spec/` at the repository root, and the spec prose no longer ships in the npm package. The JSON Schema's `$id` is now `https://welldot.org/schema/v2/well.schema.json` (served by welldot.org); the old `raw.githubusercontent.com/.../packages/core/docs/schema/v2/well.schema.json` URL no longer resolves. The schema ships in the package as `@welldot/core/well.schema.json`. ### Patch Changes - 0c76546: The READMEs now link to the documentation site at `https://docs.welldot.org`. - 0332665: The repository moved to the `welldothq` GitHub organization. The package `repository`, `homepage` and `bugs` URLs now point at `https://github.com/welldothq/welldot`. ## 0.5.0 ### Minor Changes - a74ddd6: Implements the `.well` v2.4 format revision. **Breaking for readers:** `.well` v2.4 files write `"version": "2.4"`. Parsers in `@welldot/core` ≤ 0.4.0 reject them; upgrade readers before writing v2.4 files. - **Version as a string.** `serializeWell` writes `"version": "2.4"`. `parseWell` and `deserializeWell` accept `1`, `2` and any `"2.m"`, and reject malformed values (`2.4` as a number, `"2.4.1"`, `"v2.4"`) and unsupported majors with errors that name the value. Adds `WELL_FORMAT_VERSION`, `parseFormatVersion` and `compareFormatVersions` (versions compare as two integers, so `"2.10"` is greater than `"2.9"`). - **`null` in optional fields is malformed.** `storativity` no longer accepts `null`; v2.3 files (`version: 2`) with `storativity: null` still parse, with the value dropped. `serializeWell` omits null members outside `x-` fields. - **Time zone.** Adds `location.properties.timezone` (an IANA time zone name) and `isIanaTimeZone`. An invalid value is rejected. - **`logger_series`.** Adds the hydrodynamic event type for continuous level series recorded by an installed sensor. It carries `readings` (each with a required `datetime` and no `elapsed`), plus optional `measurement_method`, `sensor_depth`, `interval`, `barometric_compensation` (`barologger`, `vented`, `none`) and `aggregation` (`none`, `hourly`, `daily`). Aggregated readings must carry `depth_min` and `depth_max`. A `logger_series` has no `steps` or `recovery`. New exports: `LoggerSeriesEvent`, `LoggerReading`, `BarometricCompensation`, `ReadingAggregation`, `LoggerSeriesEventSchema`, `LoggerReadingSchema`, and the closed vocabularies `BAROMETRIC_COMPENSATIONS` and `READING_AGGREGATIONS` with their `*_VALUES` lists. `HYDRODYNAMIC_EVENT_TYPES` gains `logger_series`. `LevelReading` gains an optional `datetime`. - **Event validation.** Known hydrodynamic event types are now validated against their own schema instead of falling back to the `x-` passthrough. A malformed `constant_rate`, `spot_measurement` or other known type is rejected, while `x-` types still pass through. The step count is no longer enforced: `steps` must be an array of valid `PumpingStep`, but a `constant_rate` or `spot_measurement` with two steps is valid. As a result, the `ConstantRateEvent.steps` type changes from `[PumpingStep]` to `PumpingStep[]`. - **Ledgers and range rules.** Ledger entries (`hydrodynamic_events`, `production`, `water_samples`) MAY be edited in place; editing is NOT RECOMMENDED after export, and `corrects` stays the recommended way to correct measurements. `operating_regime[].daily_operating_time` above 24 is now accepted and stored as written (a negative value is still a schema error). `operating_regime[].effective_from` is no longer unique. Unresolved references are preserved on re-export, and repeated records (collective permits, campaign blanks) are documented. - The spec docs, the JSON Schema and the example wells move to v2.4. ## 0.4.0 ### Minor Changes - Implement `.well` spec v2.3: pump installations and attachments, permits (history and condition fulfillments), meters, production and operating regime, and the water quality block. - Link multiple events and samples to any history log entry, and drop the `change_of_use` history category in favor of `event`. - Add `installed_by` and `removed_by` to pump installations and meters, and manufacturer, model and attachments to meters. - Rename the permit condition category values to broader groups. - Add `well_screen[].type` slot types. - Centralize the open vocabularies with en/pt labels, and move shared helpers into the library. - Add example wells. - Allow hiding individual fields when sharing. - Keep `x-` fields and blocks through parse and serialize. - Breaking: replace the production `declared_volume` with `estimated_volume`. - Breaking: remove the `purpose` field from meters and `declared_volume`. ## 0.3.0 ### Minor Changes - Add `.well` spec v2.1: `well_purpose` and centralizers. ## 0.2.3 ### Patch Changes - Polish open-source docs, licensing and schema tooling. ## 0.2.2 ### Patch Changes - Version bump released together with `@welldot/utils` and `@welldot/render`. ## 0.2.1 ### Patch Changes - Add the optional `well_depth` field for the current or usable well depth. - Add section visibility types and the `redactWell` utility. ## 0.2.0 ### Minor Changes - Implement the `.well` v2 schema with hydrodynamic events, units utilities and a v1 to v2 migration. Spec v2.0 is ratified and ships with this release. - Add a pending flag to textures, FGDC texture pattern types and options. - Add max flow rate and more steps to airlift tests. ## 0.1.1-5 ### Patch Changes - Add the pending flag to textures and filter unavailable options. - Add FGDC texture pattern types and options. - Fix the package exports. ## 0.1.1-4 ### Patch Changes - Pre-release of the 0.1.1 version bump. ## 0.1.1-3 ### Patch Changes - Pre-release of the 0.1.1 version bump. ## 0.1.1-2 ### Patch Changes - Pre-release of the 0.1.1 version bump. ## 0.1.1 ### Patch Changes - Bump the package version. ## 0.1.0 ### Minor Changes - Initial release. # First use > Read a .well file with @welldot/core, handle validation errors, inspect the typed data, change it and save it again. This page reads a well, looks at a few fields, changes one and writes the file back. It runs in Node 18 or later; in a browser, replace the file system calls with a `File` or `fetch`. ## Read and validate ```ts [read.ts] import { readFile } from 'node:fs/promises'; import { parseWell, type Well } from '@welldot/core'; const text = await readFile('PT-01.well', 'utf8'); const well: Well = parseWell(text); ``` `parseWell` takes the JSON text (not an already parsed object), validates it against the schema and returns a typed `Well`. A file with `"version": 1` is upgraded to v2 first. It throws when: - the text is not JSON (a `SyntaxError` from `JSON.parse`); - the version is malformed or unsupported (an `Error` naming the value); - the document does not match the schema (a `ZodError` listing every problem). ```ts type Issue = { path: (string | number)[]; message: string }; try { parseWell(text); } catch (error) { if (error instanceof Error && error.name === 'ZodError') { for (const issue of (error as Error & { issues: Issue[] }).issues) { console.log(issue.path.join('.'), issue.message); // well_screen.0.screen_slot Required } } else { console.log((error as Error).message); } } ``` Checking `name` rather than `instanceof ZodError` avoids importing `zod` yourself and keeps working if your project resolves a different copy of it. [Parsing and validating](/core/guides/parsing-and-validating) shows how to turn these into readable messages. ## Inspect the data Every block is typed. The construction and geology arrays are always present; the other blocks are optional. ```ts console.log(well.name, well.well_type); for (const layer of well.lithology) { console.log(`${layer.from}–${layer.to} m ${layer.description}`); } const screens = well.well_screen.map(s => `${s.from}–${s.to} m`); const tests = (well.hydrodynamic_events ?? []).filter( e => e.type === 'constant_rate' || e.type === 'step_drawdown', ); ``` `HydrodynamicEvent` is a union discriminated by `type`, so narrowing on `type` gives you the fields of that event kind. For derived values, such as the latest static level, gravel pack volume or permit status, use [`@welldot/utils`](/utils). ## Change and save ```ts [save.ts] import { writeFile } from 'node:fs/promises'; import { serializeWell } from '@welldot/core'; well.well_purpose = ['production', 'monitoring']; await writeFile('PT-01.well', serializeWell(well), 'utf8'); ``` `serializeWell` writes `"version": "2.4"`, leaves out empty optional fields and keeps every member it does not know, including `x-` extensions, as the spec requires ([Serializing and sharing](/core/guides/serializing-and-sharing)). ## Show labels and convert units Values are stored as keys and in SI units. Convert for display: ```ts import { CONSTRUCTION_MATERIALS, getVocabLabel, metersToFeet, mmToInches, } from '@welldot/core'; const casing = well.well_case[0]; getVocabLabel(CONSTRUCTION_MATERIALS, casing.type, 'pt'); // 'Aço carbono' mmToInches(casing.diameter); // 8.62… for 219 mm metersToFeet(casing.to); ``` ## Next - [Parsing and validating](/core/guides/parsing-and-validating) - [Vocabularies and textures](/core/guides/vocabularies-and-textures) - [API reference](/core/api) # Installation > Install @welldot/core with npm, pnpm or yarn, check the requirements, and find the JSON Schema and example files it ships with. ::code-group ```bash [pnpm] pnpm add @welldot/core ``` ```bash [npm] npm install @welldot/core ``` ```bash [yarn] yarn add @welldot/core ``` :: ## Requirements - Node.js 18 or later, or any modern browser or bundler. The package has no DOM or network dependency, so it also runs in workers and edge runtimes. - ES modules and CommonJS are both published (`import` and `require`), with TypeScript declarations. - Dependencies: [`zod`](https://zod.dev) 3 (the validators are Zod schemas) and `defu`. They install with the package. ## What else is in the package | Path | Contents | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `@welldot/core` | The library: types, validators, serialization, units, vocabularies. | | `@welldot/core/well.schema.json` | The [JSON Schema](/format/reference/json-schema) for v2, the same file published at `https://welldot.org/schema/v2/well.schema.json`. | | `@welldot/core/examples/.well` | The 19 [example wells](/format/reference/examples). | ```ts // Node: read an example from the installed package import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const path = require.resolve('@welldot/core/examples/coluna-mg-cristalino-abastecimento.well'); const text = await readFile(path, 'utf8'); ``` ## Versions The package follows semantic versioning from 1.0 onward; during 0.x a minor release can change the API, and the [changelog](/core/changelog) says how. Format support by release: | `@welldot/core` | Format revision | | --------------- | -------------------- | | 0.5.0 | 2.4 (writes `"2.4"`) | | 0.4.0 | 2.3 | | 0.3.0 | 2.1 | | 0.2.0 | 2.0 | ::warning Releases up to 0.4.0 reject files with `"version": "2.4"`. Update every program that reads your files before any of them starts writing 2.4 files. :: ## Next [First use](/core/getting-started/first-use) # Parsing and validating > Validate .well text with parseWell, validate objects with WellSchema and the per-object schemas, turn Zod issues into readable messages, and read legacy files. `@welldot/core` defines what a valid `.well` file is. The same validators back the [JSON Schema](/format/reference/json-schema), the welldot.org editor and this site's [MCP validator](/ai/mcp), so every tool agrees. ## `parseWell`: text in, `Well` out ```ts import { parseWell } from '@welldot/core'; const well = parseWell(text); ``` `parseWell(json: string): Well` does four things, in order: 1. Parses the JSON text. 2. Reads `version` and decides how to read the file, following [parser version handling](/format/spec/v2/format-reference#parser-version-handling): `1` is upgraded to v2, `2` and any `"2.x"` are read as v2, a missing version is read as v2, anything else throws an `Error` naming the value (`Unsupported .well format version: "3.0"`). 3. Validates the document against `WellSchema` and throws a `ZodError` on failure. 4. Returns the validated data with every unknown member kept, at every level, including inside array items ([extensibility](/format/spec/v2/format-reference#extensibility)). The v1 upgrade applies the normalizations from [changes from v1](/format/spec/v2/overview#changes-from-v1): flat `lat`, `lng` and `elevation` become `location`, `fgdc_texture` becomes `texture`, `screen_slot_mm` becomes `screen_slot`, and the `bole_hole` typo becomes `bore_hole`. ## Readable error messages A `ZodError` carries an `issues` array. Each issue has a `path` (field names and array indexes) and a `message`. This helper turns them into the `field[0].name: message` form used across the docs: ```ts [validate.ts] import { parseWell, type Well } from '@welldot/core'; type Issue = { path: (string | number)[]; message: string }; export type Result = | { valid: true; well: Well } | { valid: false; errors: string[] }; function formatPath(path: (string | number)[]): string { return path.reduce( (out, seg) => typeof seg === 'number' ? `${out}[${seg}]` : out ? `${out}.${seg}` : seg, '', ); } export function validate(text: string): Result { try { return { valid: true, well: parseWell(text) }; } catch (error) { if (error instanceof Error && error.name === 'ZodError') { const { issues } = error as Error & { issues: Issue[] }; return { valid: false, errors: issues.map(i => `${formatPath(i.path)}: ${i.message}`), }; } // Invalid JSON or an unsupported version return { valid: false, errors: [(error as Error).message] }; } } ``` ```ts validate( '{"version":"2.4","bore_hole":[{"from":0,"to":"60 m","diameter":254}]}', ); // { valid: false, errors: [ // 'bore_hole[0].to: Expected number, received string', // 'well_case: Required', … // ] } ``` [Validating](/format/guides/validating) explains the common messages. ## `WellSchema` and the object schemas `WellSchema` is the Zod schema of a whole document. Use it to check an object you already have, for example one assembled by a form: ```ts import { WellSchema } from '@welldot/core'; const result = WellSchema.safeParse(candidate); if (!result.success) console.log(result.error.issues); ``` `WellSchema` checks a v2 object as it is: it does not upgrade v1 data and does not look at `version` beyond its shape. For files, use `parseWell`. Every object has its own schema too, which is handy for validating one record before adding it: ```ts import { LithologySchema, WellScreenSchema } from '@welldot/core'; LithologySchema.parse({ from: 8, to: 30, description: 'Silty sandstone', color: '#c9a27a', texture: { code: 612, vocabulary: 'fgdc' }, geologic_unit: 'Botucatu Formation', aquifer_unit: 'Guarani', }); WellScreenSchema.safeParse({ from: 36, to: 54, type: 'pvc', diameter: 168.3 }); // fails: screen_slot is required ``` They are ordinary Zod schemas, so `.extend`, `.pick` and `.refine` work as usual. The list is in the [API reference](/core/api) under Variables (`BoreHoleSchema`, `WellCaseSchema`, `HydrodynamicEventSchema`, `PermitSchema`, `WaterSampleSchema`, …). ## What the validators check, and what they do not They check the schema: required fields, types, closed vocabularies, formats such as `YYYY-MM-DD` dates and RFC 3339 instants with an offset, and the cross-field rules the spec makes errors (for example, a water quality result has exactly one of `value`, `presence` or `text`). They do not add rules the spec does not have, and they do not check geometry (`from` below `to`, casings inside the hole) or references between records. Those are warnings in the spec, computed by [`@welldot/utils`](/utils), for example `getPermitWarnings` and `getWaterSampleWarnings`. ## Legacy files: `deserializeWell` Files written by early versions of Well Profiler, before the format had a version field, used other shapes. `deserializeWell` reads them: ```ts import { deserializeWell, parseWell, serializeWell } from '@welldot/core'; const restored = deserializeWell(oldText); // Well, or null for an unversioned file with no profile const well = restored && parseWell(serializeWell(restored)); // then validate ``` It accepts v2 and v1 files and also: - the old `constructive` / `geologic` sub-objects; - diameters in inches (`diam_pol`), converted to millimeters; - the `bole_hole` typo. `deserializeWell` normalizes but does not validate, and it fills a missing `well_depth` with the depth calculated from the construction. It throws on invalid JSON and on an unknown version. Use it when importing old archives, then validate the result as above; use `parseWell` everywhere else. ## Related - [JSON Schema](/format/reference/json-schema), for validation in other languages - [`parseWell` in the API reference](/core/api/functions/parseWell) # Serializing and sharing > Write a Well back to .well text with serializeWell, keep unknown members, check whether a well holds data, and redact sections before sharing a file. ## `serializeWell` ```ts import { serializeWell } from '@welldot/core'; const text = serializeWell(well); // a JSON string, ready to save as .well ``` `serializeWell(well: Well): string` writes the current format: - `"version": "2.4"` (the value of `WELL_FORMAT_VERSION`), whatever version the file was read from; - `location` as an object, never the v1 flat `lat` / `lng` / `elevation`; - no `null` and no `undefined` optional fields, as the spec requires ([absent values](/format/spec/v2/format-reference#absent-values-since-v24)), except inside `x-` members and `@context`, which are written as they are; - every root member the spec does not define (`x-` fields and blocks, foreign members) unchanged, after the spec blocks ([extensibility](/format/spec/v2/format-reference#extensibility)). The output is compact JSON. To store it indented, re-indent it: ```ts const pretty = JSON.stringify(JSON.parse(serializeWell(well)), null, 2); ``` ## Round trips keep what you did not touch `parseWell` keeps unknown members at every level, and `serializeWell` writes them back. A tool that reads a file, edits a field and saves it therefore preserves extensions it has never heard of: ```ts import { parseWell, serializeWell } from '@welldot/core'; const well = parseWell( '{"version":2,"x-acme":{"asset":"A-7"},"bore_hole":[{"from":0,"to":60,"diameter":254,"x-acme-bit":"PDC"}],' + '"well_case":[],"reduction":[],"well_screen":[],"surface_case":[],"hole_fill":[],' + '"lithology":[],"fractures":[],"caves":[]}', ); well.name = 'PT-07'; serializeWell(well); // {"version":"2.4","name":"PT-07","bore_hole":[{"from":0,"to":60,"diameter":254,"x-acme-bit":"PDC"}], // …,"x-acme":{"asset":"A-7"}} ``` ::warning Spreading a well into a new object of only the fields you know (`{ name, bore_hole, … }`) drops unknown members. Edit the parsed object in place, or copy it whole with `structuredClone`. :: ## Empty or not Two helpers answer different questions: | Helper | `true` when | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `isWellEmpty(well)` | There is nothing to draw: every construction and geology array is empty. | | `hasWellData(well)` | The well holds any data at all: a profile, identification, location, events, samples, permits or `x-` members. Blank strings and zeros do not count. | A registry record with a location and a permit but no construction has data (`hasWellData` is `true`) and nothing to draw (`isWellEmpty` is `true`). Both accept `null` and `undefined`. `calculatedWellDepth(well)` returns the deepest `to` across the construction arrays, ignoring geology, or `0`. Use it when `well_depth` is absent and you need the drilled depth ([Depths and intervals](/format/concepts/depths-and-intervals)). ## Redacting before sharing To publish a well without some of its data, such as a private owner's location or permits, use `redactWell`. It returns a copy with the hidden parts removed (arrays emptied, optional fields deleted), so the result is still a valid `.well` file. ```ts import { redactWell, serializeWell } from '@welldot/core'; // Hide a whole section const publicWell = redactWell(well, { operation: false }); // Or individual fields const anonymous = redactWell(well, { location: false, permits: false, meters: false, }); serializeWell(publicWell); ``` Keys are the seven sections or the fields within them; `false` hides, a missing key stays visible, and hiding a section hides all its fields: | Section | Fields | | --------------- | --------------------------------------------------------------------------------------------------------------- | | `general` | `identification`, `location`, `obs`, `attachments` | | `constructive` | `bore_hole`, `surface_case`, `well_case`, `reduction`, `well_screen`, `hole_fill`, `centralizers`, `cement_pad` | | `geology` | `lithology`, `fractures`, `caves` | | `hydrodynamic` | `hydrodynamic_events`, `aquifer_analysis` | | `history` | (the whole section) | | `operation` | `pump_installations`, `meters`, `operating_regime`, `production`, `permits` | | `water_quality` | (the whole section) | The same structure is exported as `SECTION_KEYS`, `VISIBILITY_TREE` and `VISIBILITY_LEAF_KEYS`, for building a visibility form. References from visible records to hidden ones are left as they are; they become dangling references, which readers report as warnings ([dangling references](/format/spec/v2/format-reference#dangling-references)). Unknown root members (`x-` blocks) are kept only when nothing is hidden, since `redactWell` cannot know whether they are private. ## Related - [`serializeWell`](/core/api/functions/serializeWell), [`redactWell`](/core/api/functions/redactWell), [`hasWellData`](/core/api/functions/hasWellData) in the API reference - [Building on .well](/format/guides/building-on-well) # Unit conversion > Convert between the canonical SI units of .well files and the units people read and type, with the conversion helpers of @welldot/core. A `.well` file always stores canonical SI units: meters, millimeters, m³/h, kW, m³ ([Units](/format/spec/v2/format-reference#units)). People read and type inches, feet, gallons and horsepower. `@welldot/core` provides the conversions so that every program converts the same way: to SI when writing, from SI when displaying. ```ts import { inchesToMm, metersToFeet, slotNumberToMm } from '@welldot/core'; inchesToMm(10); // 254, for a 10 in borehole slotNumberToMm(20); // 0.508, a slot-20 screen metersToFeet(60); // 196.85… ``` Results are not rounded; format them for display (for example with `formatNumber` from [`@welldot/utils`](/utils/guides/formatting)). ## The helpers | Quantity | Functions | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Length | `metersToFeet`, `feetToMeters` | | Diameter | `mmToInches`, `inchesToMm` | | Screen slot | `slotNumberToMm`, `mmToSlotNumber` | | Flow | `cubicMeterPerHourToLitersPerSecond`, `litersPerSecondToCubicMeterPerHour`, `cubicMeterPerHourToUsGallonsPerMinute`, `usGallonsPerMinuteToCubicMeterPerHour` | | Power | `cvToKilowatts`, `kilowattsToCv`, `hpToKilowatts`, `kilowattsToHp` | | Volume | `litersToCubicMeters`, `cubicMetersToLiters`, `cubicMetersToCubicFeet`, `cubicFeetToCubicMeters`, `cubicMetersToUsGallons`, `usGallonsToCubicMeters` | | Time | `minutesToHours`, `hoursToMinutes` | | Pressure | `kilopascalToPsi`, `psiToKilopascal` | | Transmissivity | `squareMeterPerSecondToSquareMeterPerDay`, `squareMeterPerDayToSquareMeterPerSecond` | | Coordinates | `decimalDegreesToDms`, `dmsToDecimalDegrees` | | Concentration | `concentrationToCanonical`, `concentrationFromCanonical` (mg/L, µg/L, ng/L, g/L) | | Conductivity | `milliSiemensPerMeterToMicroSiemensPerCm`, `milliSiemensPerCmToMicroSiemensPerCm` | Every function has a page in the [API reference](/core/api). ## Converting by a user's preference For flow, power and volume there are paired helpers that take the display unit as an argument. They fit a settings screen where the user picks units once: ```ts import { flowFromCanonical, flowToCanonical, type FlowUnits, } from '@welldot/core'; const unit: FlowUnits = 'L/s'; // 'm3/h' | 'L/s' | 'gpm' flowFromCanonical(36, unit); // 10, to show a stored 36 m³/h flowToCanonical(10, 'gpm'); // 2.27…, to store a typed 10 gpm ``` `powerFromCanonical` / `powerToCanonical` take `'kW' | 'cv' | 'hp'`, and `volumeFromCanonical` / `volumeToCanonical` take `'m3' | 'L' | 'ft3' | 'gal'` (US gallon). The `Units` type groups the preferences (`length`, `diameter`, `flow`, `power`, `volume`) and is what `@welldot/render` accepts. ## Diameters and slots: the traps - **Nominal sizes are not diameters.** `inchesToMm(6)` is 152.4 mm, but 6 in steel pipe has an outer diameter of 168.3 mm, and the file stores the real outer diameter ([as-built convention](/format/spec/v2/format-reference#diameter-and-the-as-built-convention)). Use the conversion for boreholes and true measurements; map trade sizes to real diameters in your input form. - **Slot numbers** are thousandths of an inch: `slotNumberToMm(20)` is 0.508 mm ([screen slot](/format/spec/v2/format-reference#screen-slot)). ## Coordinates ```ts import { decimalDegreesToDms, dmsToDecimalDegrees } from '@welldot/core'; decimalDegreesToDms(-1.4558, 'lat'); // { degrees: 1, minutes: 27, seconds: 20.88, direction: 'S' } dmsToDecimalDegrees({ degrees: 1, minutes: 27, seconds: 20.88, direction: 'S', }); // -1.4558 ``` Projected coordinates (UTM) are out of scope: convert them with a projection library such as `proj4` before writing `location` ([CRS scope](/format/spec/v2/format-reference#crs-scope)). ## Laboratory reports Lab reports use many unit spellings. These helpers map them onto the file's conventions ([water quality units](/format/spec/v2/water-quality#units)): ```ts import { concentrationToCanonical, expandedToStandardUncertainty, normalizeColorUnit, turbidityCodeForUnit, } from '@welldot/core'; concentrationToCanonical(50, 'ug/L'); // 0.05 mg/L turbidityCodeForUnit('NTU'); // 'turbidity_ntu', the parameter code to use normalizeColorUnit('mg Pt-Co/L'); // 'uH' expandedToStandardUncertainty(0.04); // 0.02, U / k with k = 2, for value_precision ``` The last one implements the spec's rule that `value_precision` is one standard deviation, not the expanded uncertainty ([precision](/format/spec/v2/format-reference#precision-and-uncertainty)). # Versions and time zones > Work with the .well version field and the well site time zone using WELL_FORMAT_VERSION, parseFormatVersion, compareFormatVersions and isIanaTimeZone. ## The version this library writes ```ts import { WELL_FORMAT_VERSION } from '@welldot/core'; WELL_FORMAT_VERSION; // '2.4' ``` `serializeWell` always writes this version. `parseWell` reads `1`, `2` and any `"2.x"` ([Versions and deprecation](/format/concepts/versions-and-deprecation)). ## Parsing and comparing versions Since 2.4 the version is the string `"M.m"`, compared as two integers ([Versioning](/format/spec/v2/format-reference#versioning)). Never compare version strings as text: `"2.10"` sorts before `"2.9"` as text but is the later revision. ```ts import { compareFormatVersions, parseFormatVersion } from '@welldot/core'; parseFormatVersion('2.4'); // { major: 2, minor: 4 } parseFormatVersion(2); // { major: 2, minor: undefined }, a v2.0–v2.3 file parseFormatVersion(2.4); // undefined: a JSON number is malformed parseFormatVersion('2.4.1'); // undefined: patch levels are not allowed compareFormatVersions('2.10', '2.9'); // 1 (positive: the first is later) ``` `parseFormatVersion` returns `undefined` for every malformed form, so you can report the value to the user as the spec requires ([parser version handling](/format/spec/v2/format-reference#parser-version-handling)). `compareFormatVersions` throws on malformed input. A typical check before writing to a shared archive: ```ts import { WELL_FORMAT_VERSION, compareFormatVersions } from '@welldot/core'; const archiveReads = '2.3'; // the newest revision your readers support if (compareFormatVersions(WELL_FORMAT_VERSION, archiveReads) > 0) { console.warn( `This tool writes ${WELL_FORMAT_VERSION}; update the readers first.`, ); } ``` ## The well site time zone Permit deadlines and validity are judged on the local calendar date at the well. Since 2.4, `location.properties.timezone` stores the site's IANA time zone, and the local date of an instant is that instant converted to the zone ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)). ```json "location": { "lat": -1.4558, "lng": -48.5039, "properties": { "timezone": "America/Belem" } } ``` A value that is not an IANA name is malformed. Check it before writing: ```ts import { isIanaTimeZone } from '@welldot/core'; isIanaTimeZone('America/Belem'); // true isIanaTimeZone('-03:00'); // false: offsets are not time zones isIanaTimeZone('BRT'); // false ``` `@welldot/utils` uses the zone for local dates (`instantLocalDate`, `getWellTimeZone`) and warns with `missing_timezone` when a file has permit condition fulfillments but no zone ([Permits](/utils/guides/permits)). # Vocabularies and textures > Use the recommended and closed vocabularies that @welldot/core publishes as data, show English and Portuguese labels, type open fields with OpenVocab, and look up FGDC textures. The spec recommends a list of values for most string fields and fixes the list for a few ([Vocabularies](/format/concepts/vocabularies)). `@welldot/core` publishes every list as data, with labels keyed by language, so forms, tables and reports can show readable names without keeping their own translations. ## Vocabulary entries Each vocabulary is an array of entries: ```ts import { WELL_TYPES } from '@welldot/core'; WELL_TYPES[0]; // { value: 'tubular', label: { en: 'Tubular', pt: 'Tubular' } } ``` An entry has a `value` (what the file stores), a `label` with at least `en` and `pt`, and `deprecated: true` when the spec deprecates the value (`artesian` in `WELL_TYPES`). ## Labels ```ts import { CONSTRUCTION_MATERIALS, PERMIT_TYPES, getVocabLabel, } from '@welldot/core'; getVocabLabel(PERMIT_TYPES, 'abstraction_permit', 'pt'); // 'Outorga de direito de uso' getVocabLabel(CONSTRUCTION_MATERIALS, 'stainless_steel', 'pt-BR'); // 'Aço inox' getVocabLabel(CONSTRUCTION_MATERIALS, 'Aço galvanizado DIN 2440', 'pt'); // returned as is ``` `getVocabLabel(vocab, value, locale)` falls back from `pt-BR` to `pt` to `en`. A value that is not in the list, such as free text or an `x-` value, is returned unchanged, which is exactly what the spec asks a tool to display ([recommended vocabularies](/format/spec/v2/format-reference#recommended-vocabularies)). Other helpers: | Helper | Use | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `vocabValues(vocab)` | The recommended values, for a select list. Deprecated values are left out unless you pass `{ includeDeprecated: true }`. | | `getVocabEntry(vocab, value)` | The entry of a value, or `undefined`. | | `isVocabValue(vocab, value)` | Whether a value is recommended (a type guard). | | `formatVocabList(vocab, values, locale)` | Comma-separated labels of a multi-valued field, such as `well_purpose`. | | `resolveLanguageText(text, locale)` | Resolves any `{ en, pt, … }` label object with the same fallback. | ```ts import { WELL_PURPOSES, formatVocabList, vocabValues, WELL_TYPES, } from '@welldot/core'; vocabValues(WELL_TYPES); // ['tubular', 'hand_dug', 'horizontal', 'infiltration_gallery'] formatVocabList( WELL_PURPOSES, ['production', 'monitoring', 'x-irrigation'], 'en', ); // 'Production, Monitoring well, x-irrigation' ``` ## Typing open fields Open fields are typed with `OpenVocab`, which is `T | (string & {})`: editors autocomplete the recommended values while any string still type-checks. ```ts import type { OpenVocab, PermitType } from '@welldot/core'; const a: OpenVocab = 'abstraction_permit'; // suggested const b: OpenVocab = 'x-special'; // also fine ``` ## The vocabularies | Vocabulary | Field | | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | | `WELL_TYPES` | `well_type` | | `WELL_PURPOSES` | `well_purpose[]` | | `DRILLING_METHODS` | `bore_hole[].drilling_method` | | `CONSTRUCTION_MATERIALS` | `well_case[].type`, `well_screen[].type`, `reduction[].type`, `pump_installations[].riser_material` | | `SCREEN_SLOT_TYPES` | `well_screen[].slot_type` | | `CENTRALIZER_TYPES` | `centralizers[].type` | | `CEMENT_PAD_TYPES` | `cement_pad.type` | | `HYDRODYNAMIC_EVENT_TYPES`, `MEASUREMENT_METHODS` | `hydrodynamic_events[].type`, `.measurement_method` | | `AQUIFER_ANALYSIS_METHODS` | `aquifer_analysis[].method` | | `HISTORY_LOG_CATEGORIES`, `HISTORY_LOG_SEVERITIES`, `MAINTENANCE_TYPES` | `history_logs[]` fields | | `PUMP_TYPES`, `POWER_SOURCES` | `pump_installations[]` fields | | `PERMIT_TYPES`, `WATER_USES`, `CONDITION_CATEGORIES`, `PERMIT_HISTORY_TYPES` | `permits[]` fields | | `METER_TYPES`, `READING_SOURCES`, `ESTIMATION_METHODS` | `meters[]`, `production[]` fields | | `DOCUMENT_TYPES` | `attachments[].document_type` | | `SAMPLE_TYPES`, `SAMPLING_METHODS`, `SAMPLING_POINT_TYPES`, `SAMPLING_DEVICES` | `water_samples[]` fields | Closed vocabularies ship in the same shape, so `getVocabLabel` labels them too: `WELL_STATUSES` (`history_logs[].status`), `PERMIT_ADMINISTRATIVE_STATUSES`, `VOLUME_LIMIT_PERIODS`, `PRODUCTION_ENTRY_TYPES`, `RESULT_QUALIFIERS`, `VALIDATION_STATUSES`, `BAROMETRIC_COMPENSATIONS`, `READING_AGGREGATIONS` and others. Each one is checked against its TypeScript type at compile time, so it cannot drift from the schema. The `@welldot/core` test suite compares every list with the tables in the spec. Each constant has a page in the [API reference](/core/api). ## FGDC textures `FGDC_TEXTURES_OPTIONS` lists the FGDC lithology patterns (FGDC-STD-013-2006) as `{ code, label }`. Use it to look up a code or build a picker for `lithology[].texture`: ```ts import { FGDC_TEXTURES_OPTIONS } from '@welldot/core'; FGDC_TEXTURES_OPTIONS.find(t => t.code === 607); // { code: 607, label: 'Massive sand or sandstone' } const options = FGDC_TEXTURES_OPTIONS.map(t => ({ value: t.code, label: t.label, })); ``` Codes are grouped by series: 100 surficial, 200 sedimentary, 300 igneous, 400 miscellaneous and metamorphic, 500 glacial and periglacial, 600 sedimentary lithology, 700 metamorphic and igneous lithology. The 600 and 700 series are the ones with drawn patterns and the most useful for well logs; the [FGDC texture reference](/format/spec/reference/fgdc-textures) marks which codes are available. [`@welldot/render`](/render) draws these patterns. # Water quality vocabulary > Look up water quality parameters, their units and labels, resolve CAS numbers, and use the WHO, EU and Brazilian limit sets that @welldot/core ships as data. Water quality results in a `.well` file name their parameter by a code and a vocabulary, and take their unit from the code ([parameter vocabulary](/format/spec/v2/water-quality#parameter-vocabulary)). `@welldot/core` publishes the `welldot` vocabulary as versioned data, together with drinking water limit sets that are deliberately kept out of the files. ## Parameters ```ts import { getParameterDefinition, getParameterLabel } from '@welldot/core'; getParameterDefinition({ code: 'nitrate_as_n', vocabulary: 'welldot' }); // { // code: 'nitrate_as_n', group: 'nutrient', label: 'Nitrate as N', // labels: { pt: 'Nitrato como N' }, unit: { symbol: 'mg/L', ucum: 'mg/L' }, // form: 'value', molar_mass: 14.007, charge: -1 // } getParameterLabel({ code: 'nitrate_as_n', vocabulary: 'welldot' }, 'pt'); // 'Nitrato como N' ``` Each `ParameterDefinition` carries the code, its group (`physical`, `major_ion`, `metal`, `nutrient`, `microbiology`, …), the English label and translations, the canonical unit (display symbol and UCUM code), the form of a result (`value`, `presence` or `text`), the CAS number when there is one, and molar mass and charge for ions used in ion balance and hydrochemical diagrams. | Export | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | `WATER_QUALITY_PARAMETERS` | The 98 `welldot` parameter definitions. | | `WATER_QUALITY_VOCABULARY_VERSION` | Version of the published vocabulary. | | `WATER_QUALITY_UNITS` | The canonical units the vocabulary uses. | | `getParameterDefinition(param)` | Looks up a `{ code, vocabulary }`; a `cas` code resolves to its `welldot` entry. | | `getParameterLabel(param, locale)` | Display name; unknown codes are returned as they are. | | `parameterKey(param)` | One identity for a `welldot` code and its CAS equivalent, for grouping and duplicate detection. | | `isKnownParameter(param)` | `false` only for a `welldot` code this vocabulary version does not know. | ## CAS numbers Substances can be identified by their CAS registry number ([vocabulary rules](/format/spec/v2/water-quality#vocabulary-rules)). The library resolves the ones that have a `welldot` equivalent: ```ts import { getParameterDefinition, parameterKey } from '@welldot/core'; getParameterDefinition({ code: '14808-79-8', vocabulary: 'cas' })?.code; // 'sulfate' parameterKey({ code: '14808-79-8', vocabulary: 'cas' }); // 'sulfate' parameterKey({ code: 'sulfate', vocabulary: 'welldot' }); // 'sulfate', the same key ``` ## Limit sets Limits are not stored in `.well` files: a file records what was measured, and the reader decides which standard to compare it with ([limit sets are not part of the file](/format/spec/v2/water-quality#limit-sets-are-not-part-of-the-file)). The package ships three sets: | Export | Source | `id` | | ------------------- | ------------------------------------------------ | ------------------- | | `WHO_GDWQ_2022` | WHO Guidelines for Drinking-water Quality (2022) | `who_gdwq_2022` | | `EU_2020_2184` | EU Drinking Water Directive 2020/2184 | `eu_2020_2184` | | `BR_GM_MS_888_2021` | Brazil, Portaria GM/MS 888/2021 | `br_gm_ms_888_2021` | `WATER_QUALITY_LIMIT_SETS` lists them in display order, and `getLimitSet(id)` finds one by id. A set has a `name`, a `jurisdiction`, its `source` and a list of `limits`, each with a parameter `code`, a `min` and/or `max` in the parameter's canonical unit and a `basis`. ```ts import { getLimitSet } from '@welldot/core'; getLimitSet('who_gdwq_2022')?.limits.find(l => l.code === 'arsenic'); // { code: 'arsenic', max: 0.01, basis: 'health', note: 'provisional' } ``` To compare a sample with a set, use `getExceedances` from [`@welldot/utils`](/utils/guides/water-quality), which handles censored values, presence limits and turbidity comparability. ::caution The limit values are transcribed from the cited documents. Check them against the official text before any regulatory use. :: ## Related - [Water quality](/format/spec/v2/water-quality) in the spec - [Unit conversion](/core/guides/unit-conversion) for lab report units # @welldot/utils > Pure functions that analyze a parsed .well file, from volumes and drawdown to permit status, production totals and water quality indicators, following the spec's derivation rules. `@welldot/utils` computes what a `.well` file deliberately does not store. The spec forbids storing derived values ([design principles](/format/spec/v2/overview#design-principles)): drawdown, specific capacity, production volumes, permit status and exceedances are calculated from the data whenever they are needed. This package is where those calculations live, so every tool gets the same answers. All functions are pure: they take a `Well` from [`@welldot/core`](/core) and return values, with no DOM, no network and no side effects. The one exception is the SIAGAS importer, a separate entry point that fetches registry pages. ```ts import { parseWell } from '@welldot/core'; import { calculateHoleFillVolume, formatNumber, getLatestStaticLevel, } from '@welldot/utils'; const well = parseWell(text); formatNumber(calculateHoleFillVolume('gravel_pack', well), { fractionDigits: 2, suffix: 'm³', }); // '1.88 m³' getLatestStaticLevel(well); // 3.94 (m below ground) ``` ## What it covers | Area | Examples | Guide | | -------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Geometry and volumes | Annular volumes, gravel pack and seal estimates, depths and diameters | [Geometry and volumes](/utils/guides/geometry-and-volumes) | | Hydraulics | Static and dynamic levels, drawdown, specific capacity, Jacob losses, test chart series | [Water levels and pumping tests](/utils/guides/pumping-tests) | | Permits | Permit status, condition deadlines and their fulfillment, permit warnings | [Permits](/utils/guides/permits) | | Operation | Meter volumes, production by period, operating regime, well status, pumps | [Operation and production](/utils/guides/operation) | | Water quality | Exceedances, ion balance, facies, Piper and Stiff values, QA/QC | [Water quality](/utils/guides/water-quality) | | Import | Data logger exports, the SIAGAS registry | [Logger import](/utils/guides/logger-import), [SIAGAS import](/utils/guides/siagas-import) | | Display | Number, date and coordinate formatting | [Formatting](/utils/guides/formatting) | ## Units The package follows the format's units: depths in meters, diameters in millimeters, flow in m³/h ([Units](/format/spec/v2/format-reference#units)). Volumes come out in m³, specific capacity in m²/h (m³/h per meter) and unit drawdown in h/m². ## Next - [Installation](/utils/getting-started/installation) - [First use](/utils/getting-started/first-use) - [API reference](/utils/api) - [Changelog](/utils/changelog) # @welldot/utils ## 0.5.1 ### Patch Changes - 0c76546: The READMEs now link to the documentation site at `https://docs.welldot.org`. - 0332665: The repository moved to the `welldothq` GitHub organization. The package `repository`, `homepage` and `bugs` URLs now point at `https://github.com/welldothq/welldot`. - Updated dependencies [0c76546] - Updated dependencies [0332665] - Updated dependencies [147708f] - @welldot/core@0.6.0 ## 0.5.0 ### Minor Changes - a74ddd6: Supports the `.well` v2.4 format revision. - **Time zone.** `instantLocalDate(instant, timeZone?)` converts an instant to the well's zone. Fulfillment lateness, `getPermitTimeline(permit, timeZone?)` and the regime-in-force check use it through the new `getWellTimeZone(well)`. `getPermitWarnings` adds `missing_timezone` for files whose permits have condition fulfillments but no `timezone`. - **`logger_series`.** `getOperationWarnings` adds `analysis_uses_logger_series` (an analysis references a `logger_series` in `source_event_ids` or `static_level_source_id`) and `logger_reading_below_sensor` (a reading deeper than `sensor_depth`). Both apply to effective (non-retracted) events only. - **Logger import helpers.** Adds `parseDelimitedText`, `buildLoggerReadings` (column mapping, water column to depth), `localDateTimeToInstant` (local time to instant by IANA zone), `mergeLoggerReadings`, `inferLoggerInterval` and `downsampleLoggerReadings`. - **Daily operating time.** Derivations read `daily_operating_time` above 24 as 24 through the new `normalizeDailyOperatingTime`. - The SIAGAS importer writes `"version": "2.4"`. **Breaking for consumers that switch on warning codes:** - `PermitWarningCode` `daily_operating_time_out_of_range` is renamed to `daily_operating_time_normalized`. It now fires only for values above 24 (a negative value is a schema error). - `PermitWarningCode` `fulfillment_reference_unresolved` is removed. A fulfillment `event_id` or `sample_id` may legitimately resolve only in another well's file. - `OperationWarningCode` gains `daily_operating_time_normalized` (ids: the regime). ### Patch Changes - Updated dependencies [a74ddd6] - @welldot/core@0.5.0 ## 0.4.0 ### Minor Changes - Add the `@welldot/utils/siagas` subpath to import wells from SIAGAS. - Add pumping-test charts and event view helpers. - Support the `.well` v2.3 blocks: permits, meters, production, water quality and operating regime. - Breaking: replace `declared_volume` with `estimated_volume` in production entries. - Move shared helpers and vocabulary labels into the library. - Link multiple events and samples to any history log entry. ## 0.3.0 ### Minor Changes - Add artesian detection for `.well` v2.1. ## 0.2.3 ### Patch Changes - Polish open-source docs, licensing and schema tooling. ## 0.2.2 ### Patch Changes - Version bump released together with `@welldot/core` and `@welldot/render`. ## 0.2.1 ### Patch Changes - Move `formatNumber` into the library. ## 0.2.0 ### Minor Changes - Add hydrodynamic parameter calculations (drawdown, specific capacity) and query utilities. - Add `calculateHoleFillSegmentVolume` for summary tables. ## 0.1.0 ### Minor Changes - Initial release. # First use > Load a .well file and compute a well summary with @welldot/utils, covering depth, gravel pack volume, water levels, specific capacity, status and production. This page builds a short summary of a well, the kind of block a report or a registry page shows. It uses one of the [example wells](/format/reference/examples), a 204 m supply well in crystalline rock with a pumping test, permits and meters. ```ts [summary.ts] import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { calculatedWellDepth, parseWell } from '@welldot/core'; import { calculateHoleFillVolume, calculateSubmergence, formatNumber, getCurrentWellStatus, getLatestAquiferAnalysisField, getLatestPumpingDynamicLevel, getLatestStaticLevel, getProductionTotal, isFlowingArtesian, } from '@welldot/utils'; const require = createRequire(import.meta.url); const file = require.resolve('@welldot/core/examples/coluna-mg-cristalino-abastecimento.well'); const well = parseWell(await readFile(file, 'utf8')); const m = (v: number | undefined) => formatNumber(v, { fractionDigits: 2, suffix: 'm' }); console.log({ depth: m(well.well_depth ?? calculatedWellDepth(well)), // '204.00 m' gravelPack: formatNumber(calculateHoleFillVolume('gravel_pack', well), { fractionDigits: 2, suffix: 'm³', }), // '1.88 m³' staticLevel: m(getLatestStaticLevel(well)), // '3.94 m' flowing: isFlowingArtesian(well), // false dynamicLevel: m(getLatestPumpingDynamicLevel(well)), // '52.75 m' pumpSubmergence: m(calculateSubmergence(well)), // '9.25 m' specificCapacity: getLatestAquiferAnalysisField(well, 'specific_capacity'), // 0.291 (m²/h) status: getCurrentWellStatus(well) ?? 'unknown', // 'active' produced: formatNumber(getProductionTotal(well).total, { suffix: 'm³' }), // '1,238,406 m³' }); ``` What each call does: - `calculatedWellDepth` (from core) is the deepest construction depth, used when `well_depth` is absent. - `calculateHoleFillVolume` sums the net annular volume of every gravel pack interval, subtracting the casing and screens inside it. - `getLatestStaticLevel` takes the most recent static level from the hydrodynamic events, skipping any event retracted by a correction ([corrections](/format/spec/v2/object-schemas#corrections-since-v23)). `isFlowingArtesian` is true when that level is negative. - `getLatestPumpingDynamicLevel` is the last level measured while pumping; `calculateSubmergence` is the depth of the current pump's intake below it. - `getLatestAquiferAnalysisField` reads a field from the most recent interpretation that has it. - `getCurrentWellStatus` is the status of the latest `status_change` log entry, or `undefined` (unknown) when there is none ([`status_change`](/format/spec/v2/object-schemas#status_change)). - `getProductionTotal` derives volumes from meter readings with the spec's rollover and gap rules ([volume derivation](/format/spec/v2/object-schemas#volume-derivation-normative)). ::note Functions return `undefined` when the data is not there: no test, no pump, no meter. That is the format's "absent means unknown" rule carried into code. `formatNumber` shows `—` for it. :: ## Next - [Geometry and volumes](/utils/guides/geometry-and-volumes) - [Water levels and pumping tests](/utils/guides/pumping-tests) - [API reference](/utils/api) # Installation > Install @welldot/utils, its relationship to @welldot/core, and the separate SIAGAS entry point. ::code-group ```bash [pnpm] pnpm add @welldot/core @welldot/utils ``` ```bash [npm] npm install @welldot/core @welldot/utils ``` ```bash [yarn] yarn add @welldot/core @welldot/utils ``` :: `@welldot/utils` depends on `@welldot/core`, which provides the `Well` type and the parser. Install both so your code can import `parseWell` directly. `date-fns` is installed as a dependency for the date helpers. ## Requirements - Node.js 18 or later, or any modern browser or bundler. Everything except the SIAGAS importer runs without a DOM or network. - ES modules and CommonJS are both published, with TypeScript declarations. ## Entry points | Import | Contents | | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | | `@welldot/utils` | All analysis, query and formatting functions. Pure, side-effect free. | | `@welldot/utils/siagas` | The SIAGAS registry importer. Uses `fetch`; call it from a server ([SIAGAS import](/utils/guides/siagas-import)). | ## Versions Use matching minor versions of `@welldot/core` and `@welldot/utils`; both are released together from the same repository. The current release, 0.5.0, supports format revision 2.4. See the [changelog](/utils/changelog). ## Next [First use](/utils/getting-started/first-use) # Formatting > Display numbers, dates, calendar dates, coordinates and unit labels from .well data without floating-point noise or time zone shifts, using the display helpers of @welldot/utils. These helpers turn stored values into text for screens, PDFs and tables. They are framework-free and are shared by the welldot.org editor and `@welldot/pdf`. Anything that needs translated words takes them as plain strings, so you keep your own message catalog. ## Numbers Floating-point arithmetic produces values like `39.99998784`. `formatNumber` bounds the decimals so that noise never reaches the reader: ```ts import { formatNumber } from '@welldot/utils'; formatNumber(39.99998784, { fractionDigits: 2, suffix: 'm' }); // '40.00 m' formatNumber(1234.5, { locale: 'pt-BR', maximumFractionDigits: 1 }); // '1.234,5' formatNumber(undefined); // '—' ``` Options: `fractionDigits` (fixed), `minimumFractionDigits`, `maximumFractionDigits`, `suffix`, `locale` (a BCP 47 tag, default `en-US`) and `fallback` (default `—`, shown for `null`, `undefined` and `NaN`). An absent value shows as a dash, never as `0`, following the format's "absent means unknown" rule. ## Dates The format has two kinds of temporal values ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)): calendar dates (`YYYY-MM-DD`, for construction and permits) and instants (RFC 3339 with an offset, for everything measured). Handle them differently: ```ts import { formatCalendarDate, formatDate, fromCalendarDate, toCalendarDate, } from '@welldot/utils'; formatCalendarDate('2025-03-01'); // '01/03/2025', no time zone shift formatCalendarDate('2025-03-01', 'yyyy-MM-dd'); // any date-fns pattern formatDate(new Date('2025-03-01T10:00:00Z')); // 'dd/MM/yyyy HH:mm' in the runtime's time zone toCalendarDate(new Date(2025, 2, 1)); // '2025-03-01', from a local Date, never through UTC fromCalendarDate('2025-03-01'); // a local Date, or null when malformed ``` ::warning Never pass a calendar date to `new Date('2025-03-01')`: JavaScript reads it as UTC midnight, which is the previous day in the Americas. Use `fromCalendarDate` and `formatCalendarDate`. :: For the local date of an instant at the well site, use `instantLocalDate(instant, timeZone)` with the zone from `getWellTimeZone(well)` ([Permits](/utils/guides/permits)). ## Coordinates ```ts import { formatCoord, parseToDd } from '@welldot/utils'; formatCoord(-1.4558, 'DMS', true); // `1°27'20.88"S` (true = latitude) formatCoord(-1.4558, 'DD', true); // '-1.455800' parseToDd(`1°27'20.88"S`); // -1.4558, from DD or DMS text; NaN when unreadable ``` `clampLat` and `clampLng` keep typed values in range. Projected coordinates are not handled here; the file stores latitude and longitude only ([CRS scope](/format/spec/v2/format-reference#crs-scope)). ## Unit labels | Function | Returns | | ---------------------------------------- | ----------------------------------------------------------------- | | `resolveFlowUnitLabel(unit)` | `m³/h`, `L/s`, `gpm` for the storage tokens `m3/h`, `L/s`, `gpm`. | | `resolveVolumeUnitLabel(unit)` | `m³`, `L`, `ft³`, `gal`. | | `resolveDiameterUnitLabel(unit, locale)` | `mm`, or `"` in Portuguese and `in.` in English for inches. | Convert the values themselves with the [unit conversion](/core/guides/unit-conversion) helpers of `@welldot/core`. ## Records - `formatMeterLabel(meter, { locale, untypedLabel, dateFormat? })`: `Electromagnetic · S/N 123 · 01/02/2024`. - `permitLabel(permit, fallback?)`: the authority and identifier of a permit. - `formatWaterQualityResult(result, labels)`: `< 0.001`, `0.004 (est.)`, the not-detected, present or absent label, or the text ([Water quality](/utils/guides/water-quality)). # Geometry and volumes > Estimate gravel pack and seal volumes, compute cylindrical and annular volumes, and read depths and diameters from a well's construction with @welldot/utils. Construction intervals give everything needed to estimate materials: how much gravel to order, how much grout to mix, how wide the drawing must be. These functions read the construction blocks of a `Well` in the format's units, meters for depth and millimeters for diameter, and return cubic meters. ## Gravel pack and seal ```ts import { calculateHoleFillVolume, formatNumber } from '@welldot/utils'; const gravel = calculateHoleFillVolume('gravel_pack', well); const seal = calculateHoleFillVolume('seal', well); formatNumber(gravel, { fractionDigits: 2, suffix: 'm³' }); // '1.88 m³' formatNumber(seal, { fractionDigits: 2, suffix: 'm³' }); // '0.39 m³' ``` `calculateHoleFillVolume(type, well)` adds up every `hole_fill` interval of that type (`'gravel_pack'` or `'seal'`, the two values of the closed vocabulary in [HoleFill](/format/spec/v2/object-schemas#holefill--hole_fill)). For each interval it takes the annulus at the fill's `diameter` and subtracts any casing or screen that overlaps it, so the result is the net volume of material. For one interval, use `calculateHoleFillSegmentVolume(holeFill, well)`: ```ts import { calculateHoleFillSegmentVolume } from '@welldot/utils'; for (const fill of well.hole_fill) { console.log( fill.type, fill.from, fill.to, calculateHoleFillSegmentVolume(fill, well), ); } ``` ::note These are geometric volumes of a perfect cylinder. Real holes are wider than the bit, and drillers add a margin (often 20 to 50 %) for washouts and losses. Apply your own factor; the library does not guess one. :: ## Cylinders `calculateCylindricVolume(diameter, height)` is the volume in m³ of a cylinder with a diameter in millimeters and a height in meters: ```ts import { calculateCylindricVolume } from '@welldot/utils'; calculateCylindricVolume(254, 60); // 3.04 m³: a 10 in hole, 60 m deep calculateCylindricVolume(168.3, 1); // 0.022 m³ per meter of 6 in casing ``` Useful for drilling fluid volumes, well disinfection doses or the water stored in the casing. ## Depths `getProfileLastItemsDepths(well)` returns the deepest point of each block, in a fixed order: lithology, fractures, caves, bore hole, hole fill, reduction, surface case, well case, well screen. An empty block gives `0`. ```ts import { getProfileLastItemsDepths } from '@welldot/utils'; Math.max(...getProfileLastItemsDepths(well)); // the deepest recorded point, geology included ``` For the drilled depth from construction only, use `calculatedWellDepth` from [`@welldot/core`](/core/api/functions/calculatedWellDepth). The difference matters when a lithology log goes deeper than the finished well. [Depths and intervals](/format/concepts/depths-and-intervals) explains `well_depth`. ## Diameters ```ts import { getProfileDiamValues } from '@welldot/utils'; const diameters = getProfileDiamValues(well); // every diameter in mm, both ends of each reduction const widest = Math.max(...diameters); // to scale a cross-section ``` `getProfileDiamValues` accepts any object with the construction arrays (the `Constructive` type), and a `Well` is one. `getConstructivePropertySummary(well, property)` is the general form: one named property pulled from every construction component, `undefined` where a component has no such field. ## Centralizers The spec stores centralizers as an interval and a spacing, and forbids storing their count ([Centralizer](/format/spec/v2/object-schemas#centralizer--centralizers-since-v21)). `getCentralizerDepths` derives the positions: ```ts import { getCentralizerDepths } from '@welldot/utils'; getCentralizerDepths({ from: 10, to: 58, spacing: 12, type: 'spring_bow' }); // [10, 22, 34, 46, 58] ``` Without a spacing it returns only the two ends. # Logger import > Turn a pressure transducer or data logger export into logger_series readings, with column mapping, time zones, merging and downsampling, using @welldot/utils. A pressure transducer left in a well records the water level every few minutes for months. Since 2.4, such a record is a `logger_series` hydrodynamic event: a continuous series, not a pumping test and not a static level ([`logger_series`](/format/spec/v2/object-schemas#logger_series-since-v24)). Each reading has a `datetime` (an RFC 3339 instant) and a `depth` below ground. `@welldot/utils` turns a logger's CSV export into those readings. There are no vendor-specific parsers: you map the columns once for your logger model. ## From CSV to readings ```ts import { buildLoggerReadings, parseDelimitedText } from '@welldot/utils'; const text = `date;time;level 01/03/2025;00:00;12.40 01/03/2025;01:00;12.38 01/03/2025;02:00;---`; const { rows } = parseDelimitedText(text); // detects , ; or tab, handles quotes const { readings, skipped } = buildLoggerReadings(rows, { headerRows: 1, dateColumn: 0, timeColumn: 1, valueColumn: 2, valueKind: 'water_column', // the logger reports water above the sensor sensorDepth: 42, // so depth = 42 − value dateFormat: 'dmy', timeZone: 'America/Belem', // or offset: '-03:00' }); // readings: [ // { datetime: '2025-03-01T00:00:00-03:00', depth: 29.6 }, // { datetime: '2025-03-01T01:00:00-03:00', depth: 29.62 } // ] // skipped: [{ row: 3, issue: 'invalid_value' }] ``` - `valueKind: 'depth'` takes the value as the depth to water; `'water_column'` converts the height of water above the sensor using `sensorDepth`. - Times are local to the logger. Give the IANA `timeZone` of the site, and each reading gets the offset in force at that moment, daylight saving included. A fixed `offset` also works. - Rows that cannot be read are listed in `skipped` with a reason (`invalid_date`, `invalid_value`, `missing_sensor_depth`, `missing_offset`), never silently dropped. ## Into the file ```ts import { inferLoggerInterval } from '@welldot/utils'; well.hydrodynamic_events = [ ...(well.hydrodynamic_events ?? []), { id: crypto.randomUUID(), type: 'logger_series', datetime: readings[0].datetime, measurement_method: 'pressure_transducer', sensor_depth: 42, interval: inferLoggerInterval(readings), // 60 (minutes) barometric_compensation: 'barologger', // closed: barologger, vented or none aggregation: 'none', // closed: none, hourly or daily readings, }, ]; ``` Check the spec for the meaning of each field; hourly or daily aggregation requires `depth_min` and `depth_max` on every reading ([`logger_series`](/format/spec/v2/object-schemas#logger_series-since-v24)). ## Adding a new download Loggers are downloaded again and again, and the exports overlap: ```ts import { mergeLoggerReadings } from '@welldot/utils'; const { readings: merged, replaced } = mergeLoggerReadings( event.readings, newReadings, 'append', // or 'replace' ); ``` Readings are merged by instant, the incoming value wins on a tie, and the result is sorted. `replaced` counts the readings that were overwritten (`append`) or dropped (`replace`). ## Plotting A year of 15-minute readings is 35,000 points. `downsampleLoggerReadings(readings, maxPoints)` keeps the first and last readings and the minimum and maximum depth of each bucket, so peaks survive: ```ts import { downsampleLoggerReadings } from '@welldot/utils'; const plotted = downsampleLoggerReadings(event.readings, 2000); ``` `localDateTimeToInstant('2025-03-01T00:00', 'America/Belem')` converts one local time to an instant (`'2025-03-01T00:00:00-03:00'`) if you parse files yourself. ## Warnings `getOperationWarnings` reports `logger_reading_below_sensor` (a reading deeper than the sensor) and `analysis_uses_logger_series` (an aquifer analysis citing a logger series, which is not a controlled test) ([Operation and production](/utils/guides/operation)). # Operation and production > Derive production volumes from meter readings, total them by period, read the operating regime and the well's current status, and collect operation warnings with @welldot/utils. The operational blocks record how a well is used: meters (`meters`), their readings and estimated volumes (`production`), the declared pumping regime (`operating_regime`) and the well's status changes in `history_logs`. Volumes and the current state are derived from these records, following the spec's normative rules. ## Production volumes A meter (hidrômetro) is a totalizer: the file stores its register readings, and volumes are the differences between consecutive readings ([production](/format/spec/v2/object-schemas#production-since-v23)). `@welldot/utils` implements the [volume derivation](/format/spec/v2/object-schemas#volume-derivation-normative): - readings of each meter are ordered by instant, then `sequence`; - each pair of consecutive readings gives `r₂ − r₁`; - a decrease means the register rolled over at `max_reading`, or, without `max_reading`, an unknown volume, never a negative one; - an interval never spans two meters, so the gap between a meter's removal and the next one's installation is unmetered; - an `estimated_volume` entry counts only for the part of its period that no meter interval covers ([precedence](/format/spec/v2/object-schemas#precedence)). ```ts import { getProductionByPeriod, getProductionTotal } from '@welldot/utils'; getProductionTotal(well); // { metered: 1238406, estimated: 0, total: 1238406, unknown_intervals: 0 } getProductionByPeriod(well, 'year').at(-1); // { period: '2026', metered: 52067.3, estimated: 0, total: 52067.3, unknown_intervals: 0 } ``` `getProductionByPeriod(well, 'day' | 'month' | 'year')` buckets by local date (`YYYY-MM-DD`, `YYYY-MM`, `YYYY`) and prorates each interval by time across period boundaries, so the buckets add up to the total. Volumes are in m³. Lower-level helpers: | Function | Returns | | --------------------------------- | ---------------------------------------------------------------------------------------- | | `getMeterIntervals(well)` | Each interval between consecutive readings of a meter, with `volume: null` when unknown. | | `getCurrentMeters(well)` | Meters without `removed_at`, newest first. | | `getEffectiveProduction(well)` | Production entries not retracted by `corrects`. | | `getRetractedProductionIds(well)` | The retracted ids. | ## Operating regime `operating_regime` entries declare a flow rate, daily hours and days per week, each in force from `effective_from` ([operating regime](/format/spec/v2/object-schemas#operating_regime-since-v23)). ```ts import { getCurrentRegime, normalizeDailyOperatingTime } from '@welldot/utils'; getCurrentRegime(well); // the entry in force now getCurrentRegime(well, new Date('2015-06-01T00:00:00-03:00')); // the entry in force then (a Date) normalizeDailyOperatingTime(26); // 24: values above 24 h count as 24 in derivations ``` The stored value is never rewritten; the spec makes values above 24 a warning and normalizes them only for calculation. ## Well status The status is a closed vocabulary (`active`, `maintenance`, `inactive`, `decommissioned`, `abandoned`) written by `status_change` entries in `history_logs` ([status change](/format/spec/v2/object-schemas#status_change)). Without one, the status is unknown. ```ts import { getCurrentWellStatus, getCurrentWellStatusEntry, } from '@welldot/utils'; getCurrentWellStatus(well); // 'active', or undefined when no status_change exists getCurrentWellStatusEntry(well)?.datetime; // when it changed ``` Label it with `WELL_STATUSES` from `@welldot/core`. ## Pumps `pump_installations` is an installation block: each entry is one installation, from `installed_at` to `removed_at` ([pump installations](/format/spec/v2/object-schemas#pump_installations-since-v23)). | Function | Returns | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `getCurrentPump(well)` | The installation without `removed_at` (the latest if several). | | `getPumpInstalledAt(well, instant)` | The installation in place at an instant. | | `getPumpServiceTime(well, serial, now?)` | Total minutes in service of the installations that share a serial number. | | `calculateSubmergence(well)` | Intake depth below the latest dynamic level. | | `getPumpInstallationWarnings(well)` | Intake below the bottom of the well or inside a screen, several open installations, removal not after installation. | ## Warnings ```ts import { getOperationWarnings } from '@welldot/utils'; getOperationWarnings(well); // [{ code, ids }, …] ``` Covers meters, production, operating regime, structured history entries and analyses that cite retracted events, plus the 2.4 checks for logger series (an analysis citing a `logger_series`, readings deeper than the sensor). Each is a warning in the spec, never a reason to reject the file ([meter validation](/format/spec/v2/object-schemas#validation-warnings-never-rejection-2), [production validation](/format/spec/v2/object-schemas#validation-warnings-never-rejection-3)). # Permits > Derive a permit's status, list condition deadlines and whether each was met, follow renewals, and collect permit warnings with @welldot/utils. A `permits` entry records an abstraction permit (outorga), an exemption or a registration: who issued it, its validity, the granted flow and volumes, and its conditions (condicionantes) with their fulfillments ([`permits`](/format/spec/v2/object-schemas#permits-since-v23)). The file stores the facts as written in the documents. Whether a permit is in force today, and which deadlines are due, is derived on the local calendar date at the well, never stored ([permit status](/format/spec/v2/object-schemas#permit-status-derived)). The outputs on this page come from the `colatina-es-gnaisse-rural` [example well](/format/reference/examples): an insignificant-use exemption from 2017, superseded in 2023 by an irrigation permit with a meter-installation condition and a yearly volume report. ## Status ```ts import { getPermitStatus } from '@welldot/utils'; for (const permit of well.permits ?? []) { console.log(permit.id, getPermitStatus(well, permit)); } // insig-2017 superseded // outorga-2023 active ``` `getPermitStatus(well, permit, today?)` accepts a permit or its id and returns: | Status | When | | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `requested`, `suspended`, `revoked`, `denied`, `withdrawn` | The stored administrative `status` says so ([administrative status](/format/spec/v2/object-schemas#administrative-status)). | | `superseded` | Another permit names this one in `supersedes` and has started. | | `not_yet_valid` | Granted, but `valid_from` is in the future. | | `active` | In force today. | | `active_pending_renewal` | Past `valid_until`, with a renewal requested in time. | | `expired` | Past `valid_until`, with no renewal. | `today` is a calendar date (`YYYY-MM-DD`) and defaults to today. Pass it explicitly in tests and reports, so the result does not change with the clock. `PERMIT_STATUSES` has English and Portuguese labels for each status. Related helpers: - `getActivePermit(well, today?)`: the active permit with the latest start, the one production is checked against. - `getPermitEffectiveEnd(well, permit, today?)`: the day before the successor starts, else `valid_until`. - `isPermitGranted(permit)`, `getPermitIdentifier(permit)` (the `identifier`, else the `request_identifier`), `permitLabel(permit)`. ## Condition deadlines A condition gives its deadline as a date, a duration after issue (`due_after: "P90D"`) or a recurrence (`first_due` plus `recurrence: "P1Y"`), in ISO 8601 durations ([deadline generation](/format/spec/v2/object-schemas#deadline-generation-normative)). The deadlines themselves are derived: ```ts import { getConditionDeadlineStates, getConditionDeadlines, } from '@welldot/utils'; const permit = well.permits!.find(p => p.id === 'outorga-2023')!; const reporting = permit.conditions!.find(c => c.id === 'c2')!; // yearly volume report getConditionDeadlines(well, permit, reporting); // ['2024-01-31', '2025-01-31', '2026-01-31', …] getConditionDeadlineStates(well, permit, reporting, { today: '2026-10-24' }); // [{ due_date: '2024-01-31', status: 'fulfilled', fulfillment_id: 'f-2024' }, // { due_date: '2025-01-31', status: 'fulfilled', fulfillment_id: 'f-2025' }, // { due_date: '2026-01-31', status: 'fulfilled', fulfillment_id: 'f-2026' }, // { due_date: '2027-01-31', status: 'upcoming' }, …] ``` Each state is `fulfilled`, `fulfilled_late`, `upcoming` or `overdue`, matched against the condition's `fulfillments` by `due_date` ([condition fulfillment](/format/spec/v2/object-schemas#condition-fulfillment)). Deadlines stop at the condition's `occurrences` or `last_due`, at the permit's effective end or at a horizon; a permit that was never granted has none. Month-end dates are clamped (a monthly deadline from 31 January falls on 28 or 29 February). The duration helpers are exported too: `parseDateDuration`, `addDateDuration`, `todayCalendarDate`. ## History and timeline ```ts import { getOverduePermitHistory, getPermitHistory, getPermitTimeline, } from '@welldot/utils'; getPermitHistory(permit); // administrative steps sorted by date getOverduePermitHistory(permit); // steps past due_date and not done getPermitTimeline(permit, 'America/Sao_Paulo'); // steps and fulfillments, newest first ``` ## Warnings ```ts import { getPermitWarnings } from '@welldot/utils'; getPermitWarnings(well); // [] for a consistent file // [{ code: 'missing_timezone', ids: ['outorga-2023'] }] once location.properties.timezone is removed ``` Permit warnings point at data that is valid but inconsistent: validity in the wrong order, a broken `supersedes` chain, overlapping permits, a daily operating time above 24 h (read as 24), monthly maxima above the annual limit, duplicates, missing identifiers, fulfillments that match no deadline, and, since 2.4, a missing site time zone when conditions have fulfillments. None of them makes a file invalid ([permit validation](/format/spec/v2/object-schemas#validation-warnings-never-rejection-1)). The [docs MCP server](/ai/mcp) returns them alongside the validation result. ## Time zone Local dates are computed in `location.properties.timezone` when the file has one, otherwise from the offset written in each instant ([datetime conventions](/format/spec/v2/format-reference#datetime-conventions)). `getWellTimeZone(well)` returns the zone and `instantLocalDate(instant, timeZone?)` converts an instant to its local date. # Water levels and pumping tests > Read the current static and dynamic levels, compute drawdown and specific capacity, prepare pumping test plots and respect ledger corrections with @welldot/utils. Water levels and pumping tests live in `hydrodynamic_events`, a ledger of field records ([`hydrodynamic_events`](/format/spec/v2/object-schemas#hydrodynamic_events)). The file stores what was measured: levels, rates, elapsed times. Drawdown, specific capacity and the plots are computed from those readings, never stored ([design principles](/format/spec/v2/overview#design-principles)). ## Corrections come first An event can retract an earlier one with `corrects`. A retracted event stays in the file but must be left out of every derivation ([corrections](/format/spec/v2/object-schemas#corrections-since-v23)). Every function on this page already does that. If you iterate the events yourself, start from the effective list: ```ts import { getEffectiveHydrodynamicEvents, getRetractedEventIds, } from '@welldot/utils'; const events = getEffectiveHydrodynamicEvents(well); // every event not retracted const retracted = getRetractedEventIds(well); // Set of retracted ids, chains included ``` ## Current levels ```ts import { getLatestPumpingDynamicLevel, getLatestStaticLevel, isFlowingArtesian, } from '@welldot/utils'; getLatestStaticLevel(well); // 3.94: latest static level, by UTC instant getLatestPumpingDynamicLevel(well); // 52.75: latest level measured while pumping isFlowingArtesian(well); // true when the latest static level is above ground ``` - `getLatestStaticLevel` compares events by absolute instant, not by their position in the array, as the spec requires ([ordering and ties](/format/spec/v2/format-reference#ordering-and-ties)). - `getLatestPumpingDynamicLevel` skips air-lift events and falls back to a newer `aquifer_analysis[].dynamic_level`. - A negative level is above ground. `isFlowingArtesian` is the v2.1 way to detect a flowing well, replacing the deprecated `well_type: "artesian"` ([level sign convention](/format/spec/v2/format-reference#level-sign-convention)). ## Drawdown and specific capacity ```ts import { calculateDrawdown, calculateSpecificCapacity, calculateUnitDrawdown, lastReading, } from '@welldot/utils'; const test = events.find(e => e.type === 'constant_rate'); if (test?.type === 'constant_rate' && test.static_level !== undefined) { const end = lastReading(test); // { elapsed: 1440, depth: 52.75 } const rate = test.steps?.[0]?.rate; // m³/h if (end && rate) { const s = calculateDrawdown(end.depth, test.static_level); // 50.75 m calculateSpecificCapacity(rate, s); // 0.29 m²/h (Q/s) calculateUnitDrawdown(s, rate); // 3.44 h/m² (s/Q) } } ``` `calculateSpecificCapacity` and `calculateUnitDrawdown` throw a `RangeError` on a zero denominator instead of returning `Infinity`. The spec lists the formulas these follow in [Derived parameters](/format/spec/v2/object-schemas#derived-parameters--computation-reference). Interpreted values (transmissivity, storativity, Jacob coefficients) are not computed here; they are the analyst's result, stored in `aquifer_analysis`. Read the latest one with: ```ts import { getLatestAquiferAnalysisField } from '@welldot/utils'; getLatestAquiferAnalysisField(well, 'transmissivity'); // m²/s, from the newest analysis that has it ``` And use them with `calculateFormationLoss(jacobB, Q)` (B·Q), `calculateWellLoss(jacobC, Q)` (C·Q²) and `calculateHydraulicConductivity(T, thickness)`. ## Plotting a test The profiler's test charts are built from these series, and you can feed them to any chart library: | Function | Returns | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `availablePumpingCharts(event)` | Which plots the event has data for: `hydrograph`, `semilog`, `recovery`, `stepTest`. | | `pumpingTimeline(event)` | Every step and recovery reading on one test clock, with drawdown from the static level: `{ staticLevel, steps, pumpingEnd, recovery }`. | | `semilogDrawdownSeries(event)` | Drawdown against log time per step, for a Cooper-Jacob plot. | | `theisRecoverySeries(event)` | Residual drawdown against t/t′, for a Theis recovery plot. | | `stepTestSeries(event)` | Drawdown, s/Q and Q/s at the end of each step, for a step-drawdown plot. | ```ts import { availablePumpingCharts, semilogDrawdownSeries } from '@welldot/utils'; availablePumpingCharts(test); // ['hydrograph', 'semilog', 'recovery'] semilogDrawdownSeries(test)[0]; // { step: 0, rate: 14.76, points: [{ elapsed: 1, drawdown: 27.91 }, …] } ``` Smaller helpers for tables: `allStepReadings`, `stepRate`, `recoveryReadingsCount`, `stepHasReadings`, `derivedStepDuration`. ## Pumps and levels together ```ts import { calculateSubmergence, getCurrentPump, getPumpInstalledAt, } from '@welldot/utils'; getCurrentPump(well)?.intake_depth; // the installation without removed_at getPumpInstalledAt(well, '2019-05-20T08:00:00-03:00'); // the pump in place at that instant calculateSubmergence(well); // intake depth minus dynamic level; negative = intake above water ``` ## Data logger series `logger_series` events hold continuous sensor readings ([`logger_series`](/format/spec/v2/object-schemas#logger_series-since-v24)). They are neither tests nor static levels, so the functions above ignore them as sources of static or dynamic level. To import a logger export, see [Logger import](/utils/guides/logger-import). # SIAGAS import > Import a Brazilian well from the SIAGAS groundwater registry, and its RIMAS monitoring series, into a .well file with @welldot/utils/siagas. [SIAGAS](https://siagasweb.sgb.gov.br) is the groundwater information system of the Geological Survey of Brazil (SGB), with records of hundreds of thousands of wells. RIMAS is its groundwater monitoring network. `@welldot/utils/siagas` reads a well's public SIAGAS page, and for monitoring wells its RIMAS level series and photos, and converts them into a `.well` file. ## Import a well ```ts import { SiagasError, importSiagasWell } from '@welldot/utils/siagas'; try { const { json, well, warnings } = await importSiagasWell('3100020782', { rimas: 'monthly', // 'none' | 'monthly' (last reading of each month) | 'all' media: true, // RIMAS photos and documents as root attachments }); // json: the .well text, ready to save // well: the same data as a Well object // (the result also has source: { url, rimas_url?, fetched_at }) for (const w of warnings) console.log(w.message.pt); // also w.message.en } catch (error) { if (error instanceof SiagasError) console.log(error.kind); // 'invalid_code' | 'not_found' | 'upstream' | 'parse' | 'conversion' } ``` The argument is the SIAGAS point code (the `ponto` number in the registry's URL). ::warning Unlike the rest of the package, this entry point does network requests (with the global `fetch`, or one you pass in the options). The SGB pages send no CORS headers, so a browser cannot call them directly. Run the import on a server or in a script. :: ## What the conversion does The conversion is faithful: nothing is invented. - **Kept as recorded.** Overlapping intervals and values that disagree stay as the registry has them, and each becomes a warning, so you decide what to fix. Only exact duplicate rows are dropped. - **Zeros interpreted per field.** SIAGAS writes `0` for "no data". A lithology layer starting at 0 m is real; a ground elevation of 0 m is not, and is left out. - **Censored values.** Negative concentrations, the registry's way of writing "below the detection limit", become results with `qualifier: "<"`. - **Flowing wells.** "Surgência: S" makes the static level negative, as the format records flowing wells ([level sign convention](/format/spec/v2/format-reference#level-sign-convention)). - **The source is kept.** The page as read is stored in `x-siagas.record`, an extension block every reader preserves ([extensibility](/format/spec/v2/format-reference#extensibility)). Warnings are messages in Portuguese and English describing what could not be carried over or needed a decision. Read them before trusting the file, the same way you would review any [converted report](/format/guides/converting-a-report). ## The steps on their own The import is three steps, each exported: ```ts import { serializeWell } from '@welldot/core'; import { parseRimasPage, parseSiagasPage, siagasToWell, } from '@welldot/utils/siagas'; const record = parseSiagasPage(html); // the registry page's HTML → a structured record const rimas = parseRimasPage(rimasHtml); // a RIMAS page → its readings and media const { well, warnings } = siagasToWell(record, { rimas, rimasMode: 'monthly', }); const json = serializeWell(well); ``` Use them to work from pages you saved earlier, to test with fixtures, or to plug in your own HTTP client. ## Related - The [example wells](/format/reference/examples) are built on SIAGAS records, with the invented parts marked. - [Converting a report](/format/guides/converting-a-report) for wells that are not in SIAGAS. # Water quality > Compare water samples with WHO, EU or Brazilian limits, compute ion balance, hydrochemical facies and Piper and Stiff values, and check field QA/QC with @welldot/utils. `water_samples` is a ledger of samples, each with its field and laboratory results ([water samples](/format/spec/v2/water-quality#water_samples)). The file records only what was measured. Exceedances, ion balance and facies are derived on demand, and drinking water limits are chosen by the reader, never stored ([limit sets](/format/spec/v2/water-quality#limit-sets-are-not-part-of-the-file)). The outputs below come from two [example wells](/format/reference/examples): `extremoz-rn-barreiras-raso` (nitrate rising in a shallow well) and `quixeramobim-ce-cristalino-salobro` (brackish water in crystalline rock). ## Which samples count A corrected laboratory report is a new sample whose `corrects` points to the old one, and a result whose `validation.status` is `rejected` is excluded from every derivation ([result rules](/format/spec/v2/water-quality#result-rules)). Start from the effective list: ```ts import { getEffectiveWaterSamples, isResultUsable } from '@welldot/utils'; const samples = getEffectiveWaterSamples(well); // not retracted, sorted by datetime, sequence, file order const latest = samples.at(-1)!; const usable = latest.results.filter(isResultUsable); ``` ## Latest value of a parameter ```ts import { getLatestResult } from '@welldot/utils'; getLatestResult(well, 'nitrate_as_n'); // { sample: { id: 'am-2025', … }, result: { parameter: {…}, value: 12.6, measured_in: 'lab' } } ``` It skips blanks and rejected results, and matches CAS numbers to their `welldot` code. ## Exceedances Pick a limit set from `@welldot/core` ([Water quality vocabulary](/core/guides/water-quality-vocabulary)) and compare: ```ts import { BR_GM_MS_888_2021, WHO_GDWQ_2022 } from '@welldot/core'; import { getExceedances } from '@welldot/utils'; getExceedances(latest, BR_GM_MS_888_2021); // [ // { result_index: 0, parameter: { code: 'ph', … }, limit: { min: 6, max: 9, … }, kind: 'below_min' }, // { result_index: 3, parameter: { code: 'nitrate_as_n', … }, limit: { max: 10, … }, kind: 'above_max' } // ] getExceedances(latest, WHO_GDWQ_2022); // [] (WHO limits nitrate as NO₃, not as N) ``` Each exceedance has the result's index, the parameter, the limit and its `kind` (`above_max`, `below_min`, or a presence where absence is required). Censored values are handled: `< 5` cannot exceed a limit of 10. Turbidity in FAU is never compared with nephelometric limits. You can also pass your own `Limit[]`. ::note The same sample can pass one standard and fail another. That is why limits stay out of the file: the reader chooses the standard ([jurisdiction neutrality](/format/spec/v2/overview#design-principles)). :: ## Hydrochemistry ```ts import { getEffectiveWaterSamples, getHydrochemicalFacies, getIonBalance, getPiperCoordinates, getStiffValues, } from '@welldot/utils'; const sample = getEffectiveWaterSamples(brackishWell).at(-1)!; getIonBalance(sample); // { cations_meq: 44.29, anions_meq: 44.51, error_pct: -0.26 } getHydrochemicalFacies(sample); // { cation: 'mixed', anion: 'chloride' } getStiffValues(sample); // meq/L of Na+K, Ca, Mg, Cl, HCO₃+CO₃, SO₄ getPiperCoordinates(sample); // meq % in each triangle and the diamond point ``` The ion balance error is the usual check on a complete major-ion analysis: within about ±5 % it is considered consistent. Molar masses and charges come from the parameter vocabulary in `@welldot/core`. `getAcidDrainageIndicators(sample)` returns net alkalinity and the sulfate/chloride ratio when the analysis has them. ## Field QA/QC | Function | Checks | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `getRelativePercentDifferences(well, sampleId)` | Relative percent difference per parameter between a field duplicate or split and its `parent_sample_id`. | | `getBlankContamination(well)` | Substances detected in field, trip and equipment blanks, with their campaign. | | `getHoldingTimes(sample)` | Hours from collection to analysis per result, honoring day-resolution dates. | | `getReceivedTemperatureCompliance(sample, maxC?)` | Whether the sample arrived at the lab at or below `maxC` (6 °C by default). | | `getPurgeStabilization(purge, criteria?, window?)` | Whether the purge readings stabilized, per parameter, over the last readings. | | `getSampleDepth(well, sample)` / `isFormationWater(well, sample)` | The sampling depth, and whether it lies in a screened interval below the static level. | ```ts import { getRelativePercentDifferences } from '@welldot/utils'; getRelativePercentDifferences(well, 'am-2025-dup'); // [{ key: 'nitrate_as_n', original: 2.8, duplicate: 2.7, rpd_pct: 3.64 }, …] ``` ## Warnings `getWaterSampleWarnings(well)` lists the water quality conditions the spec marks as warnings, such as a field duplicate without `parent_sample_id`, a sampling depth outside the screens, a result analyzed before it was collected or a duplicated result, with the sample ids and result indexes ([validation warnings](/format/spec/v2/water-quality#validation-warnings-never-rejection)). ## Display ```ts import { formatWaterQualityResult, parameterUnitSymbol } from '@welldot/utils'; const labels = { notDetected: 'n.d.', present: 'present', absent: 'absent', estimated: 'est.', }; for (const r of latest.results) { console.log(formatWaterQualityResult(r, labels), parameterUnitSymbol(r)); // '< 5' 'mg/L', '0.004 (est.)' 'mg/L', 'absent' '', … } ``` Labels are passed in, so you keep your own translations. # @welldot/render > A D3 renderer that draws a .well file as a technical well profile in SVG, with lithology patterns, construction, fractures, caves, labels, zoom and tooltips. `@welldot/render` draws a `.well` file as a well profile: the lithological column with FGDC patterns, the borehole and its construction, the current pump, fractures, caves, depth labels and unit strips, at true depth scale. It renders into an `` element with D3 and works in any web framework or none. ::well-preview{example="belem-pa-pirabas-abastecimento"} :: The profile above is drawn by this package from a 265 m example well. The same renderer draws the welldot.org editor and the PDF reports of `@welldot/pdf`. ```ts import { INTERACTIVE_RENDER_CONFIG, WellRenderer } from '@welldot/render'; const renderer = new WellRenderer( [ { selector: '#profile', width: 420, height: 640, margins: { top: 24, right: 16, bottom: 20, left: 48 }, }, ], { renderConfig: INTERACTIVE_RENDER_CONFIG }, ); await renderer.prepareSvg(); // once, after the is in the page renderer.draw(well); // again whenever the data changes ``` ## Features - **Lithology** with FGDC texture patterns and layer colors, geologic and aquifer unit strips, and description annotations. - **Construction** drawn as built: telescoping holes, casing and screens, reductions, gravel pack and seals in the annulus, centralizers, casing shoes, stick-up and the wellhead pad. - **The current pump**, from `pump_installations`. - **Fractures and caves**, with dip and water-intake markers. - **Several panels** for long wells, with chosen depth windows and repeated bands for paged output. - **Interaction**: zoom, pan, tooltips and highlights, driven by the mouse or by your own toolbar. - **Themes, units and locale**: every color is configurable, lengths in meters or feet, diameters in millimeters or inches, labels in English or Portuguese. ## Next - [Installation](/render/getting-started/installation) - [First use](/render/getting-started/first-use) - [API reference](/render/api) - [Changelog](/render/changelog) # @welldot/render ## 0.4.2 ### Patch Changes - 0c76546: The READMEs now link to the documentation site at `https://docs.welldot.org`. - 0332665: The repository moved to the `welldothq` GitHub organization. The package `repository`, `homepage` and `bugs` URLs now point at `https://github.com/welldothq/welldot`. - Updated dependencies [0c76546] - Updated dependencies [0332665] - Updated dependencies [147708f] - @welldot/core@0.6.0 - @welldot/utils@0.5.1 ## 0.4.1 ### Patch Changes - a74ddd6: Accepts `.well` v2.4 files: the v2 check uses `parseFormatVersion`, so `"version": "2.4"` no longer triggers the unsupported-version warning. - Updated dependencies [a74ddd6] - Updated dependencies [a74ddd6] - @welldot/core@0.5.0 - @welldot/utils@0.5.0 ## 0.4.0 ### Minor Changes - Render the `.well` v2.3 pump installations and attachments. - Draw pipes open and close them only where they end. - Draw the cement pad at a fixed size. - Support the `well_screen` slot types. - Clear the drawing when the well becomes empty. - Guard against missing fracture dip and azimuth. - Add compact and detailed profile formats for PDF export. ## 0.3.0 ### Minor Changes - Draw centralizer bow marks for `.well` v2.1. ## 0.2.3 ### Patch Changes - Polish open-source docs, licensing and schema tooling. ## 0.2.2 ### Patch Changes - Version bump released together with `@welldot/core` and `@welldot/utils`. ## 0.2.1 ### Patch Changes - Add en/pt locale resolution for all renderer-drawn text. - Wire the locale into number formatting. ## 0.2.0 ### Minor Changes - Align with the `.well` v2 schema (texture, screen slot, version guard) and support v2 only. - Add `RenderableWell` with optional stable ids and enter/exit transitions. - Add stable D3 key functions for data-join renderers. - Add reduction construction element rendering. - Move the construction labels group after the other construction layers. - Rename `WithId`/`id` to `WithKey`/`key`. ## 0.1.0 ### Minor Changes - Initial release. # First use > Draw a .well file into an SVG element with WellRenderer, from parsing the file to redrawing when the data changes. This page draws a well on a plain HTML page. The same steps apply inside any framework. ## 1. An SVG element ```html ``` The renderer sizes the element itself: its width is the panel `width` plus the left and right margins, and its height the panel `height` plus the top and bottom margins. ## 2. Parse the file ```ts import { parseWell } from '@welldot/core'; // a file served with your page, e.g. an example well downloaded from welldot.org/examples const response = await fetch('/wells/extremoz-rn-barreiras-raso.well'); const well = parseWell(await response.text()); ``` `parseWell` validates the file and upgrades v1 files, so the renderer always receives a valid v2 `Well` ([Parsing and validating](/core/guides/parsing-and-validating)). ## 3. Create the renderer and draw ```ts import { INTERACTIVE_RENDER_CONFIG, WellRenderer, applyRenderLocale, } from '@welldot/render'; const margins = { top: 24, right: 16, bottom: 20, left: 48 }; const renderer = new WellRenderer( [{ selector: '#profile', width: 420, height: 640, margins }], { locale: 'en', renderConfig: applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'), onError: error => console.error(error), }, ); await renderer.prepareSvg(); renderer.draw(well); ``` - The first argument lists the panels. One panel draws the whole well; [Panels and layout](/render/guides/panels-and-layout) splits long wells over several. - `INTERACTIVE_RENDER_CONFIG` turns on zoom, pan, tooltips and animation. - The renderer's labels default to Portuguese. `locale: 'en'` with `applyRenderLocale(config, 'en')` switches every drawn word to English ([Locale and units](/render/guides/locale-and-units)). - `prepareSvg()` builds the SVG structure and loads the texture patterns. Call it once, after the element exists. - `draw(well)` renders the profile. Drawing errors are reported to `onError`, not thrown. The result looks like this: ::well-preview{example="extremoz-rn-barreiras-raso"} :: ## 4. Redraw on change Call `draw` again with the new data. Layers the well no longer has are removed, and changes animate: ```ts well.well_screen.push({ from: 20, to: 24, type: 'stainless_steel', diameter: 219, screen_slot: 0.75, }); renderer.draw(well); ``` To keep animations stable while editing, give array items a `key` (see `RenderableWell` in the [API reference](/render/api/classes/WellRenderer)). Keys are for the renderer only; strip them before saving the file. `renderer.clear()` empties every panel and keeps the structure, so the next `draw` needs no new `prepareSvg`. ## Next - [Panels and layout](/render/guides/panels-and-layout) - [Theming](/render/guides/theming) - [Using it in a framework](/render/guides/frameworks) # Installation > Install @welldot/render, its runtime requirements, and how to keep it out of server-side rendering. ::code-group ```bash [pnpm] pnpm add @welldot/core @welldot/render ``` ```bash [npm] npm install @welldot/core @welldot/render ``` ```bash [yarn] yarn add @welldot/core @welldot/render ``` :: `@welldot/render` draws `Well` objects from [`@welldot/core`](/core), which you use to parse the files. D3 7, `d3-tip`, `textures` and `sanitize-html` install with the package; `@welldot/utils` comes along as a dependency. ## Requirements - **A browser DOM.** The renderer draws into an existing `` element with D3. It runs in the browser, or in tests with jsdom; it does not render on a server by itself. - ES modules and CommonJS are both published, with TypeScript declarations. ## Server-side rendering In Nuxt, Next.js, SvelteKit and similar frameworks, create the renderer only on the client, after the SVG element is mounted. Importing the package dynamically there also keeps D3 and the texture definitions out of your server bundle and main chunk: ```ts // inside onMounted / useEffect / onMount const { WellRenderer, INTERACTIVE_RENDER_CONFIG } = await import('@welldot/render'); ``` [Using it in a framework](/render/guides/frameworks) shows complete Vue and React components. ## Versions The current release is 0.4.1. It accepts `Well` objects from `@welldot/core` 0.5.0, including files written as format 2.4. See the [changelog](/render/changelog). ## Next [First use](/render/getting-started/first-use) # Using it in a framework > Wrap WellRenderer in a Vue or React component that renders on the client only, redraws when the data changes, follows the container width and cleans up. The renderer is framework-free: it needs an `` element in the page and a `Well`. In a component framework the pattern is always the same: 1. Render an empty `` with a unique id. 2. After mount, import the package, create the renderer, call `prepareSvg()` and `draw()`. 3. Redraw when the well changes; rebuild when the size or theme changes. 4. Never run it during server-side rendering. ## Vue 3 ```vue [WellProfile.vue]
``` `onMounted` never runs on the server, so this component is safe in Nuxt as it is. The dynamic `import()` keeps D3 out of the server bundle. In Nuxt, wrapping it in `` also avoids a hydration warning for the empty SVG. ## React ```tsx [WellProfile.tsx] import type { Well } from '@welldot/core'; import type { WellRenderer } from '@welldot/render'; import { useEffect, useId, useRef } from 'react'; export function WellProfile({ well, height = 600, }: { well: Well; height?: number; }) { const id = `well-${useId().replace(/:/g, '')}`; const container = useRef(null); const renderer = useRef(null); useEffect(() => { let cancelled = false; (async () => { const { WellRenderer, INTERACTIVE_RENDER_CONFIG, applyRenderLocale } = await import('@welldot/render'); if (cancelled) return; const margins = { top: 24, right: 16, bottom: 20, left: 48 }; const width = (container.current?.clientWidth ?? 400) - margins.left - margins.right; const r = new WellRenderer( [{ selector: `#${id}`, width, height, margins }], { locale: 'en', renderConfig: { ...applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'), zoom: false, pan: false, }, }, ); await r.prepareSvg(); renderer.current = r; r.draw(well); })(); return () => { cancelled = true; renderer.current?.clear(); }; }, [id, height]); useEffect(() => { renderer.current?.draw(well); }, [well]); return (
); } ``` In Next.js, mark the file `'use client'`. React's `useId` returns ids with colons, which are not valid in a CSS selector, hence the `replace`. ## Resizing and themes The renderer draws at the size it was created with. To follow the container, observe it and rebuild after a short pause; to follow a dark mode switch, rebuild with the other theme ([Theming](/render/guides/theming#dark-mode)). Rebuilding is cheap: create the new renderer, `prepareSvg()`, `draw()`. ## Validation first Parse with `parseWell` before handing data to the renderer, and show the errors when it fails, as the [live previews](/contributing/components) on this site do. The renderer expects a valid `Well`; it reports drawing problems through `onError` but does not validate. # Zoom, pan and highlights > Control zoom and pan, build zoom buttons that stay in sync with the mouse, and highlight intervals or items of the profile from your own UI. ## Zoom and pan With `zoom` and `pan` on (as in `INTERACTIVE_RENDER_CONFIG`), the mouse wheel zooms and dragging pans each panel. Turn them off where they would get in the way, for example on a page that scrolls: ```ts renderConfig: { ...applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'), zoom: false, pan: false } ``` The live previews on this site do exactly that, so the wheel scrolls the page. ## Your own zoom buttons `zoomBy`, `resetZoom` and `getZoomScale` drive the same zoom behavior as the mouse, so a toolbar and the wheel stay in sync. `onZoom` reports the scale after every change, from either source: ```ts const renderer = new WellRenderer(svgs, { renderConfig: { ...INTERACTIVE_RENDER_CONFIG, zoom: true, pan: true }, onZoom: scale => (scaleLabel.textContent = `${Math.round(scale * 100)} %`), }); zoomInButton.onclick = () => renderer.zoomBy(1.25); zoomOutButton.onclick = () => renderer.zoomBy(1 / 1.25); fitButton.onclick = () => renderer.resetZoom(); // back to renderConfig.zoomLevel (1 = fit) ``` A scale of `1` is the initial fit. `renderConfig.minZoomScale` and `maxZoomScale` (default 0 and 150) bound the wheel, drag, `zoomBy` and `resetZoom` alike. `zoomBy` does nothing when both zoom and pan are off. ## Highlights Highlights draw an outline and an optional label over parts of the profile, for example the row a user hovers in a table, or the interval a validation message points at. Pass them to `draw`: ```ts renderer.draw(well, { highlights: { well_screen: [{ from: 36, to: 54, label: 'Screen 1' }], lithology: [{ from: 8, to: 30 }], fractures: [{ depth: 67, label: 'Water entry' }], }, }); ``` Each key targets one component (`lithology`, `caves`, `fractures`, `bore_hole`, `surface_case`, `hole_fill`, `well_case`, `well_screen`, `reduction`, and `cement_pad` with a single item). An item gives the interval (`from`, `to`) or point (`depth`) to mark, an optional `diameter`, and an optional `label`. Draw again without `highlights` to clear them. ## Tooltips Hovering a layer shows a tooltip with its data. Limit or turn them off with `renderConfig.tooltips` ([Layers and labels](/render/guides/layers-and-labels#tooltips)), and restyle them with the `tooltip` class names ([Theming](/render/guides/theming#class-names)). ## Stable updates while editing When the user edits the well, `draw` animates from the old drawing to the new one. D3 matches old and new items by key; give each array item a stable `key` (the `RenderableWell` type allows it) so an inserted layer does not make every layer below it animate. Keys are runtime only: remove them before serializing the file, since the format does not define them. # Layers and labels > What each layer of the profile shows and which .well block it comes from, and how to turn on depth labels, descriptions, unit strips, construction labels and the optional layers. The renderer draws each block of the file as a layer. This page lists them, the data each one reads, and the options that control it. Options live in `renderConfig`, which is merged over `INTERACTIVE_RENDER_CONFIG`. ## The layers | Layer | Reads | Notes | | ------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Lithology column | `lithology` | Fill color from `color`, pattern from `texture` (FGDC codes with a drawn pattern). | | Borehole | `bore_hole` | Open at the mouth, a shoulder at each diameter change, a line at total depth. | | Casing and screens | `well_case`, `well_screen`, `surface_case` | Walls only, open at joints. A filled casing shoe at the bottom of the string, dashed lines at each screen's ends. | | Reductions | `reduction` | Two slanted walls. | | Annulus | `hole_fill` | Gravel pack and seal fill only the annulus, with a line at each change of material. | | Centralizers | `centralizers` | Bow markers at each derived position. | | Wellhead pad | `cement_pad` | Two slabs on the surface, drawn at a fixed size with break lines (not to scale); dimensions show in the tooltip. | | Pump | `pump_installations` | The installation without `removed_at`: a riser from the wellhead and a pump body ending at `intake_depth`. Earlier installations are not drawn. | | Fractures | `fractures` | Single or swarm, at their dip, with a mark when they yield water. | | Caves | `caves` | Cavity zones with wavy contacts, dry or with water. | Construction is drawn at true depth and to a diameter scale, so a telescoping well reads as it was built. A casing with a negative `from` rises above the ground ([Panels and layout](/render/guides/panels-and-layout)). Toggle individual construction details in `renderConfig.construction` (for example `casingShoe.active`, `screenEnds.active`, `pump.active`). ## Labels The interactive preset draws no text labels, which keeps the drawing clean on a screen where tooltips carry the details. Turn them on as needed: ```ts import { INTERACTIVE_RENDER_CONFIG, WellRenderer, applyRenderLocale, } from '@welldot/render'; const base = applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'); const renderer = new WellRenderer(svgs, { locale: 'en', renderConfig: { ...base, labels: { ...base.labels, active: true }, // depth, description and divider of each layer unitLabels: { ...base.unitLabels, active: true }, // geologic and aquifer unit strips constructionLabels: { ...base.constructionLabels, active: true }, // casing and screen labels }, }); ``` - `labels.active` is `true`, `false` or a list of the label groups to show (`['lithology', 'fractures', 'caves']`). - `labels.lithology` picks the parts of each lithology annotation: `'depth'`, `'description'`, `'dividers'`, plus `'units'` (bold geologic unit and italic aquifer unit), `'swatch'` (a square of the layer's fill) and `'rules'` (a rule above each annotation). - `labels.continuation.active` marks layers and construction items that started on a previous panel with `↑`. - `unitLabels.active` draws the `geologic_unit` and `aquifer_unit` strips beside the lithology column. - `constructionLabels.side` is `'left'` (each label next to its casing wall, the default) or `'right'` (one aligned column to the right of the construction). Descriptions need horizontal room. `STATIC_RENDER_CONFIG` reserves it on the right of the drawing; with the interactive preset, widen the panel or adjust `renderConfig.geologic.xRightInset`. ## Optional layers for printed output These are off by default and mostly useful for paged output such as PDF reports: | Option | Draws | | ------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `columnHeaders` | Titles over the depth, lithology, construction and description columns. | | `overview: { active, x, width, height }` | A mini-map of the whole well's lithology in the left margin, with the panel's window outlined. | | `markers: { lithology, fractures, radius }` | A numbered circle on each lithology layer and `F1`, `F2`… next to each fracture, to match a table. | | Overlap band | The hatched band of a panel with `repeatedTo`, labelled from `overlap.label`. | ## Tooltips With tooltips on (the interactive preset), hovering a layer shows its data: depths, diameters, materials, descriptions, the pump's rated power. `renderConfig.tooltips` takes `false` or a list of the tooltip kinds to keep: `geology`, `hole`, `surfaceCase`, `holeFill`, `wellCase`, `wellScreen`, `reduction`, `centralizer`, `pump`, `conflict`, `fracture`, and more. Tooltip text comes from `tooltipLabels`, set by `applyRenderLocale`. All the options are typed in `RenderConfig`; see [`INTERACTIVE_RENDER_CONFIG`](/render/api/variables/INTERACTIVE_RENDER_CONFIG) in the API reference for the full default values. # Legends > Explain the symbols of a profile with a ready-made SVG legend, or build your own legend layout from the legend entries and per-symbol drawings. A profile uses symbols for materials and features: gravel pack and seal, casing and screen, fractures that yield water, dry and wet caves. A legend explains them. `@welldot/render` offers a ready-made legend panel and the building blocks for your own. ## A legend panel ```html ``` ```ts renderer.renderLegend('#legend', well); ``` `renderLegend(selector, well)` draws a horizontal legend with only the entries the well needs, labelled from `renderConfig.legend.labels` (English after `applyRenderLocale(config, 'en')`, see [Locale and units](/render/guides/locale-and-units)). Without a renderer instance, `drawWellLegend` does the same: ```ts import { INTERACTIVE_RENDER_CONFIG, applyRenderLocale, drawWellLegend, } from '@welldot/render'; const { legend } = applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'); drawWellLegend('#legend', well, { config: legend }); ``` It sets the SVG to 0 × 0 when the well needs no legend entry. Without `config` the labels are the Portuguese defaults. ## Your own layout To put the legend in a table, an HTML list or a PDF, list the entries and draw each symbol on its own: ```ts import { drawWellLegendSymbol, getWellLegendItems } from '@welldot/render'; const items = getWellLegendItems(well); // [{ key: 'boreHole', label: '…' }, …] in display order const list = document.querySelector('#legend-list')!; items.forEach((item, i) => { const li = document.createElement('li'); li.innerHTML = ` ${item.label}`; list.append(li); drawWellLegendSymbol(`#symbol-${i}`, item); }); ``` Each symbol is a 28 × 16 px SVG with every texture it uses defined inside, so its `outerHTML` renders on its own, for example when embedded in a PDF. On a renderer instance, `renderer.getLegendItems(well)` and `renderer.renderLegendSymbol(selector, item)` do the same with the renderer's labels and theme. Entry keys are the keys of `renderConfig.legend.labels`: `boreHole`, `surfaceCase`, `holeFillGravel`, `holeFillSeal`, `wellCase`, `wellScreen`, `reduction`, `centralizer`, `pump`, `cementPad`, `fractureSingle`, `fractureSwarm`, `fractureWater`, `caveDry`, `caveWet`, `conflict`. ## Lithology swatches Lithology layers are explained by their description rather than a fixed legend. To show a layer's fill next to its description in a table, draw a swatch after `prepareSvg()`: ```ts well.lithology.forEach((layer, i) => { renderer.renderLithologySwatch(`#swatch-${i}`, layer, 12); // a 12 × 12 px square }); ``` # Locale and units > Draw the profile's labels, tooltips and legend in English or Portuguese, and show depths in meters or feet and diameters in millimeters or inches. The file always stores meters and millimeters ([Units](/format/spec/v2/format-reference#units)). What the drawing shows is up to you: the renderer converts on display, and resolves every word it draws from its label pack. ## Language For historical reasons the renderer draws Portuguese by default. For English, set the locale and resolve the English labels into the configuration: ```ts import { INTERACTIVE_RENDER_CONFIG, WellRenderer, applyRenderLocale, } from '@welldot/render'; const renderer = new WellRenderer(svgs, { locale: 'en', renderConfig: applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'), }); ``` Both parts are needed: - `locale` controls what the renderer formats directly, such as the inch symbol (`"` in Portuguese, `in.` in English). - `applyRenderLocale(config, locale)` returns a copy of the configuration with every label resolved from the built-in `RENDER_LABELS` pack: construction label prefixes, legend entries, tooltip titles and fields, fracture and cave type words, the continuation prefix, column headers and the overlap label. Everything else in the configuration is unchanged. To use your own wording, change the resolved strings after `applyRenderLocale`, for example `config.legend.labels.wellScreen = 'Screen'`. `RENDER_LABELS`, `resolveRenderLabel` and the `RenderLabelPack` type are exported for building a full pack in another language. The text inside the data, such as lithology descriptions and unit names, is drawn as written in the file. ## Units ```ts new WellRenderer(svgs, { units: { length: 'ft', diameter: 'inches' }, locale: 'en', renderConfig: applyRenderLocale(INTERACTIVE_RENDER_CONFIG, 'en'), }); ``` `units` is the `Units` type from `@welldot/core`: `length` is `'m'` or `'ft'` and `diameter` is `'mm'` or `'inches'`. It sets the depth scale, the labels and the tooltips. The pump's rated power in its tooltip follows `units.power` (`'kW'`, `'cv'` or `'hp'`). Units and locale can also change per draw, without a new renderer: ```ts renderer.draw(well, { units: { length: 'm', diameter: 'mm' } }); ``` A per-draw `locale` changes only what `locale` controls (the inch symbol). The labels stay as resolved in the configuration; to switch language fully, create a renderer with the other `applyRenderLocale` result. ## Format helpers The same formatting is exported for your own tables and legends: | Function | Example | | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `formatLength(m, units, locale)` | depth in meters to a number string, without the unit symbol; `units` is `'m'` or `'ft'`, `locale` is `'en'` or `'pt'` | | `formatDiameter(mm, units, locale)` | diameter in millimeters to a number string, without the unit symbol; `units` is `'mm'` or `'inches'`, `locale` is `'en'` or `'pt'` | | `getLengthUnit(units)` (pair with `formatLength`) | `'m'` or `'ft'` | | `getDiameterUnit(units, locale?)` | `'mm'`, or `'"'` / `'in.'` | ```ts import { formatDiameter, formatLength } from '@welldot/render'; // units is the single value, not the { length, diameter } object; no unit symbol is added formatLength(45.72, 'ft', 'en'); // '150' formatDiameter(168.3, 'inches', 'pt'); // '6,63' ``` For other conversions, use the [unit conversion](/core/guides/unit-conversion) helpers of `@welldot/core`. # Panels and layout > Size the drawing, split a long well over several panels, choose each panel's depth window, repeat bands between pages, and handle casing stick-up. A `WellRenderer` draws into one or more panels, each an `` element described by an `SvgInstance`: ```ts type SvgInstance = { selector: string; // CSS selector of the width: number; // drawing width in px, without margins height: number; // drawing height in px, without margins margins: { top: number; right: number; bottom: number; left: number }; depthRange?: [number, number]; // depth window in m repeatedTo?: number; // end of a band repeated from the previous panel, in m }; ``` The SVG element is sized to `width + left + right` by `height + top + bottom`. Keep about 40 to 50 px on the left for the depth scale. ## One panel With one panel and no `depthRange`, the whole well is drawn to fit the height. The depth scale is linear, so a 1561 m well and a 10 m well both fill the panel. ## Several panels Long wells drawn in one panel lose detail. Give several panels and, by default, `draw()` splits the depth between them in proportion to their heights, back to back: ```ts const margins = { top: 24, right: 16, bottom: 20, left: 48 }; new WellRenderer([ { selector: '#part-1', width: 380, height: 700, margins }, { selector: '#part-2', width: 380, height: 700, margins }, { selector: '#part-3', width: 380, height: 700, margins }, ]); ``` Place the three SVGs side by side to read the well as columns, as on a printed profile. ## Choosing depth windows Give **every** panel a `depthRange` to choose the windows yourself. Panels may then overlap or skip depths: ```ts new WellRenderer([ { selector: '#p1', width: 480, height: 600, margins, depthRange: [0, 124] }, { selector: '#p2', width: 480, height: 700, margins, depthRange: [116, 292], repeatedTo: 124, }, ]); ``` `repeatedTo` marks the band `[116, 124]` of the second panel as a repetition of the end of the first. It is hatched and labelled using `renderConfig.overlap.label` (`{range}` becomes `116–124 m`). This is how paged PDF output repeats a few meters at the top of each page. ## Stick-up and the top of the drawing Depth zero is the ground surface. A casing, screen or surface case with a negative `from` sticks up above it ([depths and intervals](/format/concepts/depths-and-intervals)), and the renderer draws it to scale above the wellhead pad. The default split therefore starts at the highest stick-up, not at 0. `getProfileTopDepth(well)` returns that depth, so you can start your first `depthRange` there: ```ts import { getProfileTopDepth } from '@welldot/render'; const top = getProfileTopDepth(well); // e.g. -0.5 const ranges: [number, number][] = [ [top, 120], [120, 265], ]; ``` ## Responsive width The renderer does not observe its container. To follow the page width, create a new renderer when the container resizes (debounced), as the live previews on this site do: ```ts const observer = new ResizeObserver(() => { clearTimeout(timer); timer = setTimeout(redraw, 150); }); observer.observe(container); ``` where `redraw` builds a `WellRenderer` with the container's `clientWidth` minus the margins, calls `prepareSvg()` and draws. ## Column proportions `renderConfig.layout` sets how much of the width the construction column takes (`pocoWidthRatio`) and where it sits (`pocoCenterRatio`); `renderConfig.geologic` sets the lithology column's insets. The two presets differ here: `INTERACTIVE_RENDER_CONFIG` is laid out for a screen, `STATIC_RENDER_CONFIG` leaves room on the right for description annotations, as in printed reports. # Theming > Change the colors, strokes and fonts of the profile with a WellTheme, set up a dark theme that follows your site, and rename the SVG classes. Every color, stroke width and opacity the renderer uses is part of a `WellTheme`. Pass the values you want to change; the rest come from `DEFAULT_WELL_THEME`. ```ts import { WellRenderer } from '@welldot/render'; new WellRenderer(svgs, { theme: { lithology: { stroke: '#222222', strokeWidth: 1.5 }, wellCase: { fill: '#f5f5f5', stroke: '#333333', strokeWidth: 2 }, reduction: { fill: '#f5f5f5', stroke: '#333333', strokeWidth: 2 }, }, }); ``` The theme has one group per layer (`lithology`, `lithologyTexture`, `cave`, `fracture`, the construction groups such as `boreHole`, `wellCase`, `wellScreen`, `holeFill`, `centralizer`, `pump`), the label groups (`labels`, `constructionLabels`, `unitLabels`), and the groups of the optional layers (`legend`, `overview`, `columnHeaders`, `markers`, `overlap`). [`DEFAULT_WELL_THEME`](/render/api/variables/DEFAULT_WELL_THEME) in the API reference lists every key with its default. Lithology colors come from the data (`lithology[].color`), not from the theme. The theme styles the outlines, patterns and everything around them. ## Dark mode The default theme is for light backgrounds. On a dark page, override the label and annotation colors. This is the override the welldot.org editor and this site use: ```ts import type { DeepPartial, WellTheme } from '@welldot/render'; export const DARK_THEME: DeepPartial = { labels: { color: 'rgba(255,255,255,0.55)', bodyColor: 'rgba(255,255,255,0.40)', dividerStroke: 'rgba(255,255,255,0.15)', annotationBg: '#1a2230', depthTipFill: '#2a3344', }, constructionLabels: { labelFill: '#1a2230', labelColor: 'rgba(255,255,255,0.65)', }, unitLabels: { geologicFill: '#2a3344', aquiferFill: '#1e3250', stroke: 'rgba(255,255,255,0.15)', }, }; ``` The theme is fixed when the renderer is created. To follow a color mode switch, create a new renderer with the other theme and draw again: ```ts function render(dark: boolean) { const renderer = new WellRenderer(svgs, { renderConfig, ...(dark && { theme: DARK_THEME }), }); return renderer.prepareSvg().then(() => renderer.draw(well)); } // e.g. watch a class on new MutationObserver(() => render(document.documentElement.classList.contains('dark')), ).observe(document.documentElement, { attributes: true, attributeFilter: ['class'], }); ``` ::tip Test both themes with a well that has description labels and unit strips turned on: they are the parts that change most between light and dark. :: ## Patterns The lithology patterns are SVG textures built with `textures.js` from the FGDC codes. `renderConfig.textures` adjusts them (for example the wellhead pad's concrete texture, `textures.pad`). The [FGDC texture reference](/format/spec/reference/fgdc-textures) lists which codes have a drawn pattern; a code without one falls back to the layer color. ## Class names Every SVG element gets a class, so you can also style the drawing with CSS or hook tests on it. Rename the classes with the `classNames` option (a partial `ComponentsClassNames`), for example to fit a CSS naming scheme: ```ts new WellRenderer(svgs, { classNames: { tooltip: { root: 'my-tooltip' } }, }); ``` # AI and agents > How AI assistants can read these docs, validate .well files and convert well reports, through the MCP server, llms.txt and the welldot-converter skill. AI assistants are often the fastest way to turn a scanned drilling report into data, or to write code against a file format. They are also confident when they are wrong. These tools give an assistant the real specification and the real validator, so its answers and its `.well` files can be checked instead of trusted. ::card-group :::card{title="MCP server" icon="i-lucide-plug" to="/ai/mcp"} Connect Claude, Cursor, VS Code or another MCP client to search these docs, read any page and validate `.well` files. ::: :::card{title="llms.txt and Markdown" icon="i-lucide-file-text" to="/ai/llms"} Every page as plain Markdown, an index for assistants, and the whole site in one file. ::: :::card{title="Converting reports" icon="i-lucide-wand-sparkles" to="/ai/conversion-skill"} The `welldot-converter` skill turns a well report into a `.well` file, and checks it with the validator. ::: :: ## Two common cases **A geologist converting reports.** The assistant reads a PDF or a photo of a drilling report, follows the [`welldot-converter` skill](/ai/conversion-skill) to extract every section, writes the file, and runs `validate_well` on the [MCP server](/ai/mcp) until it passes. The geologist then reviews the result against the report. **A developer writing code.** The assistant looks up fields and rules with `search_docs` and `get_page` instead of guessing, and validates the test files it writes. Without MCP, give it [`/llms.txt`](/ai/llms) or the Markdown of the pages you are working from. ## What the tools guarantee - **The same validator.** `validate_well` runs `parseWell` from `@welldot/core`, exactly as the welldot.org editor and the libraries do. It adds no rules of its own ([Validating](/format/guides/validating)). - **The real spec.** `search_docs` and `get_page` read the same pages you are reading now, including the full [specification](/format/spec). - **Nothing stored.** The MCP server is stateless and read-only. It does not keep the files you validate. ::caution A valid file can still be wrong. Validation checks the shape of the data, not whether the numbers match the report. Always review a converted file against its source ([Converting a report](/format/guides/converting-a-report#review-checklist)). :: # Converting reports with an assistant > Use the welldot-converter Agent Skill to turn water well reports in any language into validated .well files with Claude, ChatGPT, Gemini, Cursor or Codex. `welldot-converter` is an [Agent Skill](https://agentskills.io): a written procedure that an AI assistant loads and follows. It turns a water well report (PDF, Word document, scan, photo or text, in Portuguese, English, Spanish or another language) into a `.well` file, then reports what it found and what it left out. The skill lives in the repository at [`.agents/skills/welldot-converter/`](https://github.com/welldothq/welldot/tree/main/.agents/skills/welldot-converter). `SKILL.md` is the procedure, and `references/well-spec.md` is an offline copy of the spec for assistants that cannot browse. ## What it does 1. **Gets the spec.** Reads the current specification, or the bundled copy when offline. 2. **Reads the report**, with the assistant's own file and vision tools. 3. **Extracts the well**: identity, location, construction, lithology, tests, analyses, operation, permits and history, section by section ([what goes where](/format/getting-started/for-geologists)). 4. **Validates** the result: required fields, intervals, units, datetimes with offsets, closed vocabularies. 5. **Delivers** the file with a summary of the sections found, the fields left out on purpose and the free-text values it kept as written. 6. **Asks** about ambiguities instead of guessing. Its rules match the [conversion guide](/format/guides/converting-a-report): never estimate a number, keep the report's wording, convert units exactly, write datetimes with an offset, keep the drilled depth out of `well_depth`. ## Set it up ::tabs :::tabs-item{label="Claude Code, Codex, Gemini CLI, Cursor" icon="i-lucide-terminal"} Clone the repository, or copy `.agents/skills/welldot-converter/` into your own project. These clients discover skills in `.agents/skills/` (Claude Code through the `.claude/skills/` link) with no configuration. The assistant activates the skill when you ask it to convert a report. ::: :::tabs-item{label="Claude.ai" icon="i-lucide-message-square"} Upload the `welldot-converter` folder as a skill in **Settings → Capabilities → Skills**, then attach a report and ask for a `.well` file. ::: :::tabs-item{label="ChatGPT, Gemini Gems" icon="i-lucide-message-circle"} These do not load skill folders. Create a ChatGPT Project or a Gemini Gem, add `SKILL.md` and `references/well-spec.md` as knowledge files, and paste the instruction block from the skill's [README](https://github.com/welldothq/welldot/blob/main/.agents/skills/welldot-converter/README.md) into the instructions. ::: :: ## Close the loop with validation Connect the same assistant to the [docs MCP server](/ai/mcp). The skill tells it to run `validate_well` on every file it produces and to fix each reported error before handing the file over. With MCP connected, it can also look up a field in the spec with `search_docs` instead of relying on memory. ```mermaid sequenceDiagram participant U as You participant A as Assistant + welldot-converter participant M as docs MCP server U->>A: Report (PDF, photo) + "convert to .well" A->>M: search_docs / get_page (field rules) A->>A: Extract and write the file A->>M: validate_well(content) M-->>A: errors with paths A->>A: Fix A->>M: validate_well(content) M-->>A: valid, warnings A-->>U: .well file + summary of what was left out ``` ## Review the result The assistant's summary tells you where to look. Then: - Open the file in [welldot.org](https://welldot.org) and compare the drawn profile with the report's drawing. - Check the numbers in the tables: depths, diameters, levels, rates. - Read the fields it says it left out, and fill them only from the report. The [review checklist](/format/guides/converting-a-report#review-checklist) has the full list. ::caution Conversion quality depends on the assistant and on the scan. Handwritten logs, low-resolution scans and tables split across pages cause most errors. Treat every converted file as a draft until a person has checked it. :: # llms.txt and Markdown pages > Give an AI assistant the welldot docs as plain text, with llms.txt, llms-full.txt, the raw Markdown of every page and the page actions. Not every assistant supports MCP. Every page of this site is also available as plain Markdown, and two index files follow the [llms.txt](https://llmstxt.org) convention, so any assistant that can read a URL or a pasted file can use the docs. ## The files | URL | Contents | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | A short description of welldot and a link to the Markdown of every page, grouped by section, with a one-line summary each. | | [`/llms-full.txt`](/llms-full.txt) | Every hand-written page and the whole specification in one file. Large; use it when the assistant can hold it. | | `/raw/.md` | The Markdown source of one page, for example [`/raw/format/spec/v2/format-reference.md`](/raw/format/spec/v2/format-reference.md). | The generated API and object reference pages are listed in `llms.txt` only through their index pages, to keep it short; their raw Markdown is still available at `/raw/...`. `welldot.org/llms.txt` points to these files. ## On every page The links at the bottom of each page's table of contents (or at the side, on pages without one): - **Copy page** copies the page's Markdown to the clipboard, ready to paste into a chat. - **View as Markdown** opens the raw page. - **Edit this page** opens its source on GitHub, for hand-written and spec pages. ## Which to use | Situation | Use | | ------------------------------- | -------------------------------------------------------------------------------------------- | | The assistant supports MCP | The [MCP server](/ai/mcp): it searches and reads only what it needs, and can validate files. | | A question about a few pages | **Copy page** on each and paste them. | | A tool that indexes a docs site | `/llms.txt` as the entry point. | | A long session on the format | `/llms-full.txt`, if it fits the assistant's context. | ::tip When an assistant writes a `.well` file from the Markdown alone, it cannot check it. Paste the result into [welldot.org](https://welldot.org) or validate it in code before using it ([Validating](/format/guides/validating)). :: # MCP server > Connect an AI assistant to the welldot docs MCP server to search the documentation, read pages and validate .well files, with setup for Claude Code, Claude Desktop, Cursor, VS Code and Gemini CLI. This site runs a [Model Context Protocol](https://modelcontextprotocol.io) server. An assistant connected to it can search these docs, read any page, including the specification, and validate `.well` files with the same check the libraries use. ```text https://docs.welldot.org/mcp ``` The server uses the Streamable HTTP transport, is stateless and needs no account or key. ## Tools | Tool | Input | Returns | | --------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `search_docs` | `query`, optional `limit` (default 10, at most 50) | The best matching sections, each with a path, URL and snippet. | | `get_page` | `path`, for example `/format/spec/v2/overview` | The page as Markdown. A trailing slash, `.md` suffix or `#anchor` is ignored. | | `validate_well` | `content` (the file's text), optional `warnings` (default `true`) | `{ valid: true, version, warnings }` or `{ valid: false, errors }`, with the JSON Schema URL. | The server also tells the assistant to validate every `.well` file it generates or edits. ### `validate_well` in detail `validate_well` runs `parseWell` from [`@welldot/core`](/core/guides/parsing-and-validating), nothing more and nothing less. Errors come back with a path into the document: ```json { "valid": false, "errors": [ { "path": "well_screen[0].screen_slot", "code": "invalid_type", "message": "Required" }, { "path": "fractures", "code": "invalid_type", "message": "Required" } ], "schema": "https://welldot.org/schema/v2/well.schema.json" } ``` - Non-JSON text, an empty string, a JSON array or an unsupported version return `valid: false` with a readable message, never a server error. - A version 1 file is valid: it is upgraded the same way `parseWell` upgrades it. - Warnings (permit, meter, water sample and pump checks from [`@welldot/utils`](/utils)) are advisory and never make a file invalid ([Validating](/format/guides/validating#errors-and-warnings)). - Files over 5 MB return the code `too_large`. ## Setup The server works with CLI and desktop MCP clients that support remote (HTTP) servers. It sends no CORS headers, so MCP clients that run inside a web page may not be able to connect. ::tabs :::tabs-item{label="Claude Code" icon="i-lucide-terminal"} ```bash claude mcp add --transport http welldot https://docs.welldot.org/mcp ``` Add `--scope project` to share it with your team through `.mcp.json`. ::: :::tabs-item{label="Claude Desktop" icon="i-lucide-monitor"} Open **Settings → Connectors → Add custom connector**, name it `welldot` and enter `https://docs.welldot.org/mcp`. ::: :::tabs-item{label="Cursor" icon="i-lucide-mouse-pointer-2"} In `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json { "mcpServers": { "welldot": { "url": "https://docs.welldot.org/mcp" } } } ``` ::: :::tabs-item{label="VS Code" icon="i-lucide-code"} In `.vscode/mcp.json`: ```json { "servers": { "welldot": { "type": "http", "url": "https://docs.welldot.org/mcp" } } } ``` ::: :::tabs-item{label="Gemini CLI" icon="i-lucide-sparkle"} In `~/.gemini/settings.json`: ```json { "mcpServers": { "welldot": { "httpUrl": "https://docs.welldot.org/mcp" } } } ``` ::: :: Client settings change often; if one of these does not work, check your client's documentation for "remote MCP server" or "Streamable HTTP". ## Try it Once connected, ask your assistant something like: - "Using the welldot docs, what does `well_screen[].slot_type` accept?" - "Read the welldot spec on level sign conventions and tell me how to record a flowing well." - "Validate this .well file and fix every error." (paste or attach the file) To test the endpoint without an assistant: ```bash curl -s https://docs.welldot.org/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` ## Privacy The server does not store what you send. A file passed to `validate_well` is parsed in memory and discarded when the response is sent. Avoid sending files you are not allowed to share with your AI provider; that concern is between you and the provider, not this server. ## For contributors `pnpm --filter docs dev` serves the same endpoint locally at `http://localhost:3001/mcp`. The implementation is in `apps/docs/server/mcp-lib`. # Welldot documentation > Documentation for .well, the open JSON format for water well data, and the @welldot TypeScript packages that read, validate, analyze and draw it. ::u-page-hero{orientation="horizontal"} #headline :badge{label="Format v2.4 · Apache 2.0" variant="subtle" color="neutral"} #title The record of a water well, [as data]{.landing-em}. #description `.well` is an open JSON format for the whole record of a water well, from construction and lithology to pumping tests, operation, permits, water quality and history. These docs cover the format, its specification and the libraries that read, validate, analyze and draw it. #links :u-button{to="/format" size="lg" trailing-icon="i-lucide-arrow-right" label="Read about the format"} :u-button{to="/format/spec" size="lg" color="neutral" variant="outline" icon="i-lucide-scale" label="Specification"} #default :::well-preview{example="coluna-mg-cristalino-abastecimento"} ::: :: ::u-page-section{headline="Start here" title="Two ways in"} :::u-page-grid{class="lg:grid-cols-2"} ::::u-page-card{title="I work with wells" icon="i-lucide-mountain" to="/format/getting-started/for-geologists" spotlight variant="subtle"} #description Geologists, hydrogeologists and drillers: how a drilling report maps onto a `.well` file, explained in field terms. Lithology, construction, tests, operation, permits and water quality, no code needed. :::: ::::u-page-card{title="I build software" icon="i-lucide-code" to="/format/getting-started/for-developers" spotlight variant="subtle"} #description Developers: read and validate files from TypeScript or any language with the JSON Schema, what the schema guarantees, and which parts of the spec matter for editors and databases. :::: ::: :: ::u-page-section{headline="The format" title="One file, one well, every stage of its life"} #description A `.well` file starts as a drilling report and grows with the well: tests, permits, meters, analyses. Every field has one meaning and one unit, written down in a versioned specification. :::u-page-grid ::::u-page-card{title="Plain JSON" icon="i-lucide-braces" to="/format/getting-started/what-is-well" variant="ghost"} #description Opens in any editor, parses in any language, diffs in git. Full field names, so a file reads without the spec. :::: ::::u-page-card{title="One unit per quantity" icon="i-lucide-ruler" to="/format/concepts/units-and-precision" variant="ghost"} #description Meters, millimeters, m³/h. The file never declares units, so two files of the same well hold the same numbers. :::: ::::u-page-card{title="Graceful incompleteness" icon="i-lucide-file-check" to="/format/getting-started/what-is-well#key-principles" variant="ghost"} #description Unknown values are left out, never invented. A registry record with only its construction is a valid file. :::: ::::u-page-card{title="Open to extension" icon="i-lucide-puzzle" to="/format/concepts/extensibility" variant="ghost"} #description `x-` fields carry what the spec does not define, and every reader keeps them. :::: ::::u-page-card{title="Neutral by design" icon="i-lucide-globe" to="/format/concepts/profiles" variant="ghost"} #description No national rules inside the file. Regulators add theirs with profiles; limits are chosen when reading. :::: ::::u-page-card{title="19 example wells" icon="i-lucide-library" to="/format/reference/examples" variant="ghost"} #description Complete files built on real Brazilian wells: karst, crystalline, sedimentary, flowing, hand-dug, messy. :::: ::: :: ::u-page-section{headline="Packages" title="TypeScript libraries, framework-free"} #description The reference implementation of the format, published on npm under Apache 2.0. No UI framework required. :::u-page-grid ::::u-page-card{title="@welldot/core" icon="i-lucide-box" to="/core" spotlight} #description Types, Zod validators, serialization, unit conversion, vocabularies with English and Portuguese labels, water quality limit sets. :::: ::::u-page-card{title="@welldot/utils" icon="i-lucide-ruler" to="/utils" spotlight} #description Gravel pack volumes, drawdown and specific capacity, permit status and deadlines, production totals, hydrochemistry. :::: ::::u-page-card{title="@welldot/render" icon="i-lucide-layers" to="/render" spotlight} #description A D3 renderer that draws the profile in SVG: FGDC lithology patterns, construction as built, zoom, tooltips, legends. :::: ::: :: ::u-page-section{headline="AI and agents" title="Docs your assistant can check against" orientation="horizontal"} #description Connect Claude, Cursor, VS Code or Gemini CLI to the docs MCP server. Your assistant searches the spec instead of guessing, and validates every `.well` file it writes with the same `parseWell` the libraries use. #links :u-button{to="/ai/mcp" trailing-icon="i-lucide-arrow-right" label="Set up the MCP server"} :u-button{to="/ai/conversion-skill" color="neutral" variant="outline" label="Convert reports with an assistant"} #default ```bash [Claude Code] claude mcp add --transport http welldot https://docs.welldot.org/mcp # Tools your assistant gets: # search_docs search the spec, guides and API docs # get_page read any page as Markdown # validate_well check a .well file, errors with paths ``` ::