mostlyright-data 0.29.8__tar.gz → 0.29.10__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 (122) hide show
  1. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/PKG-INFO +1 -1
  2. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/pyproject.toml +1 -1
  3. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/preparing-table-search.md +28 -2
  4. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe.py +6 -5
  5. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_explain.py +9 -7
  6. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/router.py +4 -1
  7. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/session.py +68 -5
  8. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4.py +39 -10
  9. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_public.py +38 -19
  10. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_query.py +1 -2
  11. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/.gitignore +0 -0
  12. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/README.md +0 -0
  13. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/scripts/hatch_build.py +0 -0
  14. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/SKILL.md +0 -0
  15. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/agents/openai.yaml +0 -0
  16. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  17. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  18. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  19. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  20. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  21. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  22. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  23. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  24. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  25. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/agent-protocol.md +0 -0
  26. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  27. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  28. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/boundaries.md +0 -0
  29. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  30. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/commands.md +0 -0
  31. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  32. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/installation-parity.md +0 -0
  33. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/live-run.md +0 -0
  34. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  35. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  36. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  37. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/one-install.md +0 -0
  38. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/prediction-labels.md +0 -0
  39. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/promote.md +0 -0
  40. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/readers.md +0 -0
  41. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/receipts.md +0 -0
  42. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  43. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  44. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/reference-pages.md +0 -0
  45. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/required-protocol.md +0 -0
  46. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/source-credentials.md +0 -0
  47. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/sources.md +0 -0
  48. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  49. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/transforms.md +0 -0
  50. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  51. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  52. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  53. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/__init__.py +0 -0
  54. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  55. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/canonical.py +0 -0
  56. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/formats.py +0 -0
  57. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  58. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/key_seam.py +0 -0
  59. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/page_coverage.py +0 -0
  60. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  61. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/session_probes.py +0 -0
  62. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/skill_assets.py +0 -0
  63. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/table_manifest.py +0 -0
  64. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  65. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  66. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  67. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/activity.py +0 -0
  68. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  69. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/categories.py +0 -0
  70. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/commands.py +0 -0
  71. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  72. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/download.py +0 -0
  73. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  74. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  75. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/parity.py +0 -0
  76. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/probe.py +0 -0
  77. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  78. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  79. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/propose.py +0 -0
  80. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  81. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  82. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  83. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/research.py +0 -0
  84. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  85. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/runs.py +0 -0
  86. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/stream.py +0 -0
  87. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  88. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  89. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/transport.py +0 -0
  90. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  91. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  92. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  93. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  94. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  95. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  96. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  97. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  98. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  99. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  100. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  101. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  102. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  103. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  104. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  105. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  106. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  107. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  108. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  109. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  110. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  111. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  112. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  113. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  114. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  115. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  116. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  117. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.29.8 → mostlyright_data-0.29.10}/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.29.8
3
+ Version: 0.29.10
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.29.8"
3
+ version = "0.29.10"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,8 +15,9 @@ mr-data search inspect --recipe recipe.json --json
15
15
 
16
16
  Local CLI help describes installed commands; it does not prove the deployed server serves them.
17
17
  For the recipe-target probe above, read JSON refusals from stderr: `schema_version` is
18
- `mr-data-error.v1`, `code` is the typed discriminator, and `error` is the detail. There is no
19
- public `http_status` field in that envelope. Treat `code: THIN_NOT_FOUND` as absent search
18
+ `mr-data-error.v1`, `code` is the typed discriminator, and `error` is the detail. Cloud refusals
19
+ also expose `http_status`, `request_id`, `detail` and `retry_after_seconds` when supplied;
20
+ Studio inspection refusals retain their typed code. Treat `code: THIN_NOT_FOUND` as absent search
20
21
  registration support only when `error` begins exactly with
21
22
  `Studio has no such resource for /v4/search/recipe-inspection`; the CLI maps this route's HTTP 404
22
23
  that way. An explicit `code: THIN_SEARCH_CAPABILITY_UNAVAILABLE` also states a capability limit.
@@ -141,3 +142,28 @@ an exact filter.
141
142
 
