mostlyright-data 0.25.7__tar.gz → 0.25.9__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 (113) hide show
  1. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/PKG-INFO +1 -1
  2. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/pyproject.toml +1 -1
  3. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/canonical.py +19 -5
  4. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/propose.py +1 -1
  5. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe.py +209 -8
  6. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/router.py +24 -0
  7. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/runs.py +5 -2
  8. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/session.py +5 -0
  9. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_artifacts.py +86 -8
  10. mostlyright_data-0.25.9/src/mostlyright/data_harness/thin/v4_connected.py +179 -0
  11. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_datasets.py +79 -5
  12. mostlyright_data-0.25.9/src/mostlyright/data_harness/thin/v4_public.py +1087 -0
  13. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_query.py +28 -3
  14. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_tables.py +647 -12
  15. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/vocabulary.py +16 -0
  16. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/render.py +19 -0
  17. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/.gitignore +0 -0
  18. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/README.md +0 -0
  19. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/scripts/hatch_build.py +0 -0
  20. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/SKILL.md +0 -0
  21. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/agents/openai.yaml +0 -0
  22. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  23. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  24. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  25. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  26. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  27. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  28. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  29. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  30. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  31. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/agent-protocol.md +0 -0
  32. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  33. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  34. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/boundaries.md +0 -0
  35. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  36. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/commands.md +0 -0
  37. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  38. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/installation-parity.md +0 -0
  39. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/live-run.md +0 -0
  40. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  41. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  42. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/one-install.md +0 -0
  43. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/prediction-labels.md +0 -0
  44. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/promote.md +0 -0
  45. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/readers.md +0 -0
  46. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/receipts.md +0 -0
  47. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  48. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  49. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/reference-pages.md +0 -0
  50. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/required-protocol.md +0 -0
  51. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/source-credentials.md +0 -0
  52. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/sources.md +0 -0
  53. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  54. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/transforms.md +0 -0
  55. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  56. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  57. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  58. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/__init__.py +0 -0
  59. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  60. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/formats.py +0 -0
  61. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  62. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/key_seam.py +0 -0
  63. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/page_coverage.py +0 -0
  64. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  65. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/session_probes.py +0 -0
  66. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/skill_assets.py +0 -0
  67. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/table_manifest.py +0 -0
  68. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  69. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  70. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  71. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/activity.py +0 -0
  72. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  73. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/categories.py +0 -0
  74. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/commands.py +0 -0
  75. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  76. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/download.py +0 -0
  77. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  78. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/parity.py +0 -0
  79. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/probe.py +0 -0
  80. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  81. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  82. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  83. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/research.py +0 -0
  84. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/stream.py +0 -0
  85. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  86. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/transport.py +0 -0
  87. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  88. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4.py +0 -0
  89. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  90. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  91. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  92. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  93. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  94. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  95. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  96. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  97. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  98. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  99. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  100. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  101. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  102. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  103. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  104. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  105. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  106. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  107. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  108. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  109. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  110. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/login.py +0 -0
  111. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  112. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  113. {mostlyright_data-0.25.7 → mostlyright_data-0.25.9}/src/mostlyright/data_harness/ux/remediation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.25.7
3
+ Version: 0.25.9
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.25.7"
3
+ version = "0.25.9"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -34,7 +34,9 @@ def parse_json(value: str | bytes | bytearray | memoryview) -> Any:
34
34
  return _parse(value, fractions="reject")
35
35
 
36
36
 
37
- def parse_foreign_json(value: str | bytes | bytearray | memoryview) -> Any:
37
+ def parse_foreign_json(
38
+ value: str | bytes | bytearray | memoryview, *, max_members: int = MAX_CANONICAL_MEMBERS
39
+ ) -> Any:
38
40
  """Parse a third-party document with the same hardening, but accept fractional numbers.
39
41
 
40
42
  The Harness data model is integer-only, and :func:`parse_json` enforces that because every
@@ -54,12 +56,20 @@ def parse_foreign_json(value: str | bytes | bytearray | memoryview) -> Any:
54
56
  a parser but can never be sealed, digested or written into a canonical document. That is the
55
57
  boundary this pair of functions draws: read what the world publishes, write only what the model
56
58
  admits.
59
+
60
+ ``max_members`` raises the member ceiling for one document whose size a route already bounds,
61
+ such as a table manifest listing thousands of parts.
57
62
  """
58
63
 
