mostlyright-data 0.19.0__tar.gz → 0.19.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/PKG-INFO +7 -5
  2. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/README.md +6 -4
  3. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/pyproject.toml +1 -1
  4. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/skills/mr-data-build/SKILL.md +283 -149
  5. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/skills/mr-data-build/agents/openai.yaml +1 -1
  6. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/.gitignore +0 -0
  7. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/scripts/hatch_build.py +0 -0
  8. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  9. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/__init__.py +0 -0
  10. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/canonical.py +0 -0
  11. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/formats.py +0 -0
  12. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  13. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/key_seam.py +0 -0
  14. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/page_coverage.py +0 -0
  15. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  16. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/session_probes.py +0 -0
  17. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/skill_assets.py +0 -0
  18. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/table_manifest.py +0 -0
  19. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  20. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  21. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  22. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/activity.py +0 -0
  23. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  24. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/categories.py +0 -0
  25. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/commands.py +0 -0
  26. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  27. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/download.py +0 -0
  28. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  29. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/parity.py +0 -0
  30. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/probe.py +0 -0
  31. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  32. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/propose.py +0 -0
  33. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  34. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/research.py +0 -0
  35. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/router.py +0 -0
  36. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/runs.py +0 -0
  37. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/session.py +0 -0
  38. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/stream.py +0 -0
  39. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  40. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/transport.py +0 -0
  41. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  42. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4.py +0 -0
  43. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  44. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  45. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  46. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  47. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  48. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  49. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  50. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  51. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  52. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  53. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  54. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  55. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  56. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  57. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  58. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  59. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  60. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  61. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  62. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  63. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  64. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  65. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  66. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  67. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  68. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/login.py +0 -0
  69. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  70. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  71. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  72. {mostlyright_data-0.19.0 → mostlyright_data-0.19.2}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.19.0
3
+ Version: 0.19.2
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -111,10 +111,12 @@ worker executables or image publisher.
111
111
 
112
112
  ## Documentation
113
113
 