142
143
  This workflow opts in newly authored recipes only. It does not migrate old datasets, reauthor users'
143
144
  existing recipes, launch a full data rebuild or imply successful indexing of every table.
145
+
146
+ ## Diagnose preparation and query a published table
147
+
148
+ `preparation_state: unavailable` can mean a never-built table, a failed or cancelled job,
149
+ or an incompatible specification. Keep `job_id` from the search build receipt and run
150
+ `mr-data search status --job JOB_ID --json` to inspect the job's status and failure.
151
+ Studio is adding `preparation_detail`; print it when present rather than inferring a reason
152
+ from `unavailable`. Table status passes this additive field through unchanged.
153
+
154
+ For a published table, first run `mr-data public schema TABLE_ID --json`.
155
+ The table schema's `capabilities.search` contains `table_version_id`, `spec_digest`,
156
+ `keyword_columns`, `semantic_columns`, and optional stored `lexical`/`semantic` preparation values.
157
+ Use `keyword_columns` for `search.columns`; top-level `columns` is the non-empty output projection.
158
+ The following lexical example uses a syntactically valid illustrative digest:
159
+
160
+ ```sh
161
+ mr-data public query TABLE_ID --body '{"columns":["title"],"search":{"text":"example","columns":["title"],"spec_digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000","mode":"lexical"},"limit":10}' --json
162
+ ```
163
+
164
+ Replace the illustrative columns and digest with the actual schema values. Hybrid mode additionally
165
+ needs `generation_digest` (`sha256:` plus 64 lowercase hex digits) from readiness/explain;
166
+ it is not in `capabilities.search`. Omit it for this lexical example. Public query prints
167
+ Cloud stage durations in milliseconds under `result.timings` when Server-Timing is present,
168
+ and reports HTTP status, request id and retry timing. Keep refusal `detail` with the request id.
169
+ Missing timing metadata does not establish that a stage did no work.
@@ -80,11 +80,9 @@ SHOW_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-read.v1"
80
80
  READINESS_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-readiness.v1"
81
81
  PROPOSE_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-propose.v1"
82
82
 
83
- #: The bound on the document. The same number the authoring lane bounded one of its six documents
84
- #: by -- four times the contract's own 262144-byte ceiling on a canonical recipe -- rather than a
85
- #: second number chosen here, because this document is those six documents in one and the ceiling
86
- #: it has to clear is the canonical form's.
87
- MAX_DOCUMENT_BYTES = 4 * 262_144
83
+ #: A plain input file may occupy up to Studio's 2 MiB body ceiling. Before submission,
84
+ #: the complete canonical UTF-8 request envelope must also fit that ceiling.
85
+ MAX_DOCUMENT_BYTES = v4.MAX_RECIPE_REQUEST_BYTES
88
86
 
89
87
  #: ``recipe.schema.json#/$defs/recipe_document``'s required members, in the order the schema lists
90
88
  #: them. Checked here before the network, for the reason every early refusal in this client