59
- return _parse(value, fractions="accept")
64
+ return _parse(value, fractions="accept", max_members=max_members)
60
65
 
61
66
 
62
- def _parse(value: str | bytes | bytearray | memoryview, *, fractions: str) -> Any:
67
+ def _parse(
68
+ value: str | bytes | bytearray | memoryview,
69
+ *,
70
+ fractions: str,
71
+ max_members: int = MAX_CANONICAL_MEMBERS,
72
+ ) -> Any:
63
73
  if isinstance(value, str):
64
74
  text = value
65
75
  elif isinstance(value, (bytes, bytearray, memoryview)):
@@ -106,6 +116,7 @@ def _parse(value: str | bytes | bytearray | memoryview, *, fractions: str) -> An
106
116
  depth=0,
107
117
  members=[0],
108
118
  accept_fractions=accept_fractions,
119
+ max_members=max_members,
109
120
  )
110
121
 
111
122
 
@@ -171,6 +182,7 @@ def _normalize_json(
171
182
  depth: int,
172
183
  members: list[int],
173
184
  accept_fractions: bool = False,
185
+ max_members: int = MAX_CANONICAL_MEMBERS,
174
186
  ) -> Any:
175
187
  if depth > MAX_CANONICAL_DEPTH:
176
188
  raise CanonicalJSONError(
@@ -179,11 +191,11 @@ def _normalize_json(
179
191
  f"nesting exceeds {MAX_CANONICAL_DEPTH} levels",
180
192
  )
181
193
  members[0] += 1
182
- if members[0] > MAX_CANONICAL_MEMBERS:
194
+ if members[0] > max_members:
183
195
  raise CanonicalJSONError(
184
196
  "JSON_MEMBERS",
185
197
  path,
186
- f"value exceeds {MAX_CANONICAL_MEMBERS} members",
198
+ f"value exceeds {max_members} members",
187
199
  )
188
200
 
189
201
  if value is None or type(value) is bool:
@@ -236,6 +248,7 @@ def _normalize_json(
236
248
  depth=depth + 1,
237
249
  members=members,
238
250
  accept_fractions=accept_fractions,
251
+ max_members=max_members,
239
252
  )
240
253
  return result
241
254
  if isinstance(value, (list, tuple)):
@@ -246,6 +259,7 @@ def _normalize_json(
246
259
  depth=depth + 1,
247
260
  members=members,
248
261
  accept_fractions=accept_fractions,
262
+ max_members=max_members,
249
263
  )
250
264
  for index, item in enumerate(value)
251
265
  ]
@@ -2275,7 +2275,7 @@ def _dataset_conflict_remedy(refusal: ThinLaneError) -> str | None:
2275
2275
  return (
2276
2276
  "a Dataset of this name already exists in the workspace; if an earlier run of this bundle "
2277
2277
  'registered it, adopt it by id in dataset.json ({"dataset_id": "..."}) -- '
2278
- "mr-data list names the workspace's datasets -- or choose another name"
2278
+ "mr-data dataset list names the workspace's datasets -- or choose another name"
2279
2279
  )
2280
2280
 
2281
2281
 
@@ -51,17 +51,23 @@ from mostlyright.data_harness.thin.recipe_lint import lint_document, lint_refusa
51
51
  from mostlyright.data_harness.thin.session import StudioSession, open_studio_session
52
52
  from mostlyright.data_harness.thin.transport import ThinLaneError
53
53
  from mostlyright.data_harness.thin.v4 import identifier
54
- from mostlyright.data_harness.thin.v4_datasets import StudioV4DatasetClient, append_cell_body
54
+ from mostlyright.data_harness.thin.v4_datasets import (
55
+ StudioV4DatasetClient,
56
+ append_cell_body,
57
+ )
55
58
  from mostlyright.data_harness.thin.v4_narrative import v4_cell_receipt
59
+ from mostlyright.data_harness.thin.v4_runs import StudioV4RunClient
60
+ from mostlyright.data_harness.thin.v4_runs import start as start_run
56
61
  from mostlyright.data_harness.ux.credentials import resolve_cloud_credentials
57
62
  from mostlyright.data_harness.ux.plain_file import PlainFileRefusal, read_plain_file
58
63
 
59
64
  #: The one-line help both front doors print for this command.
60
- COMMAND_HELP = "register one recipe document and print the identifiers the server derived"
65
+ COMMAND_HELP = "register one recipe document, or propose a revision that also starts one replay"
61
66
 
