mostlyright-data 0.24.0__tar.gz → 0.25.0__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 (111) hide show
  1. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/PKG-INFO +12 -9
  2. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/README.md +11 -8
  3. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/pyproject.toml +1 -1
  4. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/SKILL.md +36 -1
  5. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +88 -0
  6. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +9 -5
  7. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/commands.md +7 -4
  8. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/promote.md +10 -7
  9. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/recipe.py +295 -2
  10. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/.gitignore +0 -0
  11. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/scripts/hatch_build.py +0 -0
  12. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/agents/openai.yaml +0 -0
  13. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  14. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  15. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  16. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  17. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  18. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  19. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  20. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/agent-protocol.md +0 -0
  21. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  22. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  23. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/boundaries.md +0 -0
  24. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  25. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  26. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/installation-parity.md +0 -0
  27. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/live-run.md +0 -0
  28. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  29. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  30. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/one-install.md +0 -0
  31. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/prediction-labels.md +0 -0
  32. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/readers.md +0 -0
  33. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/receipts.md +0 -0
  34. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  35. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  36. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/reference-pages.md +0 -0
  37. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/required-protocol.md +0 -0
  38. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/source-credentials.md +0 -0
  39. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/sources.md +0 -0
  40. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  41. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/transforms.md +0 -0
  42. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  43. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  44. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  45. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/__init__.py +0 -0
  46. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  47. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/canonical.py +0 -0
  48. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/formats.py +0 -0
  49. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  50. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/key_seam.py +0 -0
  51. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/page_coverage.py +0 -0
  52. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  53. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/session_probes.py +0 -0
  54. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/skill_assets.py +0 -0
  55. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/table_manifest.py +0 -0
  56. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  57. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  58. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  59. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/activity.py +0 -0
  60. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  61. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/categories.py +0 -0
  62. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/commands.py +0 -0
  63. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  64. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/download.py +0 -0
  65. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  66. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/parity.py +0 -0
  67. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/probe.py +0 -0
  68. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  69. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/propose.py +0 -0
  70. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  71. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  72. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/research.py +0 -0
  73. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/router.py +0 -0
  74. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/runs.py +0 -0
  75. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/session.py +0 -0
  76. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/stream.py +0 -0
  77. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  78. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/transport.py +0 -0
  79. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  80. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4.py +0 -0
  81. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  82. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  83. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  84. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  85. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  86. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  87. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  88. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  89. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  90. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  91. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  92. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  93. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  94. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  95. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  96. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  97. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  98. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  99. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  100. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  101. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  102. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  103. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  104. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  105. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  106. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  107. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/login.py +0 -0
  108. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  109. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  110. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  111. {mostlyright_data-0.24.0 → mostlyright_data-0.25.0}/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.24.0
3
+ Version: 0.25.0
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/
@@ -112,14 +112,17 @@ its raw inputs still retained. Replay compares against that run and never become
112
112
  Studio returns a typed refusal when replay is unavailable; the CLI does not fetch sources locally.
113
113
 
114
114
  The hosted engine executes sample, full and refresh runs. Studio's action classifier can name
115
- closed-source reuse, request-window or collection acquisition, and recorded stream input, but the
116
- current worker has only two executable normal-refresh materializers: an exact single direct
117
- partition request-window plan with a compatible predecessor, and an all-recorded-stream plan.
118
- Closed-only, collection, and mixed plans return `RESYNC_REQUIRED` before acquisition, as does a
119
- mutable snapshot or other unwindowed source. Nothing is revalidated or re-acquired whole as an
120
- implicit fallback. Use `mr-data table resync TABLE_ID --request-id UUID` when a deliberate full
121
- source reread is intended, retaining that UUID through an uncertain response. URL windows may use
122
- declared query parameters or path placeholders; a fixed date URL does not advance automatically.
115
+ closed-source reuse, request-window or collection acquisition, and recorded stream input, but
116
+ normal refresh admits only two families of plan: every source a recorded stream, and at least one
117
+ direct partition request window with a compatible predecessor, with `closed` reuse permitted
118
+ beside it. Once such a plan holds more than one source, every window in it must agree on one
119
+ `merge.materialization`, `merge.partition.column` and `merge.partition.key`, and that column must
120
+ be one the table declares. Closed-only plans, any plan carrying a collection, a window beside a
121
+ recorded stream, a mutable snapshot and any other unwindowed source return `RESYNC_REQUIRED`
122
+ before acquisition. Nothing is revalidated or re-acquired whole as an implicit fallback. Use
123
+ `mr-data table resync TABLE_ID --request-id UUID` when a deliberate full source reread is
124
+ intended, retaining that UUID through an uncertain response. URL windows may use declared query
125
+ parameters or path placeholders; a fixed date URL does not advance automatically.
123
126
  Configure a correction lookback where the publisher can revise earlier observations.
124
127
 
125
128
  A source exposing only its current snapshot cannot supply a historical delta. It is supported only
@@ -100,14 +100,17 @@ its raw inputs still retained. Replay compares against that run and never become
100
100
  Studio returns a typed refusal when replay is unavailable; the CLI does not fetch sources locally.
101
101
 
102
102
  The hosted engine executes sample, full and refresh runs. Studio's action classifier can name
103
- closed-source reuse, request-window or collection acquisition, and recorded stream input, but the
104
- current worker has only two executable normal-refresh materializers: an exact single direct
105
- partition request-window plan with a compatible predecessor, and an all-recorded-stream plan.
106
- Closed-only, collection, and mixed plans return `RESYNC_REQUIRED` before acquisition, as does a
107
- mutable snapshot or other unwindowed source. Nothing is revalidated or re-acquired whole as an
108
- implicit fallback. Use `mr-data table resync TABLE_ID --request-id UUID` when a deliberate full
109
- source reread is intended, retaining that UUID through an uncertain response. URL windows may use
110
- declared query parameters or path placeholders; a fixed date URL does not advance automatically.
103
+ closed-source reuse, request-window or collection acquisition, and recorded stream input, but
104
+ normal refresh admits only two families of plan: every source a recorded stream, and at least one
105
+ direct partition request window with a compatible predecessor, with `closed` reuse permitted
106
+ beside it. Once such a plan holds more than one source, every window in it must agree on one
107
+ `merge.materialization`, `merge.partition.column` and `merge.partition.key`, and that column must
108
+ be one the table declares. Closed-only plans, any plan carrying a collection, a window beside a
109
+ recorded stream, a mutable snapshot and any other unwindowed source return `RESYNC_REQUIRED`
110
+ before acquisition. Nothing is revalidated or re-acquired whole as an implicit fallback. Use
111
+ `mr-data table resync TABLE_ID --request-id UUID` when a deliberate full source reread is
112
+ intended, retaining that UUID through an uncertain response. URL windows may use declared query
113
+ parameters or path placeholders; a fixed date URL does not advance automatically.
111
114
  Configure a correction lookback where the publisher can revise earlier observations.