@@ -1187,6 +1185,9 @@ def register(
1187
1185
  """
1188
1186
 
1189
1187
  document = read_document(Path(args.target))
1188
+ # UUIDs have fixed wire length. Refuse an oversized envelope before credentials,
1189
+ # capability reads or delegation writes; the actual client checks it again.
1190
+ v4.recipe_registration_body(document, "00000000-0000-0000-0000-000000000000")
1190
1191
  # New grammar must be refused as unserved before local lint can diagnose its shape.
1191
1192
  keyless = any(
1192
1193
  capability == "keyless_window" for _, capability in recipe_explain.gated_grammar(document)
@@ -39,6 +39,7 @@ from mostlyright.data_harness.thin.transport import ThinLaneError
39
39
  from mostlyright.data_harness.thin.v4 import (
40
40
  EXPLAIN_AND_FORECAST_REQUEST_VERSION,
41
41
  StudioV4Client,
42
+ check_recipe_request_size,
42
43
  identifier,
43
44
  )
44
45
 
@@ -1231,17 +1232,18 @@ def explain_document_at_studio(
1231
1232
  nothing, which is what makes it safe to ask before a registration.
1232
1233
  """
1233
1234
 
1235
+ body = {
1236
+ "schema_version": ROUTES_REQUEST_VERSION,
1237
+ "workspace_id": str(client.session.workspace_id),
1238
+ "document": dict(document),
1239
+ }
1240
+ check_recipe_request_size(body)
1234
1241
  try:
1235
- # The one inherited `_call` (thin/v4.py): the route rides the same transport, byte
1236
- # ceiling and refusal translation as every other V4 call.
1242
+ # The one inherited `_call` carries the ordinary response ceiling.
1237
1243
  answer = client._call(
1238
1244
  "POST",
1239
1245
  EXPLAIN_REFRESH_PATH,
1240
- body={
1241
- "schema_version": ROUTES_REQUEST_VERSION,
1242
- "workspace_id": str(client.session.workspace_id),
1243
- "document": dict(document),
1244
- },
1246
+ body=body,
1245
1247
  expected=(200,),
1246
1248
  )
1247
1249
  except ThinLaneError as refusal:
@@ -888,7 +888,10 @@ def _hosted(command: str, arguments: Sequence[str], *, as_json: bool) -> int:
888
888
  as_json=as_json or parsed.json,
889
889
  retry_after_seconds=getattr(refusal, "retry_after_seconds", None),
890
890
  details=_refusal_details(refusal),
891
- extra={"sessions": sessions} if sessions else None,
891
+ extra={
892
+ **getattr(refusal, "cloud_metadata", {}),
893
+ **({"sessions": sessions} if sessions else {}),
894
+ },
892
895
  )
893
896
  except Exception as error:
894
897
  code = getattr(error, "code", None)
@@ -17,6 +17,7 @@ recorded production failures on 2026-08-20.
17
17
  from __future__ import annotations
18
18
 
19
19
  import json
20
+ import math
20
21
  import re
21
22
  import time
22
23
  from collections.abc import Callable, Mapping
@@ -523,8 +524,59 @@ def stated_sentence(value: Any) -> str | None:
523
524
  return stated
524
525
 
525
526
 
527
+ def _cloud_sentence(value: Any) -> str | None:
528
+ stated = stated_sentence(value)
529
+ if stated is None:
530
+ return None
531
+ # Refusal prose must never echo bearer/device tokens or URLs (including signed URLs).
532
+ stated = re.sub(r"https?://\S+", "[redacted URL]", stated, flags=re.IGNORECASE)
533
+ stated = re.sub(r"mr_(?:cli|use)_[A-Za-z0-9_-]+", "[redacted token]", stated)
534
+ stated = re.sub(r"(?i)bearer\s+\S+", "Bearer [redacted token]", stated)
535
+ return stated
536
+
537
+
538
+ def cloud_response_metadata(status: int, parsed: Any, headers: Mapping[str, str]) -> dict[str, Any]:
539
+ """Only bounded diagnostic fields, never arbitrary response headers or authority."""
540
+ normalized = {key.lower(): value for key, value in headers.items()}
541
+ metadata: dict[str, Any] = {"http_status": status}
542
+ for request_id in (
543
+ normalized.get("x-request-id"),
544
+ parsed.get("request_id") if isinstance(parsed, dict) else None,
545
+ ):
546
+ if (
547
+ isinstance(request_id, str)
548
+ and re.fullmatch(r"[A-Za-z0-9_.:-]{1,128}", request_id)
549
+ and not request_id.startswith("mr_")
550
+ ):
551
+ metadata["request_id"] = request_id
552
+ break
553
+ raw_retry = normalized.get("retry-after", "")
554
+ if isinstance(raw_retry, str) and re.fullmatch(r"[0-9]{1,4}", raw_retry.strip()):
555
+ seconds = int(raw_retry.strip())
556
+ if 0 <= seconds <= 3600:
557
+ metadata["retry_after_seconds"] = seconds
558
+ timings: dict[str, float] = {}
559
+ for metric in normalized.get("server-timing", "")[:8192].split(",")[:64]:
560
+ match = re.fullmatch(
561
+ r"\s*([A-Za-z][A-Za-z0-9_-]{0,63})\s*;\s*dur=([0-9]+(?:\.[0-9]+)?)(?:\s*;.*)?\s*",
562
+ metric,
563
+ )
564
+ if match:
565
+ duration = float(match[2])
566
+ if math.isfinite(duration):
567
+ timings[match[1]] = duration
568
+ if timings:
569
+ metadata["timings"] = timings
570
+ return metadata
571
+
572
+
526
573
  def cloud_refusal(
527
- status: int, parsed: Any, *, act: str, absent: str | None = None
574
+ status: int,
575
+ parsed: Any,
576
+ *,
577
+ act: str,
578
+ absent: str | None = None,
579
+ headers: Mapping[str, str] | None = None,
528
580
  ) -> ThinLaneError:
529
581
  """One Cloud refusal, folded the way ``runs._studio_refusal`` folds Studio's.
530
582
 
@@ -564,7 +616,7 @@ def cloud_refusal(
564
616
  stated = parsed.get("code") if isinstance(parsed, dict) else None
565
617
  named = stated if isinstance(stated, str) and CLOUD_REFUSAL_CODE.fullmatch(stated) else None
566
618
  said = (
567
- stated_sentence(parsed.get("error")) if named is None and isinstance(parsed, dict) else None
619
+ _cloud_sentence(parsed.get("error")) if named is None and isinstance(parsed, dict) else None
568
620
  )
569
621
  if status in {401, 403}:
570
622
  if named:
@@ -588,7 +640,16 @@ def cloud_refusal(
588
640
  refusal = ThinLaneError(
589
641
  "THIN_CLOUD_REFUSED", f"{detail}. Cloud said: {said}" if said else detail
590
642
  )
591
- refusal.http_status = status # type: ignore[attr-defined]
643
+ detail = _cloud_sentence(parsed.get("detail")) if isinstance(parsed, dict) else None
644
+ if detail:
645
+ refusal.detail += f". Cloud detail: {detail}"
646
+ refusal.args = (f"{refusal.code}: {refusal.detail}",)
647
+ refusal.cloud_metadata = cloud_response_metadata(status, parsed, headers or {})
648
+ if detail:
649
+ refusal.cloud_metadata["detail"] = detail
650
+ for field, value in refusal.cloud_metadata.items():
651
+ if field != "detail":
652
+ setattr(refusal, field, value)
592
653
  return refusal
593
654
 
594
655
 
@@ -613,6 +674,7 @@ class CloudClient:
613
674
  self._credentials = credentials
614
675
  self._origin = service_origin(credentials.cloud_url, "Cloud URL", allow_loopback_http=False)
615
676
  self._transport = transport or UrlLibTransport()
677
+ self.response_metadata: dict[str, Any] = {}
616
678
 
617
679
  @property
618
680
  def cloud_url(self) -> str:
@@ -661,7 +723,7 @@ class CloudClient:
661
723
  if body is not None:
662
724
  headers["Content-Type"] = "application/json"
663
725
  encoded = json.dumps(dict(body), separators=(",", ":")).encode("utf-8")
664
- status, raw, _headers = self._transport.request(
726
+ status, raw, response_headers = self._transport.request(
665
727
  method,
666
728
  f"{self._origin}{path}",
667
729
  headers,
@@ -676,8 +738,9 @@ class CloudClient:
676
738
  )
677
739
  except CanonicalJSONError:
678
740
  parsed = None
741
+ self.response_metadata = cloud_response_metadata(status, parsed, response_headers)
679
742
  if status not in expected:
680
- raise cloud_refusal(status, parsed, act=act, absent=absent)
743
+ raise cloud_refusal(status, parsed, act=act, absent=absent, headers=response_headers)
681
744
  if not isinstance(parsed, dict):
682
745
  raise ThinLaneError(
683
746
  "THIN_RESPONSE_INVALID",
@@ -39,6 +39,7 @@ from collections.abc import Mapping
39
39
  from typing import Any
40
40
  from uuid import UUID
41
41
 
42
+ from mostlyright.data_harness.canonical import canonical_json_bytes
42
43
  from mostlyright.data_harness.thin.runs import StudioApiClient
43
44
  from mostlyright.data_harness.thin.transport import ThinLaneError
44
45
 
@@ -915,6 +916,35 @@ def identifier(value: str, field: str) -> str:
915
916
  raise ThinLaneError("THIN_REQUEST_INVALID", f"{value!r} is not a {field}") from error
916
917
 
917
918
 
919
+ # Studio's API request-body middleware accepts at most 2 MiB. Bound the complete
920
+ # UTF-8 wire envelope, not the recipe's character count or its formatted file size.
921
+ MAX_RECIPE_REQUEST_BYTES = 2 * 1024 * 1024
922
+ # A GET recipe includes the document plus identifiers and registration metadata.
923
+ # Give that envelope 1 MiB headroom; ordinary JSON routes retain their 1 MiB cap.
924
+ MAX_RECIPE_RESPONSE_BYTES = MAX_RECIPE_REQUEST_BYTES + 1024 * 1024
925
+
926
+
927
+ def check_recipe_request_size(body: Mapping[str, Any]) -> None:
928
+ size = len(canonical_json_bytes(body))
929
+ if size > MAX_RECIPE_REQUEST_BYTES:
930
+ raise ThinLaneError(
931
+ "THIN_RECIPE_REQUEST_TOO_LARGE",
932
+ f"the serialized recipe request is {size} bytes; Studio accepts at most "
933
+ f"{MAX_RECIPE_REQUEST_BYTES} bytes including its request envelope. "
934
+ "Reduce the recipe before submitting it",
935
+ )
936
+
937
+
938
+ def recipe_registration_body(document: Mapping[str, Any], workspace_id: str) -> dict[str, Any]:
939
+ body = {
940
+ "workspace_id": workspace_id,
941
+ "document": dict(document),
942
+ "require_category": True,
943
+ }
944
+ check_recipe_request_size(body)
945
+ return body
946
+
947
+
918
948
  class StudioV4Client(StudioApiClient):
919
949
  """The V4 routes, over the transport and the hardening :class:`StudioApiClient` already has.