62
67
  REGISTER_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe.v1"
63
68
  SHOW_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-read.v1"
64
69
  READINESS_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-readiness.v1"
70
+ PROPOSE_SCHEMA = f"{THIN_SCHEMA_PREFIX}-recipe-propose.v1"
65
71
 
66
72
  #: The bound on the document. The same number the authoring lane bounded one of its six documents
67
73
  #: by -- four times the contract's own 262144-byte ceiling on a canonical recipe -- rather than a
@@ -126,6 +132,7 @@ UNREFRESHABLE_PLAN_TAIL = (
126
132
  #: `mr-data recipe show ID` is how a person says it out loud.
127
133
  SHOW = "show"
128
134
  READINESS = "readiness"
135
+ PROPOSE = "propose"
129
136
  # Studio evaluates the exact persisted admission proof for each recipe revision. Keep this
130
137
  # inventory page small enough that a normal production workspace completes each bounded request
131
138
  # inside the thin client's request deadline, while preserving the established 40,000-item
@@ -148,15 +155,17 @@ def declare_arguments(parser: argparse.ArgumentParser) -> None:
148
155
  metavar="FILE",
149
156
  help=(
150
157
  "the recipe document to register, as a path to a JSON file; the word `show`, "
151
- "followed by a recipe identifier, to read one back; or `readiness` to inventory "
152
- "whether current tables may schedule a bounded incremental refresh"
158
+ "followed by a recipe identifier, to read one back; `readiness` to inventory "
159
+ "whether current tables may schedule a bounded incremental refresh; or `propose`, "
160
+ "followed by a recipe document path and `--sources-from RUN_ID`, to register a "
161
+ "revision and start exactly one retained-input replay (MCP `propose_revision`)"
153
162
  ),
154
163
  )
155
164
  parser.add_argument(
156
165
  "recipe_id",
157
166
  nargs="?",
158
167
  metavar="RECIPE_ID",
159
- help="with `show`, the recipe to read back",
168
+ help="with `show`, the recipe to read back; with `propose`, the recipe document path",
160
169
  )
161
170
  parser.add_argument(
162
171
  "--no-lint",
@@ -171,6 +180,18 @@ def declare_arguments(parser: argparse.ArgumentParser) -> None:
171
180
  help="record that the user delegated the brief, in their own words, and register; this "
172
181
  "is how `you choose` or `just build it` is written onto the dataset as the answer it is",
173
182
  )
183
+ parser.add_argument(
184
+ "--sources-from",
185
+ dest="sources_from_run",
186
+ metavar="RUN_ID",
187
+ help="with `propose`, replay the retained raw inputs of this successful run; required, "
188
+ "and replay never becomes live",
189
+ )
190
+ parser.add_argument(
191
+ "--confirm",
192
+ action="store_true",
193
+ help="with `propose`, authorize the projected spend if the replay is held for one",
194
+ )
174
195
 
175
196
 
176
197
  #: The flags that describe a registration. ``show`` reads one back and neither can mean anything
@@ -181,6 +202,13 @@ _REGISTER_ONLY_FLAGS: tuple[tuple[str, str], ...] = (
181
202
  ("delegated", "--delegated"),
182
203
  )
183
204
 
205
+ #: Flags that belong only to ``propose``. A plain registration (or show/readiness) that carries
206
+ #: one is refused by name rather than silently dropped -- same construction as _REGISTER_ONLY_FLAGS.
207
+ _PROPOSE_ONLY_FLAGS: tuple[tuple[str, str], ...] = (
208
+ ("sources_from_run", "--sources-from"),
209
+ ("confirm", "--confirm"),
210
+ )
211
+
184
212
 
185
213
  def _session(args: argparse.Namespace) -> StudioSession:
186
214
  return open_studio_session(resolve_cloud_credentials())
@@ -1159,6 +1187,163 @@ def readiness(
1159
1187
  }
1160
1188
 
1161
1189
 