112
115
 
113
116
  A source exposing only its current snapshot cannot supply a historical delta. It is supported only
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.24.0"
3
+ version = "0.25.0"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -37,6 +37,24 @@ references. Load the reference for the current stage only, not the entire librar
37
37
  Bind `dataset.id`, describe columns, declare checks and units, and register with
38
38
  `mr-data recipe RECIPE.json --json`. Repair all lint findings before queuing work.
39
39
  The server returns the recipe digest; do not compute or invent it.
40
+ Decide each source's refresh continuation before you register it: `closed: true` where the
41
+ address names a range that has demonstrably ended, a `window` with **both** `request`
42
+ endpoints where the address carries a date range, a `collection` where the corpus is an index
43
+ of pages, and nothing where the source is mutable with no changed-since contract. A source
44
+ declaring none of these is `resync_only`, and one `resync_only` source makes the entire table
45
+ resync-only: it will never refresh on a schedule. Declaring every source is only the first
46
+ gate; the shape those declarations form is the second, and Studio admits two families only:
47
+ every source a recorded stream, or at least one `window` with a `request`, with `closed`
48
+ siblings permitted beside it. A closed-only plan, any plan carrying a `collection`, and a
49
+ `window` beside a recorded stream are each refused, so `closed: true` alone does not make a
50
+ table refresh. The second family is necessary and not sufficient: as soon as the plan holds
51
+ more than one source, every window in it must agree on one `merge.materialization`,
52
+ `merge.partition.column` and `merge.partition.key`, and that column must be a name
53
+ `table.columns` declares. Adding one `closed` sibling beside a lone window is what turns that
54
+ requirement on, so a window partitioning on the publisher's own header under a name the table
55
+ renames refreshes alone and stops the moment a second source joins it. Declare only what the
56
+ source's own address proves, and name the `resync_only` sources and the reason in the build
57
+ message.
40
58
  6. Read [build sizing](references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md).
41
59
  Choose bounds from measured source size; row and byte clamps apply per source. Use a bounded
42
60
  preview for unmeasured sources. Preserve the agreed semantics and quality checks.