920
950
 
@@ -993,11 +1023,7 @@ class StudioV4Client(StudioApiClient):
993
1023
  return self._call(
994
1024
  "POST",
995
1025
  CREATE_RECIPE_PATH,
996
- body={
997
- "workspace_id": str(self.session.workspace_id),
998
- "document": dict(document),
999
- "require_category": True,
1000
- },
1026
+ body=recipe_registration_body(document, str(self.session.workspace_id)),
1001
1027
  expected=(201,),
1002
1028
  )
1003
1029
 
@@ -1008,6 +1034,7 @@ class StudioV4Client(StudioApiClient):
1008
1034
  "GET",
1009
1035
  GET_RECIPE_PATH.format(recipe_id=identifier(recipe_id, "recipe identifier")),
1010
1036
  expected=(200,),
1037
+ maximum=MAX_RECIPE_RESPONSE_BYTES,
1011
1038
  )
1012
1039
 
1013
1040
  def recipe_readiness(
@@ -1049,14 +1076,16 @@ class StudioV4Client(StudioApiClient):
1049
1076
  def explain_recipe_refresh(self, document: Mapping[str, Any]) -> dict[str, Any]:
1050
1077
  """Judge one unregistered recipe document for refresh, and read what this Studio serves."""
1051
1078
 
1079
+ body = {
1080
+ "schema_version": EXPLAIN_AND_FORECAST_REQUEST_VERSION,
1081
+ "workspace_id": str(self.session.workspace_id),
1082
+ "document": dict(document),
1083
+ }
1084
+ check_recipe_request_size(body)
1052
1085
  return self._call(
1053
1086
  "POST",
1054
1087
  EXPLAIN_RECIPE_REFRESH_PATH,
1055
- body={
1056
- "schema_version": EXPLAIN_AND_FORECAST_REQUEST_VERSION,
1057
- "workspace_id": str(self.session.workspace_id),
1058
- "document": dict(document),
1059
- },
1088
+ body=body,
1060
1089
  expected=(200,),
1061
1090
  )
1062
1091
 
@@ -39,6 +39,7 @@ from mostlyright.data_harness.thin.session import (
39
39
  MAX_TOKEN_RESPONSE_BYTES,
40
40
  CloudClient,
41
41
  cloud_refusal,
42
+ cloud_response_metadata,
42
43
  require_device_key,
43
44
  stated_sentence,
44
45
  )
@@ -193,7 +194,6 @@ def declare_arguments(parser: argparse.ArgumentParser) -> None:
193
194
  ),