114
- - [CLI guide](https://mostlyright.md/docs/guides/cli/)
115
- - [Hosted research sessions](https://mostlyright.md/docs/guides/hosted-research/)
116
- - [Credentials](https://mostlyright.md/docs/guides/credentials/)
117
- - [Public datasets](https://mostlyright.md/docs/guides/use-public-datasets/)
114
+ - [Install and sign in](https://mostlyright.md/docs/start/install/)
115
+ - [Build your first dataset](https://mostlyright.md/docs/start/first-dataset/)
116
+ - [CLI reference](https://mostlyright.md/docs/reference/cli/)
117
+ - [Recipe reference](https://mostlyright.md/docs/reference/recipe/)
118
+ - [Recipe examples](https://mostlyright.md/docs/recipes/)
119
+ - [Use a public dataset](https://mostlyright.md/docs/guides/use-public-datasets/)
118
120
  - [Certified document extraction](docs/DOCUMENT-EXTRACTION.md)
119
121
 
120
122
  Use `mr-data --help` for the full command list and options. Commands that support
@@ -99,10 +99,12 @@ worker executables or image publisher.
99
99
 
100
100
  ## Documentation
101
101
 
102
- - [CLI guide](https://mostlyright.md/docs/guides/cli/)
103
- - [Hosted research sessions](https://mostlyright.md/docs/guides/hosted-research/)
104
- - [Credentials](https://mostlyright.md/docs/guides/credentials/)
105
- - [Public datasets](https://mostlyright.md/docs/guides/use-public-datasets/)
102
+ - [Install and sign in](https://mostlyright.md/docs/start/install/)
103
+ - [Build your first dataset](https://mostlyright.md/docs/start/first-dataset/)
104
+ - [CLI reference](https://mostlyright.md/docs/reference/cli/)
105
+ - [Recipe reference](https://mostlyright.md/docs/reference/recipe/)
106
+ - [Recipe examples](https://mostlyright.md/docs/recipes/)
107
+ - [Use a public dataset](https://mostlyright.md/docs/guides/use-public-datasets/)
106
108
  - [Certified document extraction](docs/DOCUMENT-EXTRACTION.md)
107
109
 
108
110
  Use `mr-data --help` for the full command list and options. Commands that support
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.19.0"
3
+ version = "0.19.2"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mr-data-build
3
- description: Build and verify a reproducible Parquet dataset from a data question and supported sources on the hosted Mostly Right backend. Use whenever a user asks to use, test, smoke-test, demonstrate, or debug the Mostly Right Harness or mr-data CLI by producing, reviewing, or verifying a dataset.
3
+ description: Build and verify a reproducible Parquet dataset from a data question and supported sources on the hosted Mostly Right backend. Use whenever a user asks to use, test, smoke-test, demonstrate, or debug the Mostly Right Harness or mr-data CLI by producing, reviewing, or verifying a dataset. The published reference at https://mostlyright.md/docs/ is the contract.
4
4
  ---
5
5
 
6
6
  # Mostly Right Data Build
@@ -81,16 +81,49 @@ work, follow it, and read what the backend proved. Studio is the hosted release
81
81
 
82
82
  The product is hosted-only and the client is thin. The engine runs on the backend; `mr-data` is a
83
83
  pure-Python client that submits work and reads the records Studio holds.
84
- [Hosted parity](../../docs/HOSTED-PARITY.md) is the ledger of which name runs where.
84
+ [Install and sign in](https://mostlyright.md/docs/start/install/) states that one install and where
85
+ a command's work actually runs.
85
86
 
86
87
  Read this document in two parts. Everything except [Not hosted yet](#not-hosted-yet) uses only
87
88
  commands this product runs. [Not hosted yet](#not-hosted-yet) is the internal capability map for
88
89
  the one command with no implementation behind it.
89
90
 
90
- Every `../../docs/NAME.md` below names a file in the `mostlyrightmd/mostlyright-data-harness`
91
- repository. This document is installed on its own, so those are references and not links you can
92
- follow from here: everything you need to run a build is in this file, and each of them is the
93
- longer reference behind one section of it.
91
+ Every link below is a page of the published reference at https://mostlyright.md/docs/, and every
92
+ one of them can be fetched from here. This document is installed on its own and remains the whole
93
+ of what a build needs; each page is the longer, current reference behind one section of it.
94
+
95
+ ## Reference pages
96
+
97
+ The published reference at https://mostlyright.md/docs/ carries the current contract for every
98
+ field, flag, code and ceiling this document names. Fetch the page for the shape in front of you
99
+ BEFORE drafting it, never guess a field name, and where a page disagrees with what you remember of
100
+ an older version, the page is right.
101
+
102
+ | What you are about to do | Fetch |
103
+ | --- | --- |
104
+ | Write a recipe document | https://mostlyright.md/docs/reference/recipe/ |
105
+ | Declare a check | https://mostlyright.md/docs/reference/checks/ |
106
+ | Plan a transform step | https://mostlyright.md/docs/reference/transforms/ |
107
+ | Name a unit | https://mostlyright.md/docs/reference/units/ |
108
+ | Choose a connector, a source kind or a connector parameter | https://mostlyright.md/docs/reference/sources/ |
109
+ | Pin a Reader | https://mostlyright.md/docs/reference/readers/ |
110
+ | Read any command's flags and `--json` shape | https://mostlyright.md/docs/reference/cli/ , then the group page: `auth`, `dataset`, `recipe`, `run`, `read`, `table`, `stream` under `/docs/reference/cli/` |
111
+ | Read an error code, a refusal or a run state | https://mostlyright.md/docs/reference/run-states-and-errors/ |
112
+ | Check a ceiling before starting a run | https://mostlyright.md/docs/reference/limits/ |
113
+ | Follow the whole lifecycle end to end | https://mostlyright.md/docs/start/how-it-works/ |
114
+ | Install the client and sign in | https://mostlyright.md/docs/start/install/ |
115
+
116
+ Read the worked recipe closest to the source in hand before writing your own.
117
+ https://mostlyright.md/docs/recipes/ lists all eight: `city-temperatures` (daily city
118
+ temperatures), `csv-snapshot` (one CSV file, one table), `json-api` (records pointer and
119
+ pagination), `html-collection` (many HTML pages, one table), `document-extraction` (PDF
120
+ projection), `weather-grib` (GRIB2 and the scientific Readers), `websocket-stream` (a `wss://`
121
+ venue), `multi-source-join` (two sources joined, with checks).
122
+
123
+ The build guides run one per stage of the loop below, each at https://mostlyright.md/docs/build/NAME/
124
+ where NAME is `probe-sources`, `write-a-recipe`, `run-and-inspect`, `publish-and-refresh`,
125
+ `credentials`, `live-streams`, `collections`, `documents` or `troubleshooting`; the list itself is
126
+ at https://mostlyright.md/docs/build/ and the reference index at https://mostlyright.md/docs/reference/.
94
127
 
95
128
  ## One install
96
129
 
@@ -117,7 +150,7 @@ gap rather than doing anything, and `export-hosted-candidate`, which is a backen
117
150
  | `mr-data login` | Complete device approval and store a device credential. `login --force` is the compatibility alias for safe `auth rotate`, never an in-place truncate. |
118
151
  | `mr-data whoami` | Use the compatibility alias for the remotely validated `auth status` answer. |
119
152
  | `mr-data clarify` | Say whether one clarifying question may be put to the user right now, and compose the record of their answer. It reaches nothing, asks nobody and waits for nothing. Exits non-zero when no person can answer or when a recipe is already registered. |
120
- | `mr-data probe` | Ask one registered source one question and print the answer. **Currently unusable:** it reaches `/v3/sessions`, which Studio refuses, and it takes a question identifier that no v4 receipt returns. Do not plan a source inspection around it. |
153
+ | `mr-data probe` | `probe SOURCE_ID QUESTION_ID --dataset DATASET_ID [--kind source_inspect\|sample_rows\|profile_columns\|evaluate_expression]` asks one already-registered source one question and prints the answer. **Do not plan a source inspection around it.** Both positionals are identifiers — `QUESTION_ID` is a question identifier, which no v4 registration receipt returns — and the command rides the frozen `/v3/sessions` routes, which a deployment may have switched off. Read a source instead by registering the recipe and taking an unwindowed sample under `--max-rows`, then `peek`, `query` and `receipt`. |
121
154
  | `mr-data catalog` | `catalog search "QUESTION"` asks the sealed public-source catalogue which of its entries might answer a question and ranks them best first. Every ranked entry comes back with the disposition its own facts earned — `admitted`, `human_escalation_required` or `refused` — because an entry the catalogue could not vouch for is still a finding. `--format csv` states the one data format the question requires — one token per question, never repeated — and it is not a filter: an entry that does not declare that format still comes back ranked, with disposition `refused` and `filters_match` false, so nothing is held back; `--limit N` ranks at most N, up to 25. It fetches nothing and registers nothing. The catalogue holds one provider (Data.gov) and only part of it, so it is never exhaustive and an empty answer is not evidence that no such source exists. When the answer is `catalog_unavailable`, this deployment has no catalogue to search: record the lane and carry on. |
122
155
  | `mr-data dataset` | Bring the dataset page into existence before there is anything on it, then fill it in while somebody watches. `dataset create --name TEXT` mints it and prints the `dataset_id`; `dataset show ID` reads it back; `dataset set ID --name TEXT --topics "a,b,c" --license ID --description-file F` writes the title, the descriptive tags, the SPDX licence and the description under the version it was read at, retrying once if somebody else wrote first, and an empty `--topics` or `--license` takes that value off the page; `dataset note ID --heading H --blocks-file B` writes one cell of the decision record that OUTLIVES every run, and `--list` reads it back; `dataset watch ID` follows the page's own event stream; `dataset activity ID --phase P --message TEXT` says what is happening right now, silently, and is never a chat message and never a cell; `dataset publish ID [--mode public|link|private]` says who can read the dataset — `public` lists it in the public directory and serves it at an address anybody can read, `link` serves it at an unlisted address, `private` takes it back to the workspace — and `dataset publish ID --show` reads that back without changing it; `dataset archive ID --confirm-name TITLE` retires the page and frees its title, deleting nothing. |
123
156
  | `mr-data recipe` | Register one recipe document — dataset, question, table plan, sources, transform, checks and units — in one call, and print the identifiers the server derived. `mr-data recipe show ID` reads one back. |
@@ -160,7 +193,7 @@ step between a run finishing and reading it: `download`, `receipt`, `checks`, `p
160
193
  answer about a run that is still workspace scratch exactly as they answer about one somebody has
161
194
  since made live.
162
195
 
163
- [The recipe document](../../docs/RECIPE-DOCUMENT.md) is the reference for what
196
+ [The recipe document](https://mostlyright.md/docs/reference/recipe/) is the reference for what
164
197
  `mr-data recipe FILE` sends. Two rules in it are worth knowing before you write one: no fractional
165
198
  number appears anywhere in the document, and you neither compute nor state a digest — the server
166
199
  canonicalizes the document, digests it, and answers with every identifier it derived. Sending the
@@ -172,8 +205,9 @@ signed something on a person's own computer, or they were named for a step the p
172
205
  performs — asking a person before the work starts, reading back a separate body of evidence,
173
206
  opening a session before a question could be asked. For a while each still typed and answered with
174
207
  where its work went; that ended when the refusing names outnumbered the working ones two to one.
175
- [Hosted parity](../../docs/HOSTED-PARITY.md) lists every one of them and what replaced it, and is
176
- the only place they are still written down.
208
+ [Install and sign in](https://mostlyright.md/docs/start/install/) states the one install and the
209
+ whole surface it carries; the deleted names and what replaced each of them are written down only in
210
+ the Harness repository's own `docs/HOSTED-PARITY.md`, which is not installed beside this document.
177
211
 
178
212
  Most of the survivors take a run identifier where the deleted local twin took a folder. That is
179
213
  the whole difference in the vocabulary: a hosted build has no folder, and the run identifier is
@@ -216,7 +250,7 @@ machine that is only using the product installs from `pip` instead.
216
250
  ## Cloud authentication preflight
217
251
 
218
252
  Authentication begins only when the next command will contact the Mostly Right cloud. Immediately
219
- before hosted `mr-data probe` or `mr-data recipe`, run
253
+ before hosted `mr-data dataset create` or `mr-data recipe`, run
220
254
  `mr-data whoami --json` and read its `status`, and it intentionally does not report
221
255
  `MOSTLYRIGHT_API_KEY`: when the variable is set, `whoami` validates that effective environment
222
256
  credential and reports stored-device metadata separately. It never prints the environment value and
@@ -394,23 +428,30 @@ provider or bypass it.
394
428
  reason codes only after the discovery lanes and source comparisons are complete. Claim
395
429
  `unsupported` only after every typed-refusal chain ends `lawful_routes_exhausted`.
396
430
 
397
- Reading a registered source's bytes before you commit to it is one command:
431
+ **Reading the bytes is the sample run, not `mr-data probe`.** `probe SOURCE_ID QUESTION_ID
432
+ --dataset DATASET_ID` takes two identifiers rather than a question in words, and the question
433
+ identifier is one no v4 registration receipt returns; the command also rides the frozen
434
+ `/v3/sessions` routes, which a deployment may have switched off. So the reading path on a hosted
435
+ workspace is the ordinary loop, one stage early: register the recipe, start an unwindowed sample
436
+ under a row ceiling, and read what came back.
398
437
 
399
438
  ```sh
400
- mr-data probe SOURCE_ID "does this feed carry city grain before 2021" --json
439
+ mr-data run --recipe RECIPE_ID --digest RECIPE_DIGEST --mode sample --max-rows 500 --json
440
+ mr-data peek RUN --json # the columns and types the run sealed
441
+ mr-data query RUN "select count(*) from run_table" --json
442
+ mr-data receipt RUN --json # what was fetched and what those bytes hashed to
401
443
  ```
402
444
 
403
- The session is plumbing and is implicit: it is opened or reused for that source, asked, and closed
404
- when the command returns, on the failure path as well as the success path, so a refused probe
405
- leaves no warm worker running. Keep its status, reason code, and exit value internal; when a probe
406
- result affects a source decision, describe only what it settled about the data.
445
+ That is stages 4 through 6 run once to settle the shape, and a sample that already meets the
446
+ requirements is the table a full run would seal. Where the deployment does answer a probe, keep its
447
+ status, reason code and exit value internal; when a probe result affects a source decision, describe
448
+ only what it settled about the data.
407
449
 
408
- **A probe answer is evidence, and it is cheap to reuse.** The v3 doctrine that a probe was never
409
- evidence is repealed. Every acquisition — a probe's or a run's — produces a receipt, and
410
- acquisitions are cacheable by source, query digest and window, so a probe's fetch feeds a sample
411
- built in the same warm session without paying for the bytes twice. In v4.0 that cache is
412
- session-scoped by owner ruling: it lives for the session's lifetime and no longer, so do not plan
413
- around a probe from an hour ago still being warm.
450
+ **An acquisition's answer is evidence, and it is cheap to reuse.** The v3 doctrine that a probe was
451
+ never evidence is repealed. Every acquisition produces a receipt, and acquisitions are cacheable by
452
+ source, query digest and window, so a fetch feeds a later build in the same warm session without
453
+ paying for the bytes twice. In v4.0 that cache is session-scoped by owner ruling: it lives for the
454
+ session's lifetime and no longer, so do not plan around a fetch from an hour ago still being warm.
414
455
 
415
456
  Where a source needs a credential, enrol it by name first — see
416
457
  [Source credentials](#source-credentials) — and reference the name from the recipe document. Where
@@ -480,7 +521,7 @@ mr-data recipe RECIPE.json --json
480
521
  One call. The server canonicalizes the bytes, computes the digest, and registers the dataset, the
481
522
  question, the sources, the connector configs and the table plan in one transaction, answering with
482
523
  `recipe_id`, `recipe_digest`, `dataset_id`, `table_id` and `source_ids`. Read
483
- [The recipe document](../../docs/RECIPE-DOCUMENT.md) for the shape of every member.
524
+ [The recipe document](https://mostlyright.md/docs/reference/recipe/) for the shape of every member.
484
525
 
485
526
  **Name the dataset the recipe binds to.** The document's `dataset` member takes an optional `id`,
486
527
  and it is the identifier stage 1 printed. With it, the registration attaches to the page somebody
@@ -596,7 +637,8 @@ seconds -- not a paragraph, and not a report. Use exactly this shape, in this or
596
637
  section out only when there is genuinely nothing to say (shown indented here; the document
597
638
  itself carries the headings at column one):
598
639
 
599
- Two plain sentences: what each row is, where it comes from, and the period it covers.
640
+ Two or three plain sentences: what the data is about, why it is worth a look, and what a
641
+ reader can see or do with it.
600
642
 
601
643
  ## Coverage
602
644
  - **Window:** the exact dates and the time zone.
@@ -618,17 +660,45 @@ itself carries the headings at column one):
618
660
 
619
661
  That is six sections: the opening paragraph, and the five headings under it.
620
662
 
621
- **Opening paragraph rules** (everything before the first `##` heading)
622
- 1. Two sentences, 150 to 240 characters together. The first sentence ends before character 200
623
- and is complete on its own.
624
- 2. Sentence one says what each row is, for where, over what window, from which publisher:
625
- `Each row is one hourly METAR observation at Prague Václav Havel Airport (LKPR), January 2000
626
- to August 2026, from the Iowa Environmental Mesonet ASOS archive.`
627
- 3. Sentence two gives the size and the measured quantities, as numbers and nouns:
628
- `43,812 rows of temperature, dew point, wind, pressure and visibility.`
629
- 4. Full names on first use with the abbreviation in parentheses once. Never "this dataset",
630
- "we", "it", "here".
631
- 5. No Markdown in the opening paragraph: no bold, no links, no bullets, no code.
663
+ **Opening paragraph** (everything before the first `##` heading)
664
+
665
+ Two or three short sentences, 120 to 280 characters, written for somebody who has never heard of
666
+ the source, the venue or the format. Sentence one says what the data is about and what makes it
667
+ worth a look, in at most 110 characters. The rest says, in the same plain words, what a reader can
668
+ see or do with it.
669
+
670
+ 1. No tickers, station codes, series codes, format names, publisher names, row definitions or row
671
+ counts here. Every one of those facts is in the sections below, which is where a reader who
672
+ wants it will look.
673
+ 2. No Markdown: no bold, no links, no bullets, no code.
674
+ 3. A hook is an interesting fact stated plainly, never a pitch. Banned outright: comprehensive,
675
+ rich, valuable, powerful, unlock, explore, dive into, perfect for, insights, "this dataset", a
676
+ rhetorical question, an exclamation mark.
677
+ 4. Every number in it must be one the run established. "About every half hour" and "thousands of
678
+ markets" are claims, and a claim that is not on the run does not go here.
679
+ 5. The stranger test, before anything else is written: read sentence one alone under the title. If
680
+ a curious non-specialist could not say what the data is about and would not want the next line,
681
+ rewrite it.
682
+
683
+ One airport weather table, the shape to avoid and the shape to write. To avoid:
684
+
685
+ Each row is one METAR or SPECI weather report from Beijing (ZBAA), since January 2020, via
686
+ Iowa Environmental Mesonet. 117,876 reports of temperature, wind, pressure, visibility with
687
+ the day's highest and lowest reading.
688
+
689
+ To write:
690
+
691
+ Beijing's weather about every half hour since 2020, with each day's official high and low on
692
+ the same row. Temperature, wind, pressure and visibility as the airport recorded them, ready
693
+ to test a forecast against what actually happened.
694
+
695
+ The station code, the report types, the publisher and the count all moved down to Coverage and to
696
+ Source and rights, where a reader who wants them looks. "About every half hour" stays only because
697
+ Coverage puts a month at 1,302 to 1,591 rows, which is one row every 27 to 33 minutes. "On the same
698
+ row" stays because it says what a reader can see; the row definition rule 1 bans is the sentence
699
+ that begins `Each row is`. Two ways to fail: the pitch (`Unlock real-time insights into how
700
+ prediction markets price the weather!`) and the bare subject with no reason to care (`Beijing
701
+ weather data.`).
632
702
 
633
703
  **Body rules** (the five sections under the headings)
634
704
  1. Every sentence must stay true and complete when quoted alone, away from the page. Never
@@ -644,14 +714,16 @@ That is six sections: the opening paragraph, and the five headings under it.
644
714
  Rules that keep it readable: short sentences; no sentence over about twenty-five words; bullets
645
715
  rather than comma lists; no tables, no code blocks, no links to internal tools, no headings beyond
646
716
  the ones above. Spell out the names a reader would search for -- the place, the identifiers in
647
- every common form, the programme and the publisher -- once each, in the opening sentences and
648
- never as a keyword list. A one-line description is a defect to fix in the revision, not a style
649
- choice. Give `table.description` the grain and the window.
717
+ every common form, the programme and the publisher -- once each, under Coverage and under
718
+ Source and rights, and never as a keyword list. A one-line description is a defect to fix in the
719
+ revision, not a style choice. Give `table.description` the grain and the window.
650
720
 
651
721
  **Check before writing.** Fix and re-check until every line passes:
652
722
  - title: at most 60 characters, contains the place, contains no colon, pipe or exclamation mark,
653
723
  no brand, no "dataset" or "data", no version number, no run date
654
- - opening paragraph: 150 to 240 characters, first full stop before character 200, no Markdown
724
+ - opening paragraph: two or three sentences, 120 to 280 characters, first full stop before
725
+ character 110, no Markdown, no identifier, publisher name, row count or row definition, no
726
+ banned word
655
727
  - category: exactly one fixed ID saved and verified before recipe registration
656
728
  - topics: 3 to 8 descriptive tags, no duplicates, lowercase, at most 40 characters each
657
729
  - licence: an SPDX identifier the sources actually grant, or `--license` left off
@@ -737,25 +809,36 @@ contradicts what the column declared. A `bucket` the span outgrew is not one of
737
809
  is widened and the chart stays yours. So `none` is the right answer to write for a column you
738
810
  already know has no shape, and a wide date range is worth a thought before you promise a timeline.
739
811
 
740
- [The recipe document](../../docs/RECIPE-DOCUMENT.md) carries which types each chart admits, what
741
- the defaults are, and the two members only some charts take.
812
+ [The recipe document](https://mostlyright.md/docs/reference/recipe/) carries which types each
813
+ chart admits, what the defaults are, and the two members only some charts take.
742
814
 
743
815
  ### What the statement must actually return
744
816
 
745
- Every acquired source is bound as all-text CSV — `read_csv(header = true, auto_detect = false,
746
- delim = ',')` with every column `VARCHAR` — so each declared type is an explicit cast in the
747
- statement, and the cast is checked against what the engine returns:
817
+ The last step's relation is projected into the declared columns: every declared column must come
818
+ back BY NAME, in an engine type that narrows into the declared one. A returned column the table does
819
+ not declare is dropped; a declared column no statement returns is `TRANSFORM_PLAN_INVALID` before a
820
+ row exists.
821
+
822
+ A source's `binding` decides what its columns are typed as. `text` — the default, and what leaving
823
+ the member out means — binds every column `VARCHAR` whatever the file said, so each declared type is
824
+ an explicit cast you write. `typed` keeps the types the bytes carry. Under either one the cast is
825
+ checked against what the engine returns:
748
826
 
749
827
  | Declared | The engine type the statement must return |
750
828
  | --- | --- |
751
829
  | `string` | `VARCHAR` |
752
830
  | `integer` | `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT` — not the widest |
753
- | `decimal` | `DECIMAL(p,s)` — never `FLOAT` or `DOUBLE` |
831
+ | `decimal` | `DECIMAL(p,s)` with `p` at most 38 — never `FLOAT` or `DOUBLE` |
832
+ | `float` | `DOUBLE`, `FLOAT` |
754
833
  | `boolean` | `BOOLEAN` |
755
834
  | `date` | `DATE` |
756
835
  | `timestamp` | `TIMESTAMP WITH TIME ZONE` |
757
836
  | `json` | `JSON` or `VARCHAR` |
758
837
 
838
+ `decimal` refusing the floating types is the point of that split: an average or a division over
839
+ exact decimals comes back `DOUBLE`, so end the expression in `CAST(… AS DECIMAL(p,s))` where the
840
+ column is exact, and declare `float` where it is genuinely approximate.
841
+
759
842
  `TRANSFORM_COLUMN_TYPE_MISMATCH: column obs_time is declared timestamp and the statement returns
760
843
  TIMESTAMP` is the time-zone row, and it names the same word twice because the difference is the zone
761
844
  rather than the type. Build one from an epoch with `to_timestamp(CAST(x AS BIGINT))`, or from text by
@@ -792,12 +875,22 @@ ones, so a caller cannot assert a digest for its own bytes even by trying; the d
792
875
  is the server's reading of what it canonicalized. Registration is idempotent by that digest:
793
876
  identical bytes register one recipe rather than two.
794
877
 
795
- Before proposing a step, read [Transforms](#transforms); before proposing a future outcome, read
796
- [Prediction labels](#prediction-labels). Declare what each measured column measures as a unit code
797
- from the accepted subset — `cm`, `ug/m3`, `km/h` and the rest of it are written out in
798
- [Units](../../docs/UNITS.md). Do not invent a unit name: a code outside the subset is refused with
799
- the construct that put it there. Say `none` where a column carries no physical unit. Remember that
800
- the two temperature scales are offsets rather than multipliers.
878
+ Before writing a step, read [Transforms](#transforms); before proposing a future outcome, read
879
+ [Prediction labels](#prediction-labels). Declare what each measured column measures as a UCUM code
880
+ from the accepted subset — `Cel`, `ug/m3`, `km/h`, `hPa`, `{count}` and the rest of it are written
881
+ out in [Units](https://mostlyright.md/docs/reference/units/). Write the code rather than a name:
882
+ `ug/m3` composes and `microgram_per_cubic_meter` does not.
883
+
884
+ **Nothing resolves the code, so there is no `UNIT_*` refusal to lean on.** The recipe schema bounds
885
+ the length of `unit` and nothing else; registration checks only that `units[].column` names a
886
+ declared column and answers `422 RECIPE_DOCUMENT_INCOHERENT` at `/units/N/column` when it does not;
887
+ the run never reads `units` at all. A code outside the subset therefore costs nothing at build time
888
+ and costs every reader afterwards, and a column declared in metres that holds feet is caught by
889
+ nothing. Convert in the statement, then declare the unit of what the column ends up holding. Leave a
890
+ column out of `units` rather than declare something approximate — a density, a rate, anything whose
891
+ unit is genuinely new. `none` is not a unit and does not become one. Remember that the two
892
+ temperature scales are offsets rather than multipliers, so `Cel` composes with nothing: `Cel/m` and
893
+ `mCel` are both outside the subset.
801
894
 
802
895
  ### 5. Build — take an unwindowed sample first
803
896
 
@@ -855,7 +948,7 @@ one", not as a failure: a document declaring 8 MiB over a feed that answers 40 K
855
948
  ceiling on that source:** that does not fetch less data, it truncates the sample or refuses the
856
949
  full run. Split the window across several sources — one per month, per region, or per whatever the
857
950
  publisher paginates on — and union them in a first transform step. A recipe names up to 256
858
- sources, which is a great many windows. [The recipe document](../../docs/RECIPE-DOCUMENT.md)
951
+ sources, which is a great many windows. [The recipe document](https://mostlyright.md/docs/reference/recipe/)
859
952
  carries the measured numbers behind the ceiling.
860
953
 
861
954
  **A corpus of many pages behind one index is a COLLECTION, not many sources.** See
@@ -1321,9 +1414,9 @@ access is denied, say the current account cannot view the build evidence and ask
1321
1414
  to an authorized account or request access.
1322
1415
 
1323
1416
  A flag that cannot mean what it means locally is accepted and reported, never refused.
1324
- `mr-data runs --depth 3` runs, lists the workspace's runs, and reports under `flags_without_effect`
1325
- that folder depth described nothing here. Read that key before repeating an argument that did
1326
- nothing. Only flags actually passed are reported.
1417
+ `mr-data status RUN_ID --receipts` runs, reports the run, and reports under `flags_without_effect`
1418
+ that a run's digests, sources, timings and checks are already on the record. Read that key before
1419
+ repeating an argument that did nothing. Only flags actually passed are reported.
1327
1420
 
1328
1421
  ### Asking the table a question
1329
1422
 
@@ -1351,7 +1444,7 @@ Two things are currently broken here and are not worth diagnosing again:
1351
1444
  version digest in `source.connection`, with the query in `parameter_values` and a valid response
1352
1445
  reader; set `dataset.id` to the granted dataset. Do not ask the user to re-enter an already saved
1353
1446
  credential under Keys. If it is not granted, explain that an owner or administrator must allow
1354
- the dataset in Settings → Connections & secrets. See [Saved connections](../../docs/RECIPE-CONNECTIONS.md)
1447
+ the dataset in Settings → Connections & secrets. See [Saved connections](https://mostlyright.md/docs/build/credentials/)
1355
1448
  for the proposal shape. Studio resolves it into the canonical recipe and enforces access during
1356
1449
  execution. Named secrets below remain available for sources without saved connections.
1357
1450
 
@@ -1615,8 +1708,9 @@ or network search. If every registered, lawful alternative is deterministically
1615
1708
  record and report the typed impossibility. Do not loop forever, fabricate a dataset, or turn a
1616
1709
  refusal into permission.
1617
1710
 
1618
- Never download source bytes directly. Never write custom Python, SQL, or shell transformations.
1619
- Never create Parquet outside a Harness Build. Never report a standalone Parquet file as a Build.
1711
+ Never download source bytes directly. Never transform data on this computer: the transform is the
1712
+ recipe's own `duckdb_sql` steps, run on the backend, and Python, shell or a local SQL engine reading
1713
+ the same bytes is not that. Never create Parquet outside a Harness Build. Never report a standalone Parquet file as a Build.
1620
1714
  Do not report completion unless the run succeeded, its declared checks were read, and any artifacts
1621
1715
  the user asked for came back with their digests checked.
1622
1716
 
@@ -1743,20 +1837,24 @@ the records pointer names an array, and whether `discovery.max_requests` plus
1743
1837
  they are the four that are cheapest to get right before a run spends anybody's rate limit.
1744
1838
 
1745
1839
  The full contract, including every bound and the coverage block's members, is
1746
- [Source collections](../../docs/SOURCE-COLLECTIONS.md).
1840
+ [Many-page sources](https://mostlyright.md/docs/build/collections/), with the connector coordinate
1841
+ and its parameters under
1842
+ [Source kinds and connectors](https://mostlyright.md/docs/reference/sources/).
1747
1843
 
1748
1844
  ## Readers
1749
1845
 
1750
- `docs/READERS.md` defines Reader behavior. Certified Readers may be pinned on an exact
1751
- `ArtifactSourceInput` for `user.file` or on a credential-free public HTTPS source. A Recipe pins the
1752
- exact Reader name and version. Unsupported versions refuse by name. A version change is a recipe
1753
- revision.
1846
+ [Readers](https://mostlyright.md/docs/reference/readers/) defines Reader behavior. Certified
1847
+ Readers may be pinned on a hash-pinned
1848
+ `user.file` artifact, on a credential-free public HTTPS source, or on a bounded ordered public HTTPS
1849
+ collection, which applies one exact pin to every member. A Recipe pins the exact Reader name and
1850
+ version. Unsupported versions refuse by name. A version change is a recipe revision.
1754
1851
 
1755
1852
  **Direct CSV supports publisher CSV aliases, including Eurostat SDMX-CSV.**
1756
1853
  Accepted labels include `text/csv`, `text/plain`, `application/csv`, `application/x-csv`,
1757
1854
  `text/x-csv`, and `application/vnd.sdmx.data+csv`. For title rows, alternate encodings,
1758
1855
  headerless files, or padded columns, pin `delimited_text@2.0.0` and configure the layout; see
1759
- `docs/READERS.md`. A publisher needing another format can use its matching Reader.
1856
+ [Readers](https://mostlyright.md/docs/reference/readers/). A publisher needing another format can
1857
+ use its matching Reader.
1760
1858
 
1761
1859
  **A v4 recipe pins a Reader through `connector.parameters`, under three reserved names.**
1762
1860
  `declared_source` is closed and has no member for a decoder, so the pin rides on the parameter array
@@ -1776,38 +1874,35 @@ field and finding none does not mean v4 cannot pin one.
1776
1874
  }
1777
1875
  ```
1778
1876
 
1779
- A parameter under `reader.` that is not one of those three is refused rather than ignored.
1877
+ A parameter under `reader.` that is not one of those three is refused rather than ignored. All three
1878
+ or none: there is no other place a Reader is named. Read the pin grammar and the closed registry of
1879
+ 48 coordinates at [Readers](https://mostlyright.md/docs/reference/readers/).
1880
+
1881
+ **No command tries a Reader against a URL.** `mr-data peek` reads the preview one hosted run sealed,
1882
+ and its `--reader`, `--reader-options`, `--format` and `--json-*` flags are accepted and print `no
1883
+ effect here; the hosted preview arrives already decoded`. A Reader is settled the way every other
1884
+ part of the document is settled: pin the coordinate in the recipe, register it, take an unwindowed
1885
+ sample under `--max-rows`, and read the columns back with `peek` and the decode evidence with
1886
+ `receipt`. A wrong coordinate or a wrong settings object is a typed refusal on that run naming the
1887
+ source, which is the answer — change the pin and register the revision.
1780
1888
 
1781
1889
  **`reader.decode_options` is the canonical JSON text of the options the family itself admits**, with
1782
1890
  every family-defaulted key filled in. A stated subset digests differently from the pin the worker
1783
- reconstructs and is refused. Derive the value rather than writing it:
1784
-
1785
- ```python
1786
- from mostlyright.data_harness.readers.json_tabular import JsonTabularReaderV1_1
1787
- from mostlyright.data_harness.canonical import canonical_json_bytes
1788
- admitted = JsonTabularReaderV1_1().validate_options(
1789
- {"columns": [{"name": "icao_id", "pointer": "/icaoId"}]}
1790
- )
1791
- value = canonical_json_bytes(admitted).decode()
1792
- ```
1891
+ reconstructs and is refused. This client ships no Reader code to derive the value with: copy the
1892
+ family's complete default object from [Readers](https://mostlyright.md/docs/reference/readers/),
1893
+ change only the keys the source needs, keep every other key at its default, and write it as
1894
+ canonical JSON — keys sorted, no whitespace.
1793
1895
 
1794
1896
  A column that may be absent from a record must state `"required": false`. Publishers routinely omit
1795
1897
  a key rather than sending null, and a required column that is missing refuses the record.
1796
1898
 
1797
- **Open defect: an empty string in the admitted options is refused at the acquisition boundary with
1798
- `TEXT`, and the refusal names no source.** `records_pointer: ""` is the RFC 6901 pointer for a
1799
- root-level array, which is what `json.tabular` admits by default and what every API returning a bare
1800
- JSON array needs, so this blocks that whole class of source. The request-side screen applies the
1801
- identifier rule — where an empty string is a missing one — to JSON values, where it is not. Fixed by
1802
- harness PR #503; until that reaches the deployed worker image, a root-array JSON source cannot be
1803
- pinned. Confirm the deployed worker carries the fix before planning a build around one.
1804
-
1805
1899
  When you pin a Reader for a source nobody has approved yet, name the family's highest certified
1806
1900
  version rather than its base version. The base version refuses a publisher that merely declined
1807
1901
  to describe its file, and a weak-label widening lands as a new coordinate precisely so an
1808
1902
  approved recipe is not moved under review's feet -- which makes the base version the wrong
1809
- default for a source that has no approval to protect. `ReaderRegistry.widest_version` answers
1810
- which coordinate that is. For a family whose major version has moved, the highest coordinate is
1903
+ default for a source that has no approval to protect. The registry table at
1904
+ [Readers](https://mostlyright.md/docs/reference/readers/) lists every certified coordinate of a
1905
+ family; take the highest within the major you want. For a family whose major version has moved, the highest coordinate is
1811
1906
  a different output contract rather than a wider door onto the same one, so choose it
1812
1907
  deliberately: `weather.grib2@2.0.0` emits columns `1.0.0` does not.
1813
1908
 
@@ -1887,78 +1982,117 @@ response matches the family's exact media-type, settings, and budget contract.
1887
1982
 
1888
1983
  ## Transforms
1889
1984
 
1890
- The graph operations are a closed set, and what gets into that set is decided by one written test:
1891
- a step is cleaning when the sources plus the declared shape determine it, and it is a modeling
1892
- choice when it needs an assumption about what the analyst intends to model. Read
1893
- [Transforms a plan may perform](../../docs/TRANSFORMS.md) before proposing a step that is not
1894
- already in the vocabulary; do not work around a refused operation with a chain that reproduces it.
1895
-
1896
- Four points recur in plans:
1897
-
1898
- - Division is an expression inside `derive`. Numerator first, denominator second, both operands in
1899
- one exact numeric type, and the result is always the exact decimal type — dividing two whole
1900
- numbers never rounds down or towards zero. Declare `zero_denominator_policy` and
1901
- `null_denominator_policy` on any node that divides, and on no other node. A zero denominator
1902
- stops the run whether or not the numerator is absent. For a rate per hundred thousand, multiply
1903
- inside the division rather than after it. Write a conversion with the division last —
1904
- `(x * 9 + 160) / 5` rather than `x * 9 / 5 + 32` — because every operation takes operands of one
1905
- exact numeric type and a quotient lands in the decimal type.
1906
- - `unpivot` turns one column per period into rows. Name the value-bearing columns in the plan,
1907
- name the two output columns, declare `value_type` for the long column, and set `null_policy` to
1908
- `retain`; drop absent cells with a separate `filter` step if the contract calls for it. Every
1909
- named column must already be the declared type.
1910
- - Join kinds are `left`, `inner`, and `anti`, and all three are graph kinds. Use `anti` for "which
1911
- of these has none of those"; do not join and then filter on an absent column, which cannot tell a
1912
- missing counterpart from a legitimately absent value. An anti join emits left columns only, takes
1913
- `unmatched_policy` `preserve`, and has its declared cardinality checked on the left side. The
1914
- older plan version's single join is `left` and stays `left` — a settled ruling, not a gap — so a
1915
- question needing `inner` or `anti` is written as a graph.
1916
- - `date_add_days` is the one fixed calendar availability correction. It takes a declared `date`
1917
- column, a new output column, and an integer `days` from -3650 to 3650; it keeps nulls null and
1918
- rejects a result outside the supported date range. Use it for a source-established statement
1919
- such as `information_available_date = rate_date + 1`; it never reads another row and is not a
1920
- lag, lead, target, or replacement for `prediction_label`. Date-only arithmetic remains a
1921
- calendar-day operation at DST boundaries.
1922
- - `trim` and `case_fold` are the whole text-normalization surface, and there is no pattern
1923
- matching. `trim` removes six named characters; `case_fold` lowers twenty-six letters and touches
1924
- nothing else. Text needing more than that is normalized before it reaches the plan.
1925
-
1926
- Declare what each measured source column is in, as `column_units` on the source node, and what
1927
- unit a computed column is in, as `output_unit` on its expression. Both are optional and both are
1928
- what the two unit checks read: a merge whose declared units disagree is refused, and a
1929
- conversion that is not the arithmetic those two units imply is refused with the arithmetic they
1930
- do imply. A column you leave undeclared is left alone by both, and a column with no physical
1931
- unit is left out rather than declared `none`. Declare `output_unit` only where the expression
1932
- converts one declared column, or where its operands settle the answer, or where nothing it
1933
- reads is declared; a computed column whose unit is genuinely new -- a density, an area, a rate
1934
- -- declares nothing, because nothing can check it.
1985
+ **A v4 transform is SQL, not an operation vocabulary.** The member is
1986
+ `{"engine": "duckdb_sql", "steps": [...]}`, and each step is `{"step_id": "…", "sql": "…"}` with an
1987
+ optional `description`: at most 64 steps, each statement at most 65536 characters. There is no node
1988
+ graph, no closed operation set and nothing to work around — the statement is the plan. The schema
1989
+ also admits `engine: "none"`, and that document registers and then refuses its first run
1990
+ `TRANSFORM_PLAN_INVALID`, as an empty `steps` array does, because neither says what a build
1991
+ produces. Fetch [Transforms](https://mostlyright.md/docs/reference/transforms/) before writing one:
1992
+ it carries the admitted function table, the worked shapes and every refusal code.
1993
+
1994
+ **What a step reads.** One relation per source, under the source's own `name`, and one relation per
1995
+ earlier step, under its `step_id`. Steps run in array order, never sorted, so a later step reads an
1996
+ earlier one by writing its id. A `step_id` equal to a source `name` is refused
1997
+ `TRANSFORM_STEP_SHADOWS_SOURCE` and two steps sharing an id are `TRANSFORM_STEP_DUPLICATE`. Quote a
1998
+ name that is also a SQL keyword.
1999
+
2000
+ **What the columns are typed as.** The relation carries the ordered header acquisition recorded, and
2001
+ the source's `binding` decides the types: `text` — the default, and what leaving the member out
2002
+ means — binds every column `VARCHAR`, and `typed` keeps what the bytes carry. Under `text` every
2003
+ declared type is a cast you write, which keeps casting an act of the recipe. Two relations are not
2004
+ declared anywhere: a recorded stream source presents one of two fixed text headers — eight
2005
+ columns `epoch`, `event_id`, `event_time`, `message`, `message_class`, `message_type`,
2006
+ `received_at`, `sequence`, or those plus `member` after `event_time` when the connector declares a
2007
+ member field — and a collection source presents the pinned Reader's columns plus seven reserved
2008
+ `page_*` columns.
2009
+
2010
+ **What the last step must return** is under
2011
+ [What the statement must actually return](#what-the-statement-must-actually-return). Row order is
2012
+ the table's `grain` and then every remaining declared column in declaration order, so an `order by`
2013
+ you write is not the sealed order.
2014
+
2015
+ ```json
2016
+ "transform": {
2017
+ "engine": "duckdb_sql",
2018
+ "steps": [
2019
+ {
2020
+ "step_id": "readings",
2021
+ "sql": "select station, cast(concat(valid, ':00+00') as timestamp with time zone) as observed_at, cast((try_cast(tmpf as decimal(6,2)) - 32) * 5 / 9 as decimal(6,2)) as air_temp_c from asos_observations where valid is not null and valid <> ''"
2022
+ },
2023
+ {
2024
+ "step_id": "hourly",
2025
+ "sql": "select station, time_bucket(interval 1 hour, observed_at) as hour_start, cast(avg(air_temp_c) as decimal(6,2)) as mean_air_temp_c, count(*) as reading_count from readings group by station, time_bucket(interval 1 hour, observed_at)"
2026
+ }
2027
+ ]
2028
+ }
2029
+ ```
2030
+
2031
+ `asos_observations` is a source's `name`; `readings` is the first step read by the second. The outer
2032
+ `cast` on each measured column is what makes it a `DECIMAL(6,2)` — without it the division and the
2033
+ `avg` both return `DOUBLE` and the column is refused.
2034
+
2035
+ **One read-only `SELECT` per step.** The engine classifies the statement: more than one is
2036
+ `TRANSFORM_STATEMENT_NOT_SINGLE`, and anything that is not a `SELECT` is
2037
+ `TRANSFORM_STATEMENT_NOT_SELECT` — so `ATTACH`, `COPY`, `INSTALL`, `LOAD`, `PRAGMA`, `CREATE`,
2038
+ `INSERT`, `UPDATE` and `DELETE` are all refused. Common table expressions, `UNION`, `QUALIFY`,
2039
+ `UNPIVOT`, `VALUES` and `SELECT DISTINCT` are each one `SELECT` and are admitted, which is how a
2040
+ long-format reshape, a join, a deduplication and a time bucket are written.
2041
+
2042
+ **The result has to re-derive, and the engine decides that rather than a word list.** A sealed table
2043
+ is rebuilt and compared byte for byte, so a step may read nothing the recipe and the acquired bytes
2044
+ do not fix. The engine's own function catalog is asked about every name the statement references:
2045
+ anything it does not report as re-derivable, anything with side effects, and anything it does not
2046
+ carry at all is refused `TRANSFORM_PLAN_INVALID` before the step runs. That catches `current_date`,
2047
+ `current_timestamp`, `now()`, `today()`, `random()`, `gen_random_uuid()` and anything written in
2048
+ terms of them. The date-granular ones are the dangerous ones: both executions inside one build agree
2049
+ and the table seals, and tomorrow's refresh can never reproduce it. Reach a column a publisher
2050
+ happened to spell like one of those names by qualifying it — `observations.current_date`.
2051
+
2052
+ **The engine reaches nothing.** It is given this run's own source files and scratch directory, then
2053
+ external access is switched off and the settings are locked, so it can open no file, fetch no URL
2054
+ and load no extension. There is no denylist of names in any of it, by design: a source is reached by
2055
+ naming its relation and never by opening its address again. Where that confinement cannot be
2056
+ established the code is `TRANSFORM_ENGINE_UNCONFINED` and no step was attempted.
2057
+
2058
+ **Every table is built in `UTC`**, established before a statement is read. A recipe declaring
2059
+ another `timezone` registers and then refuses its first run `RUN_RECIPE_INVALID`. `ICU` is not
2060
+ loaded, so `AT TIME ZONE` is unavailable; write the conversion into the statement.
2061
+
2062
+ A step the engine itself refused comes back as `RUN_EXECUTION_FAILED` carrying DuckDB's own code and
2063
+ words — `step daily_mean would not run: BINDER_ERROR: …`. Read the detail, not the code. Every
2064
+ transform code reports `failed_stage: "transform"`.
2065
+
2066
+ `units` is declared beside the table, and nothing in the transform reads it or checks the arithmetic
2067
+ against it. Convert in the statement, then declare the unit of what the column ends up holding, per
2068
+ the paragraph under [What each check kind requires](#what-each-check-kind-requires).
1935
2069
 
1936
2070
  ## Prediction labels
1937
2071
 
1938
- `prediction_label@1.0.0` is retired. Do not put it in a plan, and do not offer a future outcome as
1939
- something the harness can construct. Authoring, plan checking, validation, export and approval all
1940
- refuse a graph naming it, with a typed `OPERATION_RETIRED` refusal.
2072
+ There is no prediction-label member in a v4 recipe document and no operation that produces one. Do
2073
+ not offer a future outcome as something the harness can construct.
1941
2074
 
1942
- The reason is the admission contract above: the horizon such a label needs is a statement about what
1943
- somebody intends to model, and nothing in the sources says what it should be. A target belongs to
1944
- the modeling step that consumes the dataset, not to the dataset. There is no replacement operation,
1945
- and none is owed.
2075
+ The horizon such a label needs is a statement about what somebody intends to model, and nothing in
2076
+ the sources says what it should be. A target belongs to the step that consumes the dataset, not to
2077
+ the dataset. There is no replacement, and none is owed.
1946
2078
 
1947
- Do not reconstruct it either. `lag`, `lead`, `rolling` and `window_target` are refused by name; a
1948
- derive expression, a self join, or custom code that reproduces the same shift is the same thing
1949
- this contract refuses, whether or not any single check catches it. If a contract asks for a future
1950
- outcome, say that the harness builds the observed table and that the horizon is chosen downstream.
1951
- Set requirements `target_policy` to `none`.
2079
+ Do not reconstruct it in the statement either. A window function that shifts a measured value
2080
+ forward in time and a self join that reads a later row are the same claim under another spelling,
2081
+ and no check catches either — the transform engine refuses what cannot re-derive, not what is
2082
+ modelled. Where a request asks for a future outcome, say that the build seals the observed table and
2083
+ that the horizon is chosen downstream.
1952
2084
 
1953
- Work sealed or approved before the retirement is unaffected: such a Build still verifies and
1954
- replays, and such a Recipe still runs. [Prediction labels](../../docs/PREDICTION-LABELS.md) is the
1955
- contract for reading it.
2085
+ Work sealed before the retirement is unaffected: such a table still verifies and replays, and such a
2086
+ Recipe still runs.
2087
+ [Units and vocabulary](https://mostlyright.md/docs/reference/units/) records the withdrawal and what
2088
+ a reader of such a Build should expect from it.
1956
2089
 
1957
2090
  ## Boundaries
1958
2091
 
1959
- - Keep plans and Recipes declarative. Do not embed Python, SQL, shell, arbitrary expressions,
1960
- plugins, redirects, or callables.
1961
- - Use only contract-supported operations, joins, adapters, checks, and resource bounds.
2092
+ - The transform's `duckdb_sql` steps are the only executable text a Recipe carries, and they run on
2093
+ the backend under the confinement above. Everything else in the document stays declarative: no
2094
+ Python, no shell, no plugins, no redirects, no callables.
2095
+ - Use only contract-supported connectors, Readers, checks and resource bounds.
1962
2096
  - Do not claim that recipe registration fetched a source, that a declared check released a table
1963
2097
  version, or that agent output executed a transformation.
1964
2098
  - Preserve the cause and acceptance condition for blocker, critical, and high findings. This
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Mostly Right Data Build"
3
3
  short_description: "Build and verify reviewed datasets"
4
- default_prompt: "Use $mr-data-build to autonomously deliver the requested dataset outcome before inspecting CLI help, documentation, schemas, fixtures, examples, prior runs, or installed package details whenever the request uses, tests, demonstrates, or debugs the Harness. If the host requires a skill-use disclosure, send only one outcome-specific sentence naming $mr-data-build before the first tool call; otherwise start silently. Never narrate preparation, skill loading, CLI discovery, authentication, document or contract lookup, example searches, package versions, command batches, or acquisition-document authoring. Never narrate skill instructions, commands, receipts, events, protocols, status codes, service behavior, or notebook mechanics. Open the request's canonical Dataset page first when that address can be minted, and carry on without it rather than blocking. Keep routine work silent unless the host requires periodic status; then send concise outcome-oriented updates without mechanics or internal language, grounded in domain-specific observed evidence, a decision, required user action, or the verified outcome."
4
+ default_prompt: "Use $mr-data-build to autonomously deliver the requested dataset outcome before inspecting CLI help, documentation, schemas, fixtures, examples, prior runs, or installed package details whenever the request uses, tests, demonstrates, or debugs the Harness. If the host requires a skill-use disclosure, send only one outcome-specific sentence naming $mr-data-build before the first tool call; otherwise start silently. Never narrate preparation, skill loading, CLI discovery, authentication, document or contract lookup, example searches, package versions, command batches, or acquisition-document authoring. Never narrate skill instructions, commands, receipts, events, protocols, status codes, service behavior, or notebook mechanics. Open the request's canonical Dataset page first when that address can be minted, and carry on without it rather than blocking. Keep routine work silent unless the host requires periodic status; then send concise outcome-oriented updates without mechanics or internal language, grounded in domain-specific observed evidence, a decision, required user action, or the verified outcome. Fetch the published reference at https://mostlyright.md/docs/ for the recipe field, connector, Reader, transform, check, unit, command flag, error code, ceiling or worked example in front of you before drafting it; it is the contract, it wins over any memory of an older version, and fetching it is preparation you never narrate."