@@ -72,7 +90,12 @@ merely to obtain a green run. Read [recovery details](references/agent-protocol.
72
90
  8. Read `mr-data checks RUN_ID --json`, `mr-data receipt RUN_ID --json` and
73
91
  `mr-data peek RUN_ID --json`; use bounded `mr-data query` for agreed quality questions.
74
92
  Verify row grain, joins, requested fields, coverage and every declared check. Successful
75
- execution alone is not a verified dataset. Read
93
+ execution alone is not a verified dataset. `receipt` carries one coverage entry per source --
94
+ `bytes_fetched`, `rows_kept`, `rows_available`, `truncated` and the window -- where the run
95
+ record folds all sources into four numbers. That is what each source ACTUALLY weighed and
96
+ carried, and it is the evidence the refresh decision belongs on rather than the ceiling you
97
+ typed in `limits.max_source_bytes` before anything was fetched. Carry the correction into
98
+ step 10. Read
76
99
  [verification](references/7-interrogate-ask-the-run-what-it-actually-delivered.md).
77
100
  9. Any truncated source means a preview, not the requested complete dataset. Inspect coverage
78
101
  even when the run succeeded. A null coverage window does not mean all history was acquired.
@@ -80,6 +103,18 @@ merely to obtain a green run. Read [recovery details](references/agent-protocol.
80
103
  Record cadence only when authorized and `mr-data recipe readiness --json` reports the current
81
104
  revision refresh-ready. A blocked mutable snapshot requires an explicit table resync, not a
82
105
  scheduled whole-source fallback. Do not promise freshness without evidence.
106
+ A recipe that is not refresh-ready is a design fault to repair, not a state to wait out: read
107
+ `blocking_source_names` and every source's classification in the readiness answer. That list
108
+ is all-or-nothing: when one source states no continuation Studio cannot derive a plan at all,
109
+ so it classifies EVERY source `resync_only` and names every one of them, and a twenty-source
110
+ recipe with one bare address comes back with twenty names rather than the one to repair. Read
111
+ the document to find which of them actually declares nothing rather than revising nineteen
112
+ truthful windows. An EMPTY list beside `refresh_ready: false` is the other answer: every source
113
+ is declared and the plan's SHAPE or its predecessor state is what refuses. Revise against what
114
+ this run measured, register the revision and resync once. The
115
+ revision changes the recipe digest, so the sealed predecessor stops matching and one full
116
+ resync is the price of the repair -- which is why the decision belongs in step 5. A table left
117
+ not refresh-ready never refreshes; it does not become ready later.
83
118
  Download and verify artifacts when the user requested bytes.
84
119
  10. Report rows and coverage actually verified, checks, limitations and the stable dataset link.
85
120
  Read [delivery](references/9-present-only-what-survived-inspection-with-caveats.md) for details.
@@ -292,6 +292,94 @@ names.
292
292
  Leaving it out is a defect in the same way a one-line dataset description is: the source is then
293
293
  drawn with its endpoint alone, and a reader is left to guess from a query string.
294
294
 
295
+ ### Say how each source refreshes
296
+
297
+ Studio reads one refresh strategy per source out of this document, and a source that states none
298
+ is `resync_only`. One `resync_only` source makes the whole table resync-only: it never refreshes
299
+ on a schedule, however truthful its siblings are.
300
+
301
+ | what the source declares | classification | next action |
302
+ | --- | --- | --- |
303
+ | `source_class: "stream"` | `recorded` | `recorded` |
304
+ | `closed: true` | `closed` | `reuse_predecessor` |
305
+ | a `collection` | `collection` | `acquire_incremental` |
306
+ | a `window` carrying a `request` | `window` | `acquire_incremental` |
307
+ | none of those | `resync_only` | `resync_required` |
308
+
309
+ The rows are in Studio's own precedence order -- stream, then `closed`, then `collection`, then
310
+ `window` -- because a source may declare more than one of those members and the first match wins.
311
+ A source carrying both `closed: true` and a request window is `closed`, and its window is never
312
+ read. A recorded stream that also says `closed: true` is `recorded`, not `closed`.
313
+
314
+ `closed: true` says the range at this address is over and a refresh may reuse the bytes the
315
+ predecessor sealed rather than ask again. Use it for a range that has demonstrably ended, and
316
+ understand what you are accepting: if the publisher later restates that period, the table keeps
317
+ serving the old bytes, no run fails, and only an explicit resync repairs it. Never mark the
318
+ current period closed -- that drops new data outright, which is a different and worse mistake.
319
+
320
+ A `window` is the better answer wherever the address carries time, because it re-requests its
321
+ recent range on every refresh and so picks up corrections inside `lookback_seconds` on its own.
322
+ It needs `start_at`, `granularity`, `timezone`, `lookback_seconds`, `max_span_seconds` and
323
+ `merge`, and a `request` naming BOTH ends -- an address that names only one side of a range
324
+ cannot be narrowed. Each grammar below is closed; a spelling outside it belongs to a later
325
+ version and is refused.
326
+
327
+ - `request.start` and `request.end` take `encoding`: `date_parts` (names three parameters and
328
+ takes `pad`), `iso_date` or `epoch_seconds` (each names one parameter and takes no `pad`).
329
+ - `bound` is `inclusive` or `exclusive`. The engine's own window is half-open
330
+ `[start_inclusive, end_exclusive)` in UTC; start/inclusive and end/exclusive pass both instants
331
+ through unchanged, and the other two spellings shift by a day. **Probe the publisher rather
332
+ than assuming**: both spellings validate against the schema, and only the start is checked at
333
+ registration, so a wrong `end` silently fetches a different range than the address states.
334
+ - `merge.materialization` is `partition_replace` only.
335
+ - `merge.partition.column` names a column of the ACQUIRED relation -- the header the publisher
336
+ sent, not the name your statement gives it. **That holds only while the window is the recipe's
337
+ one source.** In a plan of more than one source the column must ALSO be a name `table.columns`
338
+ declares, so a renamed field is refused at admission and has to take the one-source route. If
339
+ this recipe has, or will have, a second source of any kind -- a `closed` sibling is enough --
340
+ partition on a name the table itself declares.
341
+ - `merge.partition.key` is `iso_date_prefix` (first ten characters of an ISO value),
342
+ `iso_date_value` (a complete ISO date) or `compact_utc_hour` (a real `YYYYMMDDHH`).
343
+ - `merge.row_identity` names the columns the merged relation must be unique on.
344
+
345
+ `start_at` is a UTC midnight, and the start parameters already written in the address must render
346
+ exactly that instant. If the address says `year1=2026&month1=5&day1=31`, `start_at` is
347
+ `2026-05-31T00:00:00Z`.
348
+
349
+ Declaring nothing is the honest answer for a mutable address with no changed-since contract, and
350
+ pagination is not one. Say which sources you left `resync_only` and why, because a recipe that
351
+ never asked the question and one that asked and answered `resync_only` register identically.
352
+
353
+ **Declaring every source is the FIRST gate, and the SHAPE of the plan is the second.** Once Studio
354
+ has one action per source it asks whether a worker can materialize the plan those actions make,
355
+ and admits two families only: every source a recorded stream, which appends sealed batches, or at
356
+ least one `window` carrying a `request`, with `closed` siblings permitted beside it, which replaces
357
+ the bounded partitions the window asked for. A closed-only plan refreshes nothing -- it restores
358
+ the predecessor's bytes and leaves no partition to replace. Any `collection` in the plan refuses
359
+ the whole table, alone or beside a window. A `window` beside a recorded stream refuses it too. Each
360
+ of those is `RESYNC_REQUIRED` before acquisition: the table registers, builds and serves, and only
361
+ an explicit resync ever moves it again. So `closed: true` alone does NOT make a table refresh, and
362
+ marking every source closed to clear the first warning buys a table that is just as dead and warns
363
+ about nothing.
364
+
365
+ **The second family is NECESSARY and not sufficient, and the count is of sources.** A plan holding
366
+ more than one source -- one `window` and one `closed` sibling is already two -- is materialized by
367
+ the several-source lane, which writes ONE table manifest. Every window in such a plan must
368
+ therefore agree with the others on `merge.materialization`, `merge.partition.column` and
369
+ `merge.partition.key`, and each partition column must be a name `table.columns` declares.
370
+ Disagreeing windows, and a partition column the table does not declare, are both
371
+ `RESYNC_REQUIRED`. So adding a `closed` sibling beside a lone window is not free: it is what turns
372
+ the declared-column requirement on.
373
+
374
+ **Decide it from MEASUREMENT, not from the ceiling you typed.** `limits.max_source_bytes` is a
375
+ bound written before anything was fetched, and it is wrong in both directions: one live publisher
376
+ came in an order of magnitude under the ceiling declared for it, and another came in over. The
377
+ run's own coverage is the other thing entirely -- `mr-data receipt RUN --json` carries one entry
378
+ per source with `bytes_fetched`, `rows_kept`, `rows_available` and `truncated`, which is what that
379
+ source actually weighed and how many rows it actually carried. Build once, read that, and correct
380
+ the declarations against it before you record a cadence. A revision changes the recipe digest and
381
+ costs one full resync, so the cheapest time to be right is here.
382
+
295
383
  ### Say what each column is, and what it shows
296
384
 
297
385
  **Every column carries a `description`, and every column that measures a physical quantity carries
@@ -183,11 +183,15 @@ settle it. `mr-data run --cancel RUN_ID` stops a run that is queued or running.
183
183
 
184
184
  Do not treat the accepted command vocabulary as execution evidence. Before proposing a schedule,
185
185
  classify every source under one truthful continuation strategy and record the supporting evidence.
186
- Current normal refresh materializes only an exact single direct `window.request` source with a
187
- compatible partitioned predecessor, or an all-recorded-stream recipe. The classifier may name
188
- closed reuse or collection continuation, but closed-only, collection-only, and mixed plans return
189
- `RESYNC_REQUIRED` before acquisition because their bounded table materializers do not exist yet. A
190
- `window.snapshot` or unwindowed mutable source is likewise resync-only. Explicit resync is full,
186
+ Normal refresh materializes an all-recorded-stream recipe, or a plan holding at least one direct
187
+ `window.request` source with a compatible partitioned predecessor, `closed` reuse permitted beside
188
+ it. Beside it is not free: as soon as the plan holds more than one source, every window in it must
189
+ agree on one `merge.materialization`, `merge.partition.column` and `merge.partition.key`, and that
190
+ column must be a name the table declares. The classifier may name closed reuse or collection
191
+ continuation, but closed-only plans, any plan carrying a collection, and a window beside a
192
+ recorded stream return `RESYNC_REQUIRED` before acquisition because their bounded table
193
+ materializers do not exist yet. A `window.snapshot` or unwindowed mutable source is likewise
194
+ resync-only. Explicit resync is full,
191
195
  has no predecessor, and a collection starts from the beginning. Never hide one behind conditional
192
196
  revalidation, generic pagination, or a whole-source comparison. Research publisher cursors,
193
197
  revision identities, listings, corrections, and deletions, but use only deployed recipe grammar
@@ -14,7 +14,7 @@ gap rather than doing anything, and `export-hosted-candidate`, which is a backen
14
14
  | `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; a saved write whose public sync fails exits 2 and reports `public_projection_synced: false` — use `dataset sync ID` to retry that sync without rewriting Studio; `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. |
15
15
  | `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. The document is read first and every fault comes back in one refusal, each with the JSON pointer that names it; `--no-lint` sends it as written instead. Registration refuses `THIN_BRIEF_MISSING` when the dataset's record carries no answered question and no delegation, and `THIN_RECIPE_DATASET_UNBOUND` when the document names no `dataset.id`; `--delegated "their words"` writes the delegation onto the dataset and registers in the same command, and clears the first of those two and never the second. `mr-data recipe show ID` reads one back. |
16
16
  | `mr-data cover` | Generate and attach one branded 1200×630 dataset cover. Give it the dataset ID and what the image should depict; Studio fixes the model, single-color style, curated random palette, dimensions and storage. |
17
- | `mr-data run` | Start one run against a registered recipe, named as `--recipe RECIPE_ID --digest RECIPE_DIGEST` — both required, neither positional, and the digest is the bare hex the registration receipt printed. The mode is one of six: `sample`, `full`, `refresh`, `backfill`, `compact` and `replay`. Four have a shorthand flag (`--sample`, `--full`, `--refresh`, `--backfill`) and two do not, so write `--mode MODE`, which is accepted for every one of them. A refresh executes only Studio's persisted strict source-action plan. Although the classifier may name closed reuse, a request-window or collection delta, or recorded input, current normal refresh materializes only an exact single direct partition request-window plan with a compatible predecessor, or an all-recorded-stream plan. Closed-only, collection, mixed, snapshot, and unwindowed plans return `RESYNC_REQUIRED` before acquisition. Use `mr-data table resync TABLE_ID --request-id UUID` for an explicit full source reread, retaining that UUID if the response is uncertain. `--backfill` states the exact window with `--window START END`. `--mode replay --sources-from RUN_ID` asks Studio to run the registered revision against the RETAINED RAW INPUTS of one named successful run of the same table: nothing is fetched from this computer, nothing is acquired again, the result is compared against that run and never becomes the live version, and it neither asks for nor records an approval. Where Studio has replay switched off, or the named run is not successful, not the same table, or no longer retains its inputs, it answers a typed refusal — report the code rather than retrying in another mode. `--sources-from` on any other mode is refused `THIN_ARGUMENT_INVALID` before anything is sent. `--max-rows` and `--max-source-bytes` bound ONE SOURCE rather than the finished table. A large run is held for a spend confirmation, which `--confirm` settles and whose printed `confirm_command` re-states every argument the request carried. `--approve-full RUN_ID` releases the full a sample-first pair is holding once its preview has succeeded, naming either half of the pair; where the deployment still wants a person at a browser it answers `THIN_INTERACTIVE_HUMAN_REQUIRED` and names the run page. `--retry RUN_ID` tries one failed run again when its triple carries `room_fault: true`, re-stating that run's own coordinate under a byte ceiling that cannot narrow it, and refuses with the triple when the recipe was at fault. `--cancel RUN` stops one. |
17
+ | `mr-data run` | Start one run against a registered recipe, named as `--recipe RECIPE_ID --digest RECIPE_DIGEST` — both required, neither positional, and the digest is the bare hex the registration receipt printed. The mode is one of six: `sample`, `full`, `refresh`, `backfill`, `compact` and `replay`. Four have a shorthand flag (`--sample`, `--full`, `--refresh`, `--backfill`) and two do not, so write `--mode MODE`, which is accepted for every one of them. A refresh executes only Studio's persisted strict source-action plan. Although the classifier may name closed reuse, a request-window or collection delta, or recorded input, current normal refresh materializes only a plan carrying at least one direct `partition_replace` request window with a compatible predecessor — `closed` siblings may reuse their sealed bytes beside it, a lone window needs a retained source-history record, and several must agree on materialization and partition layout — or an all-recorded-stream plan. Closed-only, any plan carrying a collection, a window beside a recorded stream, snapshot and unwindowed plans return `RESYNC_REQUIRED` before acquisition. Use `mr-data table resync TABLE_ID --request-id UUID` for an explicit full source reread, retaining that UUID if the response is uncertain. `--backfill` states the exact window with `--window START END`. `--mode replay --sources-from RUN_ID` asks Studio to run the registered revision against the RETAINED RAW INPUTS of one named successful run of the same table: nothing is fetched from this computer, nothing is acquired again, the result is compared against that run and never becomes the live version, and it neither asks for nor records an approval. Where Studio has replay switched off, or the named run is not successful, not the same table, or no longer retains its inputs, it answers a typed refusal — report the code rather than retrying in another mode. `--sources-from` on any other mode is refused `THIN_ARGUMENT_INVALID` before anything is sent. `--max-rows` and `--max-source-bytes` bound ONE SOURCE rather than the finished table. A large run is held for a spend confirmation, which `--confirm` settles and whose printed `confirm_command` re-states every argument the request carried. `--approve-full RUN_ID` releases the full a sample-first pair is holding once its preview has succeeded, naming either half of the pair; where the deployment still wants a person at a browser it answers `THIN_INTERACTIVE_HUMAN_REQUIRED` and names the run page. `--retry RUN_ID` tries one failed run again when its triple carries `room_fault: true`, re-stating that run's own coordinate under a byte ceiling that cannot narrow it, and refuses with the triple when the recipe was at fault. `--cancel RUN` stops one. |
18
18
  | `mr-data status` | Report which of the seven states one run is in, what it delivered and whether a ceiling cut it short, and — on a failure — what failed, where, and whether the fault was the execution room's rather than the recipe's. Exits non-zero on a failed run. |
19
19
  | `mr-data runs` | Report this workspace's own runs: identifier, mode, state and creation time, plus the failure triple of any that failed and whether that failure was the execution room's. `--status` and `--mode` narrow it and are refused `THIN_ARGUMENT_INVALID` for a word outside the seven states or the six modes; `--limit` says how many, and pages are followed to reach it. A narrow filter over a workspace of thousands of runs reads a long way to find its matches, and a listing that does not end says how many were read and suggests a smaller `--limit`. |
20
20
  | `mr-data watch` | Stream one run's live progress under the durable event type of each stage, resuming across stream cuts. Exits non-zero when the run failed. |
@@ -46,9 +46,12 @@ action, and blocking source names. It reads persisted facts; it does not authori
46
46
 
47
47
  A refresh executes only Studio's persisted source-action plan. The classifier may name closed
48
48
  reuse, a request window or collection delta, or recorded input, but current normal refresh
49
- materializes only an exact single direct partition request-window plan with a compatible
50
- predecessor, or an all-recorded-stream plan. Closed-only, collection, mixed, snapshot, and
51
- unwindowed plans return `RESYNC_REQUIRED` before acquisition. `mr-data table resync TABLE --request-id UUID` is the explicit
49
+ materializes only a plan carrying at least one direct `partition_replace` request window with a
50
+ compatible predecessor, or an all-recorded-stream plan. `closed` siblings may reuse their sealed
51
+ bytes beside a window; a lone window needs a retained source-history record, and several windows
52
+ must agree on materialization and partition layout. Closed-only, any plan carrying a collection, a
53
+ window beside a recorded stream, snapshot and unwindowed plans return `RESYNC_REQUIRED` before
54
+ acquisition. `mr-data table resync TABLE --request-id UUID` is the explicit
52
55
  full reread; retain and reuse the caller-generated UUID after a lost response. A held resync receipt
53
56
  prints `mr-data run --confirm-held RUN_ID` for that exact run. A sample-first resync instead uses
54
57
  `--approve-full` only after its preview succeeds.
@@ -47,13 +47,16 @@ the cadence itself, and it belongs to the user.
47
47
  This call records a schedule; it is not proof of data continuation. Schedule only a current recipe
48
48
  that `mr-data recipe readiness --json` reports as refresh-ready. Studio must have a persisted bounded
49
49
  action for every source. The classifier may name closed reuse, a request-window or collection
50
- delta, or recorded stream input, but current normal refresh executes only an exact single direct
51
- partition request-window plan with a compatible predecessor, or an all-recorded-stream plan.
52
- Closed-only, collection, mixed, snapshot, and unwindowed plans return `RESYNC_REQUIRED` before
53
- acquisition. Use `mr-data table resync TABLE_ID --request-id UUID` when a full reread is
54
- genuinely needed; it is an explicit run, not a schedule fallback. Only returned state and durable
55
- run evidence support a claim that the table is caught up or refreshing. If that evidence is absent,
56
- report that a schedule was recorded and that ongoing freshness is unavailable.
50
+ delta, or recorded stream input, but normal refresh admits only an all-recorded-stream plan, or a
51
+ plan holding at least one direct partition request window with a compatible predecessor, `closed`
52
+ reuse permitted beside it. A plan of more than one source is admitted only when its windows agree
53
+ on one `merge.materialization`, `merge.partition.column` and `merge.partition.key`, and that
54
+ column is a name the table declares. Closed-only, collection, snapshot, and unwindowed plans, and
55
+ a window beside a recorded stream, return `RESYNC_REQUIRED` before acquisition. Use `mr-data
56
+ table resync TABLE_ID --request-id UUID` when a full reread is genuinely needed; it is an
57
+ explicit run, not a schedule fallback. Only returned state and durable run evidence support a
58
+ claim that the table is caught up or refreshing. If that evidence is absent, report that a
59
+ schedule was recorded and that ongoing freshness is unavailable.
57
60
 
58
61
  `mr-data pin TABLE_ID --version VERSION_ID` freezes the pointer on an exact version for a rollback
59
62
  or a hold, `mr-data unpin TABLE_ID` resumes tracking, and `mr-data demote TABLE_ID` detaches the
@@ -31,7 +31,7 @@ which is the point of having two.
31
31
  from __future__ import annotations
32
32
 
33
33
  import argparse
34
- from collections.abc import Mapping, Sequence
34
+ from collections.abc import Collection, Mapping, Sequence
35
35
  from pathlib import Path
36
36
  from typing import Any
37
37
  from uuid import UUID
@@ -109,6 +109,19 @@ DRAINED_SOURCE_BYTE_CEILING = 16_777_216
109
109
  #: nothing is lost by naming a dozen and saying how many more there are.
110
110
  NAMED_UNDESCRIBED_COLUMNS = 12
111
111
 
112
+ #: How many sources stating no bounded continuation the warning names before it counts the rest.
113
+ #: The same dozen as the columns above and for the same reason, against a different ceiling: a
114
+ #: recipe may declare many sources, and a sentence that spelt every undeclared one of them out
115
+ #: would be the wall the `numbered` receipt member exists to avoid.
116
+ NAMED_UNCONTINUED_SOURCES = 12
117
+
118
+ #: The fact every plan-shape sentence ends on. The refusal is about the SCHEDULE and not about the
119
+ #: registration, so each of them says what is still true -- the table registers, builds and serves
120
+ #: -- and names the one operation that moves it afterwards.
121
+ UNREFRESHABLE_PLAN_TAIL = (
122
+ "This table still registers and builds; it refreshes only by an explicit resync"
123
+ )
124
+
112
125
  #: The word that selects the read-back form. A positional rather than a flag because
113
126
  #: `mr-data recipe show ID` is how a person says it out loud.
114
127
  SHOW = "show"
@@ -292,6 +305,265 @@ def source_byte_warnings(sources: Any) -> list[str]:
292
305
  return said
293
306
 
294
307
 
308
+ def sources_without_continuation(sources: Any) -> list[str]:
309
+ """The sources a scheduled refresh cannot continue, named in document order.
310
+
311
+ The branch order is Studio's own, from ``refresh_plan.action_plan``: a stream is ``recorded``,
312
+ ``closed`` is ``reuse_predecessor``, a ``collection`` and a ``window`` carrying a ``request``
313
+ are ``acquire_incremental``, and everything else is ``resync_only``. A ``window`` with only a
314
+ ``snapshot`` is counted here deliberately: it states an address, not a bounded continuation.
315
+
316
+ One walk, because the receipt carries both the count and the sentence and the pair must not be
317
+ able to disagree -- the same rule :func:`columns_without_description` follows.
318
+ """
319
+
320
+ if not isinstance(sources, list):
321
+ return []
322
+ named: list[str] = []
323
+ for index, source in enumerate(sources):
324
+ if not isinstance(source, Mapping):
325
+ continue
326
+ if source.get("source_class") == "stream" or source.get("closed") is True:
327
+ continue
328
+ if isinstance(source.get("collection"), Mapping):
329
+ continue
330
+ window = source.get("window")
331
+ if isinstance(window, Mapping) and isinstance(window.get("request"), Mapping):
332
+ continue
333
+ name = source.get("name")
334
+ named.append(name if isinstance(name, str) and name else f"sources[{index}]")
335
+ return named
336
+
337
+
338
+ def _declared_column_names(table: Any) -> set[str] | None:
339
+ """The names ``table.columns`` declares, or ``None`` when this document states none readably.
340
+
341
+ ⚠ AN EMPTY SET AND ``None`` ARE DIFFERENT ANSWERS, which is why this does not just return a
342
+ set. Studio builds the same set off the same member and refuses a partition column that is not
343
+ in it, so a table declaring no columns really does refuse every window. A table this client
344
+ could not read is a question it cannot answer, and it says so with ``None`` rather than with an
345
+ empty set that would refuse the same plans for a reason it never established.
346
+
347
+ ⚠ AND ``None`` IS NOT THE REGISTRATION PATH. :func:`check_document` has already refused a
348
+ document carrying no ``table`` by the time this is read, so the only documents that reach the
349
+ silent half are ones whose ``table`` is present and not an object, or whose ``columns`` is not
350
+ a list -- both refused by the schema. The `window` beside `closed` plan, which is the shape
351
+ this check exists for, always arrives with a real column list.
352
+ """
353
+
354
+ if not isinstance(table, Mapping):
355
+ return None
356
+ columns = table.get("columns")
357
+ if not isinstance(columns, list):
358
+ return None
359
+ return {
360
+ column["name"]
361
+ for column in columns
362
+ if isinstance(column, Mapping) and isinstance(column.get("name"), str)
363
+ }
364
+
365
+
366
+ def _window_layout_refusal(
367
+ sources: Sequence[Any], *, declared_columns: Collection[str] | None
368
+ ) -> str | None:
369
+ """The sentence a several-source windowed plan is refused with, or ``None``.
370
+
371
+ Studio's ``_windowed_table_layout_is_consistent``, read the way it reads it: for every direct
372
+ window in the plan, ``merge.materialization``, ``merge.partition.column`` and
373
+ ``merge.partition.key`` must all be stated as strings, the column must be one the table itself
374
+ declares, and the three together must be identical across the windows. Closed siblings are
375
+ skipped because they are reuse rather than a window; collections and recorded streams never
376
+ reach this branch, which the caller has already refused them on.
377
+
378
+ ``declared_columns`` of ``None`` answers only the half that does not need it. The
379
+ agreement and completeness checks read nothing but the sources.
380
+ """
381
+
382
+ layouts: set[tuple[str, str, str]] = set()
383
+ for index, source in enumerate(sources):
384
+ if source.get("source_class") == "stream" or source.get("closed") is True:
385
+ continue
386
+ if isinstance(source.get("collection"), Mapping):
387
+ continue
388
+ stated = source.get("name")
389
+ named = repr(stated) if isinstance(stated, str) and stated else f"sources[{index}]"
390
+ window = source.get("window")
391
+ merge = window.get("merge") if isinstance(window, Mapping) else None
392
+ partition = merge.get("partition") if isinstance(merge, Mapping) else None
393
+ materialization = merge.get("materialization") if isinstance(merge, Mapping) else None
394
+ column = partition.get("column") if isinstance(partition, Mapping) else None
395
+ key = partition.get("key") if isinstance(partition, Mapping) else None
396
+ if (
397
+ not isinstance(materialization, str)
398
+ or not isinstance(column, str)
399
+ or not isinstance(key, str)
400
+ ):
401
+ return (
402
+ "Every source states a continuation, but this plan holds more than one source and "
403
+ f"the window on {named} states no complete `merge.partition`: a plan of several "
404
+ "sources is materialized by the generic delta worker, which writes ONE table "
405
+ "manifest, so Studio reads `merge.materialization`, `merge.partition.column` and "
406
+ "`merge.partition.key` off every window in the plan and refuses it when one of "
407
+ f"them is missing. {UNREFRESHABLE_PLAN_TAIL}"
408
+ )
409
+ if declared_columns is not None and column not in declared_columns:
410
+ return (
411
+ "Every source states a continuation, but this plan holds more than one source and "
412
+ f"the window on {named} partitions on {column!r}, which `table.columns` does not "
413
+ "declare: the generic delta worker carries ONE identifier across the source, the "
414
+ "transform and the table manifest, so a partition column the statement renames on "
415
+ "the way through is refused rather than localized. Partition on the name the "
416
+ "TABLE declares, or leave this window the only one in the plan, which is the "
417
+ f"separately bounded single-source route. {UNREFRESHABLE_PLAN_TAIL}"
418
+ )
419
+ layouts.add((materialization, column, key))
420
+ if len(layouts) > 1:
421
+ disagreement = ", ".join(sorted(f"`{column}`/`{key}`" for _, column, key in layouts))
422
+ return (
423
+ "Every source states a continuation, but this plan's windows disagree on the table "
424
+ f"partition layout: {disagreement}. Several windows share one refresh only when their "
425
+ "`merge.materialization`, `merge.partition.column` and `merge.partition.key` are "
426
+ "identical, because the generic delta worker writes ONE table manifest and cannot "
427
+ "replace two different partitionings in one pass. Give every window the same layout, "
428
+ f"or build them as separate tables. {UNREFRESHABLE_PLAN_TAIL}"
429
+ )
430
+ return None
431
+
432
+
433
+ def unrefreshable_plan_shape(
434
+ sources: Any, *, declared_columns: Collection[str] | None = None
435
+ ) -> str | None:
436
+ """The one sentence saying a FULLY DECLARED recipe still cannot refresh, or ``None``.
437
+
438
+ ⚠ DECLARING EVERY SOURCE IS THE FIRST GATE, NOT THE ONLY ONE.
439
+ :func:`sources_without_continuation` answers whether Studio can derive one action per source.
440
+ ``assess_refresh_readiness`` then asks a second question of the plan those actions FORM, and
441
+ admits two families: every source a recorded stream, or at least one direct ``window`` with no
442
+ ``collection`` anywhere in the plan and no recorded stream beside it. A closed-only plan
443
+ restores the predecessor's bytes and leaves no bounded partition to replace; a collection
444
+ carries a ledger that cannot join a replacement-partition proof; a window beside a recorded
445
+ stream asks one worker to append and replace at once. Each is `409 RESYNC_REQUIRED`.
446
+
447
+ ⚠ AND THE SECOND FAMILY IS NECESSARY RATHER THAN SUFFICIENT. A plan of MORE THAN ONE source is
448
+ materialized by the generic delta worker, which writes one table manifest, so Studio holds
449
+ every direct window in such a plan to one layout: identical ``merge.materialization``,
450
+ ``merge.partition.column`` and ``merge.partition.key``, each stated, and the column among the
451
+ names ``table.columns`` declares. That is why ``declared_columns`` is taken here. Studio reads
452
+ it off the document being registered, and a caller that does not have it gets the half of the
453
+ answer that does not need it rather than a refusal invented from a member nobody read. The
454
+ count is of SOURCES and not of windows: one window beside one closed source is already a plan
455
+ of two, and it is held to this same gate.
456
+
457
+ ⚠ WITHOUT THIS, THE REMEDY FOR THE FIRST WARNING BECOMES THE NEXT SILENT FAILURE. ``closed:
458
+ true`` is the cheapest declaration that makes a source continuable, so an author who reads only
459
+ the first sentence marks twelve sources closed, is warned about NOTHING, and still holds a
460
+ table that never refreshes. `closed: true` alone does not make a table refresh.
461
+
462
+ The branch order is Studio's own twice over -- ``refresh_plan.action_plan`` and the readiness
463
+ inventory both read ``closed`` BEFORE ``window`` -- so a source carrying both is ``closed``
464
+ here exactly as it is there. That is the ordering that bites: a closed source beside a window
465
+ is reuse rather than a second window, and a recipe whose every source says ``closed: true``
466
+ is a closed-only plan however many windows it also wrote.
467
+
468
+ Answered only once the first gate passes. With an undeclared source in the document the plan
469
+ has no settled shape to judge, and two sentences about one repair read as two faults.
470
+ """
471
+
472
+ if not isinstance(sources, list) or not sources:
473
+ return None
474
+ if sources_without_continuation(sources):
475
+ return None
476
+ held: set[str] = set()
477
+ for source in sources:
478
+ if not isinstance(source, Mapping):
479
+ return None
480
+ if source.get("source_class") == "stream":
481
+ held.add("recorded")
482
+ elif source.get("closed") is True:
483
+ held.add("closed")
484
+ elif isinstance(source.get("collection"), Mapping):
485
+ held.add("collection")
486
+ else:
487
+ held.add("window")
488
+ if held == {"recorded"}:
489
+ return None
490
+ if "window" in held and "collection" not in held and "recorded" not in held:
491
+ # One source is the single-window lane, which continues from its own retained source
492
+ # history and is never held to this. Two or more is the generic delta worker's one
493
+ # manifest, and that is where the windows have to agree with each other and with the table.
494
+ if len(sources) == 1:
495
+ return None
496
+ return _window_layout_refusal(sources, declared_columns=declared_columns)
497
+ if "collection" in held:
498
+ return (
499
+ "Every source states a continuation, but this plan carries a page collection and "
500
+ "Studio admits no bounded incremental materialization for one: a collection restores "
501
+ "its ledger from the raw snapshot and cannot join the replacement-partition proof a "
502
+ "refresh writes, so ANY collection in the plan refuses the whole table. "
503
+ f"{UNREFRESHABLE_PLAN_TAIL}"
504
+ )
505
+ if "window" in held:
506
+ return (
507
+ "Every source states a continuation, but this plan puts a `window` beside a recorded "
508
+ "stream, and no worker appends recorded batches and replaces bounded partitions in "
509
+ f"one pass: the two continuations cannot share a refresh. {UNREFRESHABLE_PLAN_TAIL}"
510
+ )
511
+ if "recorded" in held:
512
+ return (
513
+ "Every source states a continuation, but this plan is `closed: true` beside a "
514
+ "recorded stream, and only an ALL-recorded plan appends: a closed source restores "
515
+ "bytes and leaves the refresh no bounded partition to replace. "
516
+ f"{UNREFRESHABLE_PLAN_TAIL}"
517
+ )
518
+ return (
519
+ "Every source states a continuation, but every one of them is `closed: true`, and a "
520
+ "closed-only plan refreshes nothing: it restores the predecessor's bytes and has no "
521
+ "bounded partition to replace, so Studio refuses it before it can spend a slot. If these "
522
+ "sources really are finished, an explicit resync is the operation that moves this table "
523
+ "and there is nothing here to repair. Where one of them is not, a `window` carrying both "
524
+ "`request` endpoints is the declaration that makes a table refresh on a schedule -- but "
525
+ "write one only where the publisher honours it, because a window a publisher does not "
526
+ f"honour serves the wrong range and nothing says so. {UNREFRESHABLE_PLAN_TAIL}"
527
+ )
528
+
529
+
530
+ def continuation_warnings(absent: list[str], total: int) -> list[str]:
531
+ """One sentence naming the sources a scheduled refresh will refuse to continue.
532
+
533
+ ⚠ A WARNING RATHER THAN A FINDING, because ``resync_only`` is a TRUTHFUL answer. A mutable
534
+ address with no changed-since contract has no bounded continuation, and saying so is what the
535
+ document is for; refusing to register it would push an author toward declaring a window the
536
+ publisher does not honour, which is the one error that silently serves wrong data.
537
+
538
+ ⚠ AND IT IS ABOUT THE WHOLE TABLE, NOT THE SOURCE. Studio derives one action per source, and a
539
+ plan holding a single undeclared source refuses the ENTIRE table's refresh -- a twenty-source
540
+ recipe with nineteen truthful windows and one bare address refreshes nothing, ever. An author
541
+ who never learns that ships a table which looks finished and never moves again.
542
+ """
543
+
544
+ if not absent:
545
+ return []
546
+ named = ", ".join(repr(name) for name in absent[:NAMED_UNCONTINUED_SOURCES])
547
+ beyond = len(absent) - NAMED_UNCONTINUED_SOURCES
548
+ if beyond > 0:
549
+ named += f", and {beyond} more"
550
+ return [
551
+ f"{len(absent)} of {total} sources state no bounded continuation: {named}. Studio "
552
+ "classifies these as `resync_only` -- and marks every other source `resync_only` too, so "
553
+ "`mr-data recipe readiness` names them all. ONE of them refuses this whole table's "
554
+ "scheduled refresh -- the other sources' windows do not run without it. Declaring every "
555
+ "source is only the FIRST gate; the plan's SHAPE is the second, and Studio admits just "
556
+ "two families: every source a recorded stream, or at least one `window` carrying both "
557
+ "`request` endpoints with no collection and no recorded stream beside it. The second is "
558
+ "NECESSARY and not sufficient: once the plan holds more than one source, every window in "
559
+ "it must also partition on a column `table.columns` declares, and on the SAME one. So "
560
+ "`closed: true` alone does NOT make a table refresh. Where the source is genuinely "
561
+ "mutable with no changed-since contract, leaving it undeclared is the honest answer and "
562
+ "this table then refreshes only by an explicit resync; say which sources those are and "
563
+ "why"
564
+ ]
565
+
566
+
295
567
  def _source_byte_ceiling(source: Mapping[str, Any]) -> tuple[int, str]:
296
568
  """The fetch ceiling one declared source is held to, and the phrase that names it.
297
569
 
@@ -509,7 +781,20 @@ def register(
509
781
  # written, so they do not depend on the answer; putting them on the receipt rather than in
510
782
  # front of the registration is what makes them a warning rather than a gate.
511
783
  undescribed = columns_without_description(document.get("table"))
512
- warnings = source_byte_warnings(document.get("sources"))
784
+ uncontinued = sources_without_continuation(document.get("sources"))
785
+ declared_sources = document.get("sources")
786
+ warnings = source_byte_warnings(declared_sources)
787
+ warnings += continuation_warnings(
788
+ uncontinued, len(declared_sources) if isinstance(declared_sources, list) else 0
789
+ )
790
+ # THE SECOND GATE, in the same place as the first. A recipe that passed the first one can
791
+ # still hold a plan Studio refuses outright, and an author who repaired the first warning by
792
+ # declaring `closed` everywhere would otherwise be answered with silence.
793
+ unrefreshable = unrefreshable_plan_shape(
794
+ declared_sources, declared_columns=_declared_column_names(document.get("table"))
795
+ )
796
+ if unrefreshable is not None:
797
+ warnings.append(unrefreshable)
513
798
  warnings += column_description_warnings(undescribed)
514
799
  # ⚠ EVERYTHING THIS COMMAND CAN SETTLE ON ITS OWN IS SETTLED BEFORE A CREDENTIAL IS RESOLVED,
515
800
  # which is the rule every other lane here follows: a document naming no dataset, and a
@@ -573,6 +858,9 @@ def register(
573
858
  # count out of a paragraph and should not have to parse one to learn how bad it is.
574
859
  # Omitted at zero, which is this receipt's own rule for `warnings`: absent means none.
575
860
  **({"columns_without_description": len(undescribed)} if undescribed else {}),
861
+ # THE SAME FACT AS ITS WARNING, AS A NUMBER, for whatever reads this receipt as JSON.
862
+ # Omitted at zero, which is this receipt's own rule for `warnings`.
863
+ **({"sources_without_continuation": len(uncontinued)} if uncontinued else {}),
576
864
  # OMITTED WHEN THERE IS NOTHING TO SAY, rather than an empty list on every clean
577
865
  # registration. `Warnings: (none)` under every successful recipe is the kind of line a
578
866
  # reader learns to skip, and a reader who skips it will skip the one that matters.
@@ -819,6 +1107,7 @@ __all__ = [
819
1107
  "MAX_DOCUMENT_BYTES",
820
1108
  "MAX_READINESS_ITEMS",
821
1109
  "MAX_READINESS_PAGES",
1110
+ "NAMED_UNCONTINUED_SOURCES",
822
1111
  "NAMED_UNDESCRIBED_COLUMNS",
823
1112
  "NDJSON_SOURCE_BYTE_CEILING",
824
1113
  "READER_SOURCE_BYTE_CEILING",
@@ -829,11 +1118,13 @@ __all__ = [
829
1118
  "REQUIRED_DOCUMENT_MEMBERS",
830
1119
  "SHOW",
831
1120
  "SHOW_SCHEMA",
1121
+ "UNREFRESHABLE_PLAN_TAIL",
832
1122
  "bound_dataset",
833
1123
  "check_dataset_binding",
834
1124
  "check_document",
835
1125
  "column_description_warnings",
836
1126
  "columns_without_description",
1127
+ "continuation_warnings",
837
1128
  "declare_arguments",
838
1129
  "read_document",
839
1130
  "readiness",
@@ -842,4 +1133,6 @@ __all__ = [
842
1133
  "register",
843
1134
  "show",
844
1135
  "source_byte_warnings",
1136
+ "sources_without_continuation",
1137
+ "unrefreshable_plan_shape",
845
1138
  ]