1190
+ def propose(
1191
+ args: argparse.Namespace,
1192
+ *,
1193
+ client: StudioV4DatasetClient | None = None,
1194
+ run_client: StudioV4RunClient | None = None,
1195
+ ) -> dict[str, Any]:
1196
+ """``mr-data recipe propose FILE --sources-from RUN``: register + start one replay.
1197
+
1198
+ Mirrors cloud MCP ``propose_revision``: the revision is durable even when the retained-input
1199
+ replay is held for spend confirmation or refused. Validate ``--sources-from`` before
1200
+ registration so a bad run id never leaves an orphan revision. A non-held replay refusal
1201
+ returns ``revision_registered_replay_not_started`` with the immutable coordinates; a held
1202
+ replay returns outer status ``held`` (exit 2) so a script does not treat spend-gate as success.
1203
+ """
1204
+
1205
+ document_path = getattr(args, "recipe_id", None)
1206
+ if not isinstance(document_path, str) or not document_path:
1207
+ raise ThinLaneError(
1208
+ "THIN_REQUEST_INVALID",
1209
+ "mr-data recipe propose needs the recipe document path after the word propose",
1210
+ )
1211
+ sources_raw = getattr(args, "sources_from_run", None)
1212
+ if not isinstance(sources_raw, str) or not sources_raw:
1213
+ raise ThinLaneError(
1214
+ "THIN_REQUEST_INVALID",
1215
+ "mr-data recipe propose needs --sources-from RUN_ID naming the "
1216
+ "retained inputs to replay",
1217
+ )
1218
+ # Validate before register — a bad identifier must not leave a durable revision behind.
1219
+ sources_from = identifier(sources_raw, "source run identifier")
1220
+
1221
+ register_args = argparse.Namespace(**vars(args))
1222
+ register_args.target = document_path
1223
+ register_args.recipe_id = None
1224
+ registration = register(register_args, client=client)
1225
+
1226
+ coordinates = {
1227
+ "recipe_id": registration["recipe_id"],
1228
+ "recipe_digest": registration["recipe_digest"],
1229
+ "dataset_id": registration["dataset_id"],
1230
+ "table_id": registration["table_id"],
1231
+ "source_ids": registration["source_ids"],
1232
+ }
1233
+
1234
+ run_args = argparse.Namespace(
1235
+ recipe=registration["recipe_id"],
1236
+ digest=registration["recipe_digest"],
1237
+ mode="replay",
1238
+ sources_from_run=sources_from,
1239
+ max_rows=None,
1240
+ max_source_bytes=None,
1241
+ window=None,
1242
+ resource_class=None,
1243
+ progressive=False,
1244
+ resume_capture_run_id=None,
1245
+ confirm=bool(getattr(args, "confirm", False)),
1246
+ confirm_held=None,
1247
+ cancel=None,
1248
+ retry=None,
1249
+ approve_full=None,
1250
+ )
1251
+ try:
1252
+ replay = start_run(run_args, client=run_client)
1253
+ except ThinLaneError as refusal:
1254
+ payload: dict[str, Any] = {
1255
+ "schema_version": PROPOSE_SCHEMA,
1256
+ "status": "revision_registered_replay_not_started",
1257
+ "lane": "hosted",
1258
+ **coordinates,
1259
+ "sources_from_run": sources_from,
1260
+ "refusal": {"code": refusal.code, "detail": refusal.detail},
1261
+ "idempotent_by": registration.get("idempotent_by", "recipe_digest"),
1262
+ "brief": registration["brief"],
1263
+ "next": (
1264
+ "The recipe revision was registered, but its retained-input replay did not "
1265
+ "start. Keep these immutable coordinates and correct the replay precondition "
1266
+ "before proposing again."
1267
+ ),
1268
+ }
1269
+ _copy_registration_extras(registration, payload)
1270
+ return payload
1271
+
1272
+ if replay.get("status") == "run_awaiting_confirmation":
1273
+ held: dict[str, Any] = {
1274
+ "schema_version": PROPOSE_SCHEMA,
1275
+ "status": "held",
1276
+ "lane": "hosted",
1277
+ **coordinates,
1278
+ "sources_from_run": sources_from,
1279
+ "mode": "replay",
1280
+ "idempotent_by": registration.get("idempotent_by", "recipe_digest"),
1281
+ "brief": registration["brief"],
1282
+ "replay": replay,
1283
+ "next": (
1284
+ "Show the projected cost to the user, then call "
1285
+ f"`{replay.get('confirm_command', 'mr-data run --confirm …')}` only if they "
1286
+ "agree. The replay produces repair evidence only; no full run is approved."
1287
+ ),
1288
+ }
1289
+ for key in (
1290
+ "run_id",
1291
+ "projected_bytes",
1292
+ "projected_runtime_s",
1293
+ "projected_cost",
1294
+ "confirm_command",
1295
+ "build_scope",
1296
+ ):
1297
+ if key in replay:
1298
+ held[key] = replay[key]
1299
+ if "run_id" in held:
1300
+ held["replay_run_id"] = held["run_id"]
1301
+ _copy_registration_extras(registration, held)
1302
+ return held
1303
+
1304
+ payload = {
1305
+ "schema_version": PROPOSE_SCHEMA,
1306
+ "status": "recipe_proposed",
1307
+ "lane": "hosted",
1308
+ **coordinates,
1309
+ "sources_from_run": sources_from,
1310
+ "replay": replay,
1311
+ "idempotent_by": registration.get("idempotent_by", "recipe_digest"),
1312
+ "brief": registration["brief"],
1313
+ }
1314
+ run_body = replay.get("run") if isinstance(replay.get("run"), Mapping) else None
1315
+ if isinstance(run_body, Mapping) and isinstance(run_body.get("run_id"), str):
1316
+ payload["replay_run_id"] = run_body["run_id"]
1317
+ _copy_registration_extras(registration, payload)
1318
+ return payload
1319
+
1320
+
1321
+ def _copy_registration_extras(
1322
+ registration: Mapping[str, Any], payload: dict[str, Any]
1323
+ ) -> None:
1324
+ """Carry optional register fields onto a propose receipt without inventing them."""
1325
+
1326
+ for key in (
1327
+ "warnings",
1328
+ "columns_without_description",
1329
+ "sources_without_continuation",
1330
+ "delegation_cell",
1331
+ ):
1332
+ if key in registration:
1333
+ payload[key] = registration[key]
1334
+
1335
+
1336
+ def propose_exit_code(payload: Mapping[str, Any]) -> int:
1337
+ """Held or failed replay after a durable register is a gate, not a successful propose."""
1338
+
1339
+ status = payload.get("status")
1340
+ if status in {"held", "revision_registered_replay_not_started"}:
1341
+ return 2
1342
+ return 0
1343
+
1344
+
1345
+
1346
+
1162
1347
  def recipe(
1163
1348
  args: argparse.Namespace, *, client: StudioV4DatasetClient | None = None
1164
1349
  ) -> dict[str, Any]:
@@ -1174,7 +1359,7 @@ def recipe(
1174
1359
  if args.target in {SHOW, READINESS}:
1175
1360
  given = [
1176
1361
  flag
1177
- for destination, flag in _REGISTER_ONLY_FLAGS
1362
+ for destination, flag in (*_REGISTER_ONLY_FLAGS, *_PROPOSE_ONLY_FLAGS)
1178
1363
  if getattr(args, destination, None) not in (None, False)
1179
1364
  ]
1180
1365
  if given:
@@ -1191,12 +1376,25 @@ def recipe(
1191
1376
  "mr-data recipe readiness takes no recipe identifier",
1192
1377
  )
1193
1378
  return readiness(args, client=client)
1379
+ if args.target == PROPOSE:
1380
+ return propose(args, client=client)
1381
+ propose_only = [
1382
+ flag
1383
+ for destination, flag in _PROPOSE_ONLY_FLAGS
1384
+ if getattr(args, destination, None) not in (None, False)
1385
+ ]
1386
+ if propose_only:
1387
+ raise ThinLaneError(
1388
+ "THIN_REQUEST_INVALID",
1389
+ f"{', '.join(propose_only)} belong to mr-data recipe propose, not to a plain "
1390
+ "registration; say `propose` before the document path, or drop the flag",
1391
+ )
1194
1392
  if args.recipe_id:
1195
1393
  raise ThinLaneError(
1196
1394
  "THIN_REQUEST_INVALID",
1197
1395
  f"mr-data recipe takes one document to register, `show` and a recipe identifier, "
1198
- f"or `readiness`; "
1199
- f"{args.target!r} is neither",
1396
+ f"`readiness`, or `propose` and a document path; "
1397
+ f"{args.target!r} is none of those",
1200
1398
  )
1201
1399
  return register(args, client=client)
1202
1400
 
@@ -1210,6 +1408,8 @@ __all__ = [
1210
1408
  "NAMED_UNCONTINUED_SOURCES",
1211
1409
  "NAMED_UNDESCRIBED_COLUMNS",
1212
1410
  "NDJSON_SOURCE_BYTE_CEILING",
1411
+ "PROPOSE",
1412
+ "PROPOSE_SCHEMA",
1213
1413
  "READER_SOURCE_BYTE_CEILING",
1214
1414
  "READINESS",
1215
1415
  "READINESS_PAGE_SIZE",
@@ -1226,6 +1426,7 @@ __all__ = [
1226
1426
  "columns_without_description",
1227
1427
  "continuation_warnings",
1228
1428
  "declare_arguments",
1429
+ "propose",
1229
1430
  "read_document",
1230
1431
  "readiness",
1231
1432
  "recipe",
@@ -131,6 +131,12 @@ HOSTED_SUMMARY = dict(
131
131
  ("reader", "ask Studio to normalize reader options for a family and version"),
132
132
  ("source", "queue or read one bounded source inspection"),
133
133
  ("catalog", "ask the sealed public-source catalogue what might answer a question"),
134
+ (
135
+ "public",
136
+ "read the public dataset catalogue anonymously "
137
+ "(search, show, tables, schema, sample)",
138
+ ),
139
+ ("connected", "list or connect public datasets for this workspace (device key)"),
134
140
  ("stream", "record a public wss venue and name the recording a build reads"),
135
141
  ("keys", "store a source credential by name, name the stored ones, and withdraw one"),
136
142
  ("open", "hand this terminal's session to the browser as a single-use address"),
@@ -152,6 +158,7 @@ HOSTED_SUMMARY = dict(
152
158
  ("demote", "ask Studio to withdraw the live pointer from one table or many"),
153
159
  ("watch", "stream one run's live progress, resuming across stream cuts"),
154
160
  ("download", "bring one run's artifacts back here, each one's digest checked"),
161
+ ("artifacts", "list what one run sealed, without fetching bytes"),
155
162
  ("receipt", "print the record one run sealed about how it was built"),
156
163
  ("checks", "report how each check the recipe declared came out for one run"),
157
164
  ("note", "write one cell of this run's decision record, or list what is written"),
@@ -263,6 +270,7 @@ def _hosted_parser() -> argparse.ArgumentParser:
263
270
  from mostlyright.data_harness.thin import recipe as recipe_module
264
271
  from mostlyright.data_harness.thin import (
265
272
  v4_artifacts,
273
+ v4_connected,
266
274
  v4_connections,
267
275
  v4_dataset_covers,
268
276
  v4_datasets,
@@ -299,6 +307,13 @@ def _hosted_parser() -> argparse.ArgumentParser:
299
307
 
300
308
  v4_catalog.declare_arguments(_add("catalog"))
301
309
 
310
+ from mostlyright.data_harness.thin import v4_public
311
+
312
+ v4_public.declare_arguments(_add("public"))
313
+
314
+
315
+ v4_connected.declare_arguments(_add("connected"))
316
+
302
317
  # ------------------------------------------------------------------------------------
303
318
  # The source whose bytes do not exist until somebody records them
304
319
  # ------------------------------------------------------------------------------------
@@ -404,11 +419,13 @@ def _handlers() -> dict[str, Callable[[argparse.Namespace], dict[str, Any]]]:
404
419
  stream_venue,
405
420
  v4_artifacts,
406
421
  v4_catalog,
422
+ v4_connected,
407
423
  v4_connections,
408
424
  v4_dataset_covers,
409
425
  v4_datasets,
410
426
  v4_handoff,
411
427
  v4_narrative,
428
+ v4_public,
412
429
  v4_query,
413
430
  v4_reader,
414
431
  v4_runs,
@@ -442,6 +459,8 @@ def _handlers() -> dict[str, Callable[[argparse.Namespace], dict[str, Any]]]:
442
459
  # around the command rather than around its one verb, because the verb is a
443
460
  # positional and the module is where a second one would land.
444
461
  "catalog": v4_catalog.catalog,
462
+ "public": v4_public.public,
463
+ "connected": v4_connected.connected,
445
464
  # The venue lane. One name over eight verbs, because registering a document,
446
465
  # admitting it, probing it, capturing it, subscribing to it and reading back what
447
466
  # any of those produced are steps of one act -- and eight top-level names would be
@@ -453,6 +472,7 @@ def _handlers() -> dict[str, Callable[[argparse.Namespace], dict[str, Any]]]:
453
472
  "watch": v4_stream.watch,
454
473
  "status": v4_runs.status,
455
474
  "download": v4_artifacts.download,
475
+ "artifacts": v4_artifacts.artifacts,
456
476
  "verify": v4_artifacts.verify,
457
477
  "receipt": v4_artifacts.receipt,
458
478
  "checks": v4_artifacts.checks,
@@ -488,8 +508,10 @@ def _exit_codes() -> dict[str, Callable[[dict[str, Any]], int]]:
488
508
  """
489
509
 
490
510
  from mostlyright.data_harness.thin import probe as probe_module
511
+ from mostlyright.data_harness.thin import recipe as recipe_module
491
512
  from mostlyright.data_harness.thin import (
492
513
  v4_artifacts,
514
+ v4_connected,
493
515
  v4_datasets,
494
516
  v4_query,
495
517
  v4_runs,
@@ -508,6 +530,8 @@ def _exit_codes() -> dict[str, Callable[[dict[str, Any]], int]]:
508
530
  "table": v4_tables.lifecycle_exit_code,
509
531
  "demote": v4_tables.lifecycle_exit_code,
510
532
  "dataset": v4_datasets.dataset_exit_code,
533
+ "recipe": recipe_module.propose_exit_code,
534
+ "connected": v4_connected.connected_exit_code,
511
535
  }
512
536
 
513
537
 
@@ -28,6 +28,7 @@ from urllib.parse import urlencode
28
28
  from uuid import UUID
29
29
 
30
30
  from mostlyright.data_harness.canonical import (
31
+ MAX_CANONICAL_MEMBERS,
31
32
  CanonicalJSONError,
32
33
  canonical_json_bytes,
33
34
  parse_foreign_json,
@@ -235,6 +236,8 @@ class StudioApiClient:
235
236
  expected: tuple[int, ...],
236
237
  shape: type = dict,
237
238
  response_headers: dict[str, str] | None = None,
239
+ maximum: int = MAX_JSON_RESPONSE_BYTES,
240
+ max_members: int = MAX_CANONICAL_MEMBERS,
238
241
  ) -> Any:
239
242
  headers = {
240
243
  "Accept": "application/json",
@@ -250,7 +253,7 @@ class StudioApiClient:
250
253
  url = f"{self._session.studio_base_url}{path}{_query_string(query)}"
251
254
  try:
252
255
  status, raw, answered_headers = self._transport.request(
253
- method, url, headers, raw_body, MAX_JSON_RESPONSE_BYTES
256
+ method, url, headers, raw_body, maximum
254
257
  )
255
258
  if response_headers is not None:
256
259
  response_headers.update(answered_headers)
@@ -270,7 +273,7 @@ class StudioApiClient:
270
273
  # "Studio did not answer with an object". Nothing read here can be sealed either way:
271
274
  # `canonical_json_bytes` still refuses a float, so a value parsed here can be shown
272
275
  # and can never reach a digest.
273
- parsed = parse_foreign_json(raw)
276
+ parsed = parse_foreign_json(raw, max_members=max_members)
274
277
  except CanonicalJSONError:
275
278
  parsed = None
276
279
  if status not in expected:
@@ -63,6 +63,10 @@ CLOUD_SESSION_HANDOFF_PATH = "/api/cli/session-handoffs"
63
63
  #: this client sends a mode and renders what came back rather than deciding any part of it.
64
64
  CLOUD_DATASET_ACCESS_PATH = "/api/cli/datasets/{dataset_id}/access"
65
65
 
66
+ # Workspace dataset directory for agents (`mr-data dataset list`), same facts MCP's
67
+ # `list_my_datasets` reads from Cloud's own table.
68
+ CLOUD_DATASETS_LIST_PATH = "/api/cli/datasets"
69
+
66
70
  # Projection repair after a Studio metadata write, without a publication mutation.
67
71
  CLOUD_DATASET_IDENTITY_PATH = "/api/cli/datasets/{dataset_id}/identity"
68
72
 
@@ -530,6 +534,7 @@ class CloudClient:
530
534
 
531
535
  __all__ = [
532
536
  "CLOUD_APPROVAL_DASHBOARD_PATH",
537
+ "CLOUD_DATASETS_LIST_PATH",
533
538
  "CLOUD_DATASET_ACCESS_PATH",
534
539
  "CLOUD_DATASET_DASHBOARD_PATH",
535
540
  "CLOUD_DATASET_RUN_QUERY_KEY",