194
195
  )
195
196
 
196
-
197
197
  parser.add_argument(
198
198
  "--body",
199
199
  default=None,
@@ -287,8 +287,10 @@ class PublicCatalogClient:
287
287
  )
288
288
  error = parsed.get("error")
289
289
  detail = parsed.get("detail") if isinstance(parsed.get("detail"), str) else None
290
- code = f"THIN_CLOUD_{error}".upper() if isinstance(error, str) and error else (
291
- "THIN_CLOUD_REFUSED"
290
+ code = (
291
+ f"THIN_CLOUD_{error}".upper()
292
+ if isinstance(error, str) and error
293
+ else ("THIN_CLOUD_REFUSED")
292
294
  )
293
295
  message = detail or f"Cloud refused to {act} (HTTP {status})"
294
296
  refusal = ThinLaneError(code, message)
@@ -471,9 +473,7 @@ def _search_hit(dataset: Mapping[str, Any]) -> dict[str, Any]:
471
473
  "id": dataset.get("id"),
472
474
  "slug": dataset.get("slug"),
473
475
  "title": dataset.get("title"),
474
- "summary": (
475
- f"{summary[: SEARCH_SUMMARY_LIMIT - 1].rstrip()}…" if cut else summary
476
- ),
476
+ "summary": (f"{summary[: SEARCH_SUMMARY_LIMIT - 1].rstrip()}…" if cut else summary),
477
477
  "topics": list(dataset.get("topics") or []),
478
478
  "publisher": _publisher(publisher),
479
479
  "published_at": dataset.get("published_at"),
@@ -861,7 +861,6 @@ def _fetch(args: argparse.Namespace, client: PublicCatalogClient) -> dict[str, A
861
861
  }
862
862
 
863
863
 
864
-
865
864
  CLOUD_PUBLIC_TABLE_QUERY_PATH = "/api/cli/public/tables/{table_id}/query"
866
865
  CLOUD_PUBLIC_TABLE_DOWNLOAD_URL_PATH = "/api/cli/public/tables/{table_id}/download-url"
867
866
  QUERY_SCHEMA = f"{THIN_SCHEMA_PREFIX}-v4-public-query.v1"
@@ -885,8 +884,15 @@ MAX_PUBLIC_QUERY_RESPONSE_MEMBERS = (
885
884
  class PublicQueryClient(CloudClient):
886
885
  """Authenticated public table query + download-url over the device-key client."""
887
886
 
887
+ def __init__(self, credentials, *, transport=None) -> None:
888
+ # Let Cloud's gateway deadline return its own refusal and request id first.
889
+ super().__init__(credentials, transport=transport)
890
+ self._query_client = CloudClient(
891
+ credentials, transport=transport or UrlLibTransport(timeout=150.0)
892
+ )
893
+
888
894
  def query_table(self, table_id: str, body: Mapping[str, Any]) -> dict[str, Any]:
889
- return self.call(
895
+ answer = self._query_client.call(
890
896
  "POST",
891
897
  CLOUD_PUBLIC_TABLE_QUERY_PATH.format(table_id=table_id),
892
898
  act="query a public table",
@@ -897,6 +903,9 @@ class PublicQueryClient(CloudClient):
897
903
  max_members=MAX_PUBLIC_QUERY_RESPONSE_MEMBERS,
898
904
  )
899
905
 
906
+ self.response_metadata = self._query_client.response_metadata
907
+ return answer
908
+
900
909
  def download_url(self, table_id: str) -> dict[str, Any]:
901
910
  """Read download instructions; keep download_not_enabled distinct from missing table.
902
911
 
@@ -911,7 +920,7 @@ class PublicQueryClient(CloudClient):
911
920
  "Accept": "application/json",
912
921
  "Authorization": f"Bearer {self._credentials.raw_key}",
913
922
  }
914
- status, raw, _headers = self._transport.request(
923
+ status, raw, response_headers = self._transport.request(
915
924
  "GET",
916
925
  f"{self.cloud_url}{path}",
917
926
  headers,
@@ -941,6 +950,7 @@ class PublicQueryClient(CloudClient):
941
950
  "THIN_CLOUD_DOWNLOAD_NOT_ENABLED",
942
951
  f"Cloud refused to {act} (download_not_enabled). Cloud said: {said}",
943
952
  )
953
+ refusal.cloud_metadata = cloud_response_metadata(404, parsed, response_headers)
944
954
  refusal.http_status = 404 # type: ignore[attr-defined]
945
955
  raise refusal
946
956
  raise cloud_refusal(
@@ -948,6 +958,7 @@ class PublicQueryClient(CloudClient):
948
958
  parsed,
949
959
  act=act,
950
960
  absent="no public table has that id",
961
+ headers=response_headers,
951
962
  )
952
963
 
953
964
 
@@ -993,12 +1004,16 @@ def _query(args: argparse.Namespace, client: Any = None) -> dict[str, Any]:
993
1004
  body = _read_query_body(args)
994
1005
  selected = _auth_query_client(client if isinstance(client, PublicQueryClient) else None)
995
1006
  answer = selected.query_table(table_id, body)
1007
+ metadata = getattr(selected, "response_metadata", {})
1008
+ if "timings" in metadata:
1009
+ answer = {**answer, "timings": metadata["timings"]}
996
1010
  return {
997
1011
  "schema_version": QUERY_SCHEMA,
998
1012
  "status": "public_table_queried",
999
1013
  "lane": "hosted",
1000
1014
  "table_id": table_id,
1001
1015
  "result": answer,
1016
+ **{key: value for key, value in metadata.items() if key != "timings"},
1002
1017
  "next_step": _NEXT["public_table_queried"],
1003
1018
  }
1004
1019
 
@@ -1061,16 +1076,20 @@ def public(
1061
1076
  "THIN_REQUEST_INVALID",
1062
1077
  f"--cursor applies to public search, not public {action}",
1063
1078
  )
1064
- if action in {
1065
- "show",
1066
- "tables",
1067
- "schema",
1068
- "sample",
1069
- "fetch",
1070
- "lookup",
1071
- "query",
1072
- "download-url",
1073
- } and not args.target:
1079
+ if (
1080
+ action
1081
+ in {
1082
+ "show",
1083
+ "tables",
1084
+ "schema",
1085
+ "sample",
1086
+ "fetch",
1087
+ "lookup",
1088
+ "query",
1089
+ "download-url",
1090
+ }
1091
+ and not args.target
1092
+ ):
1074
1093
  needed = {
1075
1094
  "show": "a dataset slug",
1076
1095
  "tables": "a dataset slug",
@@ -54,7 +54,6 @@ from mostlyright.data_harness.thin.session import (
54
54
  )
55
55
  from mostlyright.data_harness.thin.transport import ThinLaneError, UrlLibTransport
56
56
  from mostlyright.data_harness.thin.v4 import (
57
- GET_RECIPE_PATH,
58
57
  GET_RUN_PATH,
59
58
  GET_RUN_QUERY_PATH,
60
59
  RUN_QUERY_PATH,
@@ -278,7 +277,7 @@ class StudioV4QueryClient(StudioV4Client):
278
277
  def recipe_source_ids(self, recipe_id: str) -> list[str]:
279
278
  """The sources a recipe registered, which a session opened for its dataset may probe."""
280
279
 
281
- record = self._call("GET", GET_RECIPE_PATH.format(recipe_id=recipe_id), expected=(200,))
280
+ record = self.recipe(recipe_id)
282
281
  listed = record.get("source_ids")
283
282
  sources = (
284
283
  [item for item in listed if isinstance(item, str) and item]