mostlyright-data 0.29.3__tar.gz → 0.29.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/PKG-INFO +1 -1
  2. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/pyproject.toml +1 -1
  3. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +19 -4
  4. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/making-a-table-incremental.md +67 -0
  5. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/readers.md +46 -1
  6. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/sources.md +24 -0
  7. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/canonical.py +23 -3
  8. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/table_manifest.py +1 -1
  9. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/recipe.py +15 -6
  10. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/recipe_explain.py +58 -3
  11. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/session.py +25 -2
  12. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/studio_capabilities.py +11 -0
  13. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4.py +2 -2
  14. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_datasets.py +32 -55
  15. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_public.py +19 -0
  16. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/credential_store.py +7 -1
  17. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/.gitignore +0 -0
  18. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/README.md +0 -0
  19. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/scripts/hatch_build.py +0 -0
  20. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/SKILL.md +0 -0
  21. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/agents/openai.yaml +0 -0
  22. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/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.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  24. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  25. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  26. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  27. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  28. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  29. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  30. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/agent-protocol.md +0 -0
  31. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  32. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  33. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/boundaries.md +0 -0
  34. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  35. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/commands.md +0 -0
  36. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  37. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/installation-parity.md +0 -0
  38. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/live-run.md +0 -0
  39. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  40. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  41. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/one-install.md +0 -0
  42. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/prediction-labels.md +0 -0
  43. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  44. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/promote.md +0 -0
  45. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/receipts.md +0 -0
  46. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  47. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  48. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/reference-pages.md +0 -0
  49. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/required-protocol.md +0 -0
  50. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/source-credentials.md +0 -0
  51. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  52. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/transforms.md +0 -0
  53. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  54. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  55. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  56. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/__init__.py +0 -0
  57. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  58. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/formats.py +0 -0
  59. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  60. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/key_seam.py +0 -0
  61. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/page_coverage.py +0 -0
  62. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  63. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/session_probes.py +0 -0
  64. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/skill_assets.py +0 -0
  65. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  66. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  67. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  68. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/activity.py +0 -0
  69. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  70. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/categories.py +0 -0
  71. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/commands.py +0 -0
  72. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  73. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/download.py +0 -0
  74. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  75. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  76. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/parity.py +0 -0
  77. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/probe.py +0 -0
  78. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  79. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  80. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/propose.py +0 -0
  81. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  82. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  83. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  84. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/research.py +0 -0
  85. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/router.py +0 -0
  86. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  87. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/runs.py +0 -0
  88. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/stream.py +0 -0
  89. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  90. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/transport.py +0 -0
  91. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  92. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  93. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  94. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  95. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  96. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  97. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  98. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  99. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  100. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  101. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  102. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  103. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  104. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  105. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  106. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  107. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  108. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  109. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  110. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  111. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  112. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  113. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  114. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  115. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  116. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  117. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.29.3 → mostlyright_data-0.29.5}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.29.3
3
+ Version: 0.29.5
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.29.3"
3
+ version = "0.29.5"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -108,10 +108,25 @@ not an answer yet, so read it back again later. A status of `truncated` is an an
108
108
  a limit: keep its limits and its evidence with whatever you record, and never call it complete. The
109
109
  command takes no key and no Reader options, so a source that needs either, or that is not a CSV, is
110
110
  read by the run below instead. So is any source that answers `capability_unavailable`, which means
111
- this deployment cannot inspect it: do not retry it, and do not guess in its place. What the look did
112
- observe belongs in that feed's `source` block, in `coverage`, as one plain line: what the sample
113
- showed (the format and the columns) and how much of the file it covered (the rows and bytes read,
114
- and whether it was cut short).
111
+ this deployment cannot inspect it: do not retry it, and do not guess in its place.
112
+
113
+ **Write down what each look found.** An inspection is not a run, and nothing on the page shows what
114
+ it found unless you write it down. Once a look has shown you the start of a file, write one
115
+ `mr-data dataset note DATASET_ID` for that source. The heading names the source and the outcome;
116
+ writing the same heading again revises that cell instead of adding one, so two sources need two
117
+ headings. The body says what the sample showed (the format and the columns) and how much of the file
118
+ it covered (the rows and bytes read, and whether it was cut short). Then count the source in
119
+ `--fact sources_probed` the next time you set the activity line.
120
+
121
+ ```sh
122
+ mr-data dataset note DATASET_ID \
123
+ --heading "Gauge file A is a CSV with four columns, cut short at 1,000 rows" \
124
+ --json <<'EOF'
125
+ The look read the first 1,000 rows (41,208 bytes) of the file and stopped at the row limit, so it
126
+ describes the start of the file and not the whole file. It is a CSV with four columns: station_id,
127
+ observed_at, stage_m and quality_flag.
128
+ EOF
129
+ ```
115
130
 
116
131
  **Both obey the ordinary gate.** Registering needs the brief answered or delegated, so the run
117
132
  happens AFTER stage 2's answers arrive — or at once under a delegation — and an inspection, which
@@ -560,3 +560,70 @@ again. Ask the user before passing either flag. Never pass one to clear a refusa
560
560
  | calls on the first full after the change | every hit whose answer is not held | each hit that passes the filter, once, because that full reads fresh |
561
561
  | calls on each weekly rebuild | every hit whose answer is not held | only new hits that pass the filter; the held answers are carried forward |
562
562
  | `pages.max_fetches_per_run` | 36,874 | the forecast's suggestion after the first filtered run |
563
+
564
+ #### Keyless API windows (Studio 4.26)
565
+
566
+ Check `studio_capabilities.keyless_window` is literal `true` before using this grammar.
567
+ An absent or false capability means "not available on this Studio yet". Registration and
568
+ file explanation refuse with `THIN_GRAMMAR_NOT_SERVED` before local lint or registration;
569
+ an older API's schema 422 on the explanation also refuses. No dataset delegation is written.
570
+ The capability covers `public.https.api@1.0.0`, `generic_api.auth.kind: none`, endpoint
571
+ `epoch_millis`, the `epoch_millis_value` partition key, and the two request controls below. Existing credential-free file and collection
572
+ adapters do not require this capability.
573
+
574
+ For a public JSON/CSV API use `credential_mode: none`, an HTTPS `origin`, and `generic_api`
575
+ with exact `auth: {"kind":"none"}`. Requests are GET-only, with no body or credential reference.
576
+ Authenticated adapters retain their existing credential requirements. Declare both window
577
+ endpoints with `location: parameter` and corresponding required request parameters. Origin,
578
+ host allowlists and cursor next-URL restrictions still apply.
579
+
580
+ For Binance BTCUSDT one-minute klines, start the backfill at `2017-08-17T00:00:00Z` and request
581
+ `/api/v3/klines` with `symbol=BTCUSDT`, `interval=1m`, `limit=1000`, and parameter templates for
582
+ `startTime` and `endTime`. Use this request window:
583
+
584
+ ```json
585
+ {
586
+ "request": {
587
+ "start": {"location": "parameter", "encoding": "epoch_millis", "bound": "inclusive", "parameter": "startTime"},
588
+ "end": {"location": "parameter", "encoding": "epoch_millis", "bound": "inclusive", "parameter": "endTime"},
589
+ "max_slice_seconds": 43200,
590
+ "min_interval_milliseconds": 1000
591
+ }
592
+ }
593
+ ```
594
+
595
+ Keep `granularity: day`, `timezone: UTC` and `merge.materialization: partition_replace`.
596
+ For the acquired `open_time_ms` column declare
597
+ `merge.partition: {"column":"open_time_ms","key":"epoch_millis_value"}` and
598
+ `merge.row_identity: ["open_time_ms"]`. This encoding maps the integer millisecond cell to
599
+ its UTC calendar day before transforms; it floors negative epochs correctly too.
600
+ The 43,200-second maximum cuts each UTC day into two 12-hour requests, each with at most 720
601
+ one-minute bars under the publisher's 1,000-row cap. Slices are requests, never new partitions.
602
+ An inclusive millisecond end renders the next slice boundary minus 1 ms; adjacent slices do not
603
+ overlap. An exclusive start renders the start minus 1 ms. The older date/epoch-second encodings
604
+ retain their day-step conversions. Slice bounds require both endpoints to use milliseconds.
605
+ `max_slice_seconds` is 1–86,400; `window.request.min_interval_milliseconds` is 1–3,600,000 and is supported on
606
+ the new public generic adapter, defaulting to 1,000 when omitted. Pacing applies across the
607
+ entire walk, including pagination, rather than resetting at each day.
608
+
609
+ After the full backfill, choose an hourly cadence in the agreed brief. Refresh re-reads the
610
+ current UTC day, flooring a catch-up start to midnight when prior coverage reached the next
611
+ midnight; it replaces that day's partition. Do not set `complete_period_only` when the open day
612
+ is needed. Size `limits.max_requests` for every slice and every pagination page across the full
613
+ walk; the global budget must allow it to finish. The 4,096-partition ceiling still counts days.
614
+ Python and Keel plan the same windows; large declared relations use the admitted Keel route,
615
+ with each day still subject to its in-memory ceiling. A declaration is not evidence of a
616
+ completed large ingestion: verify actual coverage from the run.
617
+
618
+ The [Reader example](readers.md#needs-studio-426) maps Binance's root array of 12-column arrays.
619
+ An explicit `table.partition` may use `{"column":"open_time_ms","encoding":"epoch_millis_value"}`;
620
+ that table encoding also requires `keyless_window`. The native refresh subset preserves this
621
+ partition column with exact `CAST(open_time_ms AS BIGINT) AS open_time_ms`; its current subset
622
+ requires `checks: []`. Verify the built values and coverage with bounded queries and receipts.
623
+ Bind the real dataset ID before Harness registration, following the brief workflow; the fixture
624
+ omits an ID rather than inventing an existing dataset.
625
+ The raw millisecond timestamp uses `epoch_millis_value`, never `iso_date_prefix`;
626
+ a transform does not run before source partition merging. Existing JSON cursor pagination is reused unchanged. XML readers
627
+ do not add extraction of arXiv OAI-PMH XML continuation tokens to JSON-pointer pagination.
628
+ Wikipedia's existing `public.https` date-in-path source remains a literal full fetch; this
629
+ example does not prove an entire historical date-path backfill.
@@ -119,7 +119,7 @@ decoder. Zero matches and multiple matches both refuse.
119
119
  | `raster.geospatial@1.0.0` | `format`, exact native `crs`, and an `x`/`y`/`width`/`height` window. |
120
120
  | `satellite.sentinel_safe@1.0.0` | SAFE ZIP with exact spectral/quality member names, native CRS, quality layer, and an `x`/`y`/`width`/`height` window. |
121
121
  | `image.rendered@1.0.0` | `format` plus a bounded `x`/`y`/`width`/`height` rendered-image crop. |
122
- | `scientific.fits@1.0.0` | `band` plus an exact native `x`/`y`/`width`/`height` image window. |
122
+ | `scientific.fits@1.0.0` | `band_id: "195"` plus an exact native `x`/`y`/`width`/`height` image window. |
123
123
  | `satellite.himawari_hsd@1.0.0` | Certified HSD v1.3 single-segment B13 plus an exact native `x`/`y`/`width`/`height` radiance window. |
124
124
  | `satellite.himawari_hsd_complete@1.0.0` | Complete HSD v1.3 scan ZIP: `band_id`, exact ordered `segment_members`, and a native `x`/`y`/`width`/`height` radiance window. |
125
125
  | `satellite.xrit_image@1.0.0` | Exact ZIP prologue, eight ordered HRIT/LRIT image segments and epilogue; sealed channel/grid/navigation plus a bounded raw-count window. |
@@ -140,3 +140,48 @@ settings. Spreadsheet receipts also identify stored calculated values.
140
140
  Pinned HTTPS Reader sources retrieve, decode, and parse in one attested Clean room call. JSON,
141
141
  NDJSON, ZIP, TAR, GZIP, XLSX, delimited text, and single-record GRIB2 are supported when the
142
142
  response matches the family's exact media-type, settings, and budget contract.
143
+
144
+ ### Needs Studio 4.26
145
+
146
+ Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
147
+ Use `public.https.api@1.0.0` only when `keyless_window` is literal `true`; otherwise report
148
+ "not available on this Studio yet". Registration and file explanation refuse with
149
+ `THIN_GRAMMAR_NOT_SERVED`, including an older API's schema 422 before capabilities.
150
+
151
+ The new adapter uses the existing `generic_api.response` Reader contract. For Binance klines,
152
+ state `format: json`, `records_pointer: ""`, and `reader.family_id: json.tabular` with
153
+ `family_version: 1.0.0` (or the already admitted `1.1.0`). Pin `options_digest` to the SHA-256
154
+ of the canonical decode options, rather than inserting a placeholder digest. These decode
155
+ options project a root JSON array of arrays, with RFC 6901 numeric indexes into each row:
156
+
157
+ ```json
158
+ {
159
+ "document_format": "json",
160
+ "records_pointer": "",
161
+ "expand": [],
162
+ "columns": [
163
+ {"name": "open_time_ms", "pointer": "/0"},
164
+ {"name": "open", "pointer": "/1"},
165
+ {"name": "high", "pointer": "/2"},
166
+ {"name": "low", "pointer": "/3"},
167
+ {"name": "close", "pointer": "/4"},
168
+ {"name": "volume", "pointer": "/5"},
169
+ {"name": "close_time_ms", "pointer": "/6"},
170
+ {"name": "quote_volume", "pointer": "/7"},
171
+ {"name": "trades", "pointer": "/8"},
172
+ {"name": "taker_buy_base_volume", "pointer": "/9"},
173
+ {"name": "taker_buy_quote_volume", "pointer": "/10"},
174
+ {"name": "unused", "pointer": "/11"}
175
+ ]
176
+ }
177
+ ```
178
+
179
+ Every column is required by default. The Reader projects values; it does not turn an epoch
180
+ millisecond cell into an ISO date. Declare numeric/decimal types in the table transform and
181
+ declare `window.merge.partition: {"column":"open_time_ms","key":"epoch_millis_value"}`.
182
+ The key maps the acquired integer millisecond value to its UTC day before transforms;
183
+ `merge.row_identity` is `["open_time_ms"]`. It also needs `keyless_window`. See
184
+ [the cap-safe window](making-a-table-incremental.md#keyless-api-windows-studio-426) for the
185
+ backfill, hourly cadence, shared Python/Keel windows and request limits. Cursor pagination
186
+ continues to use its existing JSON next-token pointer; XML OAI-PMH token parsing is not supplied
187
+ by this mapping.
@@ -271,3 +271,27 @@ its upstream cannot refresh incrementally. A `closed` or `reference` source may
271
271
  table, and a refresh restores it from the predecessor (ADR 0061). Do not split a table
272
272
  to separate them. No per-row collection may derive its rows from a `closed` or `reference` source.
273
273
  [Which plans refresh](making-a-table-incremental.md#which-plans-refresh) has the rest.
274
+
275
+ ### Needs Studio 4.26
276
+
277
+ Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
278
+ Use the following only when `keyless_window` is literal `true`; otherwise report "not available
279
+ on this Studio yet". The client refuses with `THIN_GRAMMAR_NOT_SERVED` before registration,
280
+ including when an older API rejects the explanation document with schema 422.
281
+
282
+ `public.https.api@1.0.0` is the credential-free generic HTTPS API adapter. State
283
+ `credential_mode: none`, `origin`, and `generic_api` with exact `auth: {"kind":"none"}`.
284
+ Use GET only: no request body, credential reference, token, secret header or secret query value.
285
+ This adapter does not relax authentication on the keyed adapters. The owned HTTPS origin,
286
+ host allowlist, bounded request budgets and cursor next-URL restrictions remain in force.
287
+ The existing generic response mapping and JSON/CSV pagination apply; a cursor needs the
288
+ publisher's actual next-token pointer. JSON-pointer pagination does not extract XML OAI-PMH tokens.
289
+
290
+ For Binance klines use parameter-windowed `startTime`/`endTime`, millisecond endpoints and
291
+ 12-hour slices at one request per second. Follow the backfill and hourly-refresh example in
292
+ [making a table incremental](making-a-table-incremental.md#keyless-api-windows-studio-426)
293
+ and the root-array projection in [Readers](readers.md#needs-studio-426). Keep the day partition
294
+ with `epoch_millis_value` on the raw millisecond time column, and size the request ceiling
295
+ for all slices and pages. Do not substitute a whole-day query:
296
+ 1,440 one-minute bars exceed Binance's 1,000-row response cap. Existing credential-free
297
+ `public.https` file fetches and `public.https.collection@2.0.0` keep their own contracts.
@@ -14,6 +14,7 @@ from typing import Any, NoReturn
14
14
 
15
15
  MAX_CANONICAL_DEPTH = 64
16
16
  MAX_CANONICAL_MEMBERS = 100_000
17
+ _MAX_INTEGER_LITERAL_LENGTH = len(str(-(2**63)))
17
18
  MAX_CANONICAL_STRING_LENGTH = 1_000_000
18
19
  MAX_CANONICAL_INTEGER = (1 << 63) - 1
19
20
 
@@ -35,7 +36,10 @@ def parse_json(value: str | bytes | bytearray | memoryview) -> Any:
35
36
 
36
37
 
37
38
  def parse_foreign_json(
38
- value: str | bytes | bytearray | memoryview, *, max_members: int = MAX_CANONICAL_MEMBERS
39
+ value: str | bytes | bytearray | memoryview,
40
+ *,
41
+ max_members: int = MAX_CANONICAL_MEMBERS,
42
+ wide_integers: bool = False,
39
43
  ) -> Any:
40
44
  """Parse a third-party document with the same hardening, but accept fractional numbers.
41
45
 
@@ -59,9 +63,14 @@ def parse_foreign_json(
59
63
 
60
64
  ``max_members`` raises the member ceiling for one document whose size a route already bounds,
61
65
  such as a table manifest listing thousands of parts.
66
+
67
+ ``wide_integers`` reads an integer literal outside the signed 64-bit range as a finite float
68
+ rather than refusing the document. It is for an answer that carries a table's own values from
69
+ a publisher that writes a whole-valued double without a fraction or an exponent, so ``1e20``
70
+ arrives as ``100000000000000000000``. An integer inside the range is still an ``int``.
62
71
  """
63
72
 
64
- return _parse(value, fractions="accept", max_members=max_members)
73
+ return _parse(value, fractions="accept", max_members=max_members, wide_integers=wide_integers)
65
74
 
66
75
 
67
76
  def _parse(
@@ -69,6 +78,7 @@ def _parse(
69
78
  *,
70
79
  fractions: str,
71
80
  max_members: int = MAX_CANONICAL_MEMBERS,
81
+ wide_integers: bool = False,
72
82
  ) -> Any:
73
83
  if isinstance(value, str):
74
84
  text = value
@@ -98,7 +108,7 @@ def _parse(
98
108
  parsed = json.loads(
99
109
  text,
100
110
  object_pairs_hook=_reject_duplicate_pairs,
101
- parse_int=_parse_integer,
111
+ parse_int=_parse_wide_integer if wide_integers else _parse_integer,
102
112
  parse_float=_parse_fraction if accept_fractions else _reject_fraction,
103
113
  parse_constant=_reject_constant,
104
114
  )
@@ -318,6 +328,16 @@ def _parse_integer(value: str) -> int:
318
328
  return parsed
319
329
 
320
330
 
331
+ def _parse_wide_integer(value: str) -> int | float:
332
+ """Read an integer literal, as a float when it is outside the signed 64-bit range."""
333
+
334
+ if len(value) <= _MAX_INTEGER_LITERAL_LENGTH:
335
+ parsed = int(value)
336
+ if -MAX_CANONICAL_INTEGER - 1 <= parsed <= MAX_CANONICAL_INTEGER:
337
+ return parsed
338
+ return _parse_fraction(value)
339
+
340
+
321
341
  def _parse_fraction(value: str) -> float:
322
342
  """Read a fractional literal from a foreign document, refusing a non-finite one."""
323
343
 
@@ -101,7 +101,7 @@ _SUPPORTED_MANIFEST_SCHEMAS = frozenset((TABLE_MANIFEST_SCHEMA, TABLE_MANIFEST_S
101
101
  #: ``iso_month_prefix`` tables read as TABLE_MANIFEST_INVALID on 0.28.0 (planning#807).
102
102
  #: ``tests/test_thin_table_manifest_encodings.py`` holds this equal to the vendored recipe schema.
103
103
  MANIFEST_PARTITION_ENCODINGS: frozenset[str] = frozenset(
104
- {"value", "iso_date_prefix", "iso_date_value", "iso_month_prefix"}
104
+ {"value", "iso_date_prefix", "iso_date_value", "iso_month_prefix", "epoch_millis_value"}
105
105
  )
106
106
 
107
107
  #: The two media types the format adds to the closed artifact vocabulary.
@@ -1181,13 +1181,21 @@ def register(
1181
1181
  ) -> dict[str, Any]:
1182
1182
  """``mr-data recipe FILE``: register one document and print what the server derived from it.
1183
1183
 
1184
- The order is the order the answers cost what they cost. The document is read and linted with
1185
- no credential resolved and nothing sent, so an author whose checks are malformed learns it
1186
- before a token is minted. The brief gate comes next, because it is one GET and it decides
1187
- whether this registration should happen at all. The registration itself is last.
1184
+ Existing documents are linted before a credential is resolved. A keyless-window document
1185
+ asks what Studio serves before local lint, because an older schema may reject the grammar
1186
+ before reporting capabilities. The brief and delegation gates follow; registration is last.
1188
1187
  """
1189
1188
 
1190
1189
  document = read_document(Path(args.target))
1190
+ # New grammar must be refused as unserved before local lint can diagnose its shape.
1191
+ keyless = any(
1192
+ capability == "keyless_window" for _, capability in recipe_explain.gated_grammar(document)
1193
+ )
1194
+ selected = client
1195
+ served = []
1196
+ if keyless:
1197
+ selected = selected or _client(args)
1198
+ served = recipe_explain.registration_gate(selected, document)
1191
1199
  check_document(document)
1192
1200
  if not getattr(args, "no_lint", False):
1193
1201
  refusal = lint_refusal(
@@ -1239,13 +1247,14 @@ def register(
1239
1247
  )
1240
1248
  delegated = getattr(args, "delegated", None)
1241
1249
  blocks = delegation_blocks(delegated) if delegated is not None else None
1242
- selected = client or _client(args)
1250
+ selected = selected or _client(args)
1243
1251
  # ⚠ THE CAPABILITY GATE COMES BEFORE ANYTHING IS WRITTEN. A document using grammar this
1244
1252
  # Studio does not serve (`row_filter`, a per-row `interval` revisit) is refused here, before
1245
1253
  # the delegation append below and before the registration, so a refusal leaves nothing
1246
1254
  # behind. It asks Studio only when the document uses such grammar or a snapshot window, so
1247
1255
  # every other registration makes exactly the calls it made before.
1248
- served = recipe_explain.registration_gate(selected, document)
1256
+ if not keyless:
1257
+ served = recipe_explain.registration_gate(selected, document)
1249
1258
  warnings += served
1250
1259
  warnings += sampled
1251
1260
  warning_codes = [recipe_explain.SNAPSHOT_WILL_NOT_REFRESH_YET] if served else []
@@ -125,6 +125,7 @@ CAPABILITY_NAMES: tuple[str, ...] = (
125
125
  "fresh_read_control",
126
126
  "snapshot_refresh",
127
127
  "row_request_batch",
128
+ "keyless_window",
128
129
  )
129
130
 
130
131
  #: What a Studio that answers 404 or 405 from the explanation routes serves (README D27).
@@ -1246,6 +1247,17 @@ def explain_document_at_studio(
1246
1247
  except ThinLaneError as refusal:
1247
1248
  if route_unavailable(refusal):
1248
1249
  return None
1250
+ keyless = [pointer for pointer, capability in gated_grammar(document)
1251
+ if capability == "keyless_window"]
1252
+ if keyless and getattr(refusal, "http_status", None) == 422:
1253
+ # Older API schemas reject the document before they can answer capabilities.
1254
+ # A schema rejection is never evidence that this grammar is served.
1255
+ raise ThinLaneError(
1256
+ GRAMMAR_NOT_SERVED_CODE,
1257
+ f"not available on this Studio yet: {', '.join(keyless)} "
1258
+ "(Studio rejected the explanation document before reporting capabilities). "
1259
+ "Nothing was registered. Wait for the Studio deploy, or remove the member",
1260
+ ) from refusal
1249
1261
  raise
1250
1262
  return _explanation(answer, EXPLAIN_REFRESH_PATH)
1251
1263
 
@@ -1286,14 +1298,42 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
1286
1298
 
1287
1299
  ``row_filter`` on any collection discovery, and a per-row request collection whose
1288
1300
  ``pages.revisit`` states ``interval`` (4.14.0). A revisit on any other collection is old
1289
- grammar. ``batch`` on a per-row request (4.15.0).
1301
+ grammar. ``batch`` on a per-row request (4.15.0). The public generic API, no-auth kind,
1302
+ millisecond endpoints and slice/pacing members (4.26.0) each need ``keyless_window``.
1290
1303
  """
1291
1304
 
1292
1305
  gated: list[tuple[str, str]] = []
1306
+ table = document.get("table")
1307
+ partition = table.get("partition") if isinstance(table, Mapping) else None
1308
+ if isinstance(partition, Mapping) and partition.get("encoding") == "epoch_millis_value":
1309
+ gated.append(("/table/partition/encoding", "keyless_window"))
1293
1310
  sources = document.get("sources")
1294
1311
  for index, source in enumerate(sources if isinstance(sources, list) else ()):
1295
1312
  if not isinstance(source, Mapping):
1296
1313
  continue
1314
+ prefix = f"/sources/{index}"
1315
+ connector = source.get("connector")
1316
+ if isinstance(connector, Mapping):
1317
+ if connector.get("adapter_id") == "public.https.api@1.0.0":
1318
+ gated.append((f"{prefix}/connector/adapter_id", "keyless_window"))
1319
+ generic = connector.get("generic_api")
1320
+ auth = generic.get("auth") if isinstance(generic, Mapping) else None
1321
+ if isinstance(auth, Mapping) and auth.get("kind") == "none":
1322
+ gated.append((f"{prefix}/connector/generic_api/auth/kind", "keyless_window"))
1323
+ window = source.get("window")
1324
+ merge = window.get("merge") if isinstance(window, Mapping) else None
1325
+ partition = merge.get("partition") if isinstance(merge, Mapping) else None
1326
+ if isinstance(partition, Mapping) and partition.get("key") == "epoch_millis_value":
1327
+ gated.append((f"{prefix}/window/merge/partition/key", "keyless_window"))
1328
+ window_request = window.get("request") if isinstance(window, Mapping) else None
1329
+ if isinstance(window_request, Mapping):
1330
+ for endpoint in ("start", "end"):
1331
+ value = window_request.get(endpoint)
1332
+ if isinstance(value, Mapping) and value.get("encoding") == "epoch_millis":
1333
+ gated.append((f"{prefix}/window/request/{endpoint}/encoding", "keyless_window"))
1334
+ for member in ("max_slice_seconds", "min_interval_milliseconds"):
1335
+ if member in window_request:
1336
+ gated.append((f"{prefix}/window/request/{member}", "keyless_window"))
1297
1337
  collection = source.get("collection")
1298
1338
  if not isinstance(collection, Mapping):
1299
1339
  continue
@@ -1348,6 +1388,16 @@ def registration_gate(client: StudioV4Client, document: Mapping[str, Any]) -> li
1348
1388
  if not gated and not snapshots:
1349
1389
  return []
1350
1390
  answer = explain_document_at_studio(client, document)
1391
+ return grammar_gate(document, answer)
1392
+
1393
+
1394
+ def grammar_gate(
1395
+ document: Mapping[str, Any], answer: Mapping[str, Any] | None
1396
+ ) -> list[str]:
1397
+ """Admit only grammar explicitly served by an explanation; perform no writes."""
1398
+
1399
+ gated = gated_grammar(document)
1400
+ snapshots = snapshot_sources(document)
1351
1401
  capabilities = served_capabilities(answer)
1352
1402
  missing = [pointer for pointer, capability in gated if not capabilities[capability]]
1353
1403
  if missing:
@@ -1455,10 +1505,15 @@ def explain(args: argparse.Namespace, *, client: StudioV4Client | None = None) -
1455
1505
  answer = explain_table_at_studio(selected, registered["table_id"])
1456
1506
  else:
1457
1507
  document = recipe_module.read_document(Path(target))
1508
+ selected = client or recipe_module._client(args)
1509
+ keyless = any(capability == "keyless_window" for _, capability in gated_grammar(document))
1510
+ if keyless:
1511
+ answer = explain_document_at_studio(selected, document)
1512
+ grammar_gate(document, answer)
1458
1513
  recipe_module.check_document(document)
1459
1514
  subject = {"kind": "document", "table_id": None, "recipe_id": None, "recipe_digest": None}
1460
- selected = client or recipe_module._client(args)
1461
- answer = explain_document_at_studio(selected, document)
1515
+ if not keyless:
1516
+ answer = explain_document_at_studio(selected, document)
1462
1517
  capabilities = served_capabilities(answer)
1463
1518
  # The mirror is judged under the fence Studio states, so the two can only disagree about
1464
1519
  # the document; with no Studio to ask the fence reads closed, as a pre-4.14.0 Studio runs.
@@ -26,7 +26,12 @@ from typing import Any
26
26
  from urllib.parse import quote, urlencode
27
27
  from uuid import UUID
28
28
 
29
- from mostlyright.data_harness.canonical import CanonicalJSONError, parse_json
29
+ from mostlyright.data_harness.canonical import (
30
+ MAX_CANONICAL_MEMBERS,
31
+ CanonicalJSONError,
32
+ parse_foreign_json,
33
+ parse_json,
34
+ )
30
35
  from mostlyright.data_harness.thin.transport import (
31
36
  MAX_JSON_RESPONSE_BYTES,
32
37
  ThinLaneError,
@@ -70,6 +75,9 @@ CLOUD_DATASETS_LIST_PATH = "/api/cli/datasets"
70
75
  # Projection repair after a Studio metadata write, without a publication mutation.
71
76
  CLOUD_DATASET_IDENTITY_PATH = "/api/cli/datasets/{dataset_id}/identity"
72
77
 
78
+ # Shared UI/CLI archive orchestration, including public projection withdrawal.
79
+ CLOUD_DATASET_ARCHIVE_PATH = "/api/cli/datasets/{dataset_id}/archive"
80
+
73
81
  #: The one Cloud address a Dataset has. ``mostlyright-cloud`` serves it at this route with
74
82
  #: section suffixes, and a run has no page of its own: its evidence lives on this page's Harness
75
83
  #: card, so a receipt names its run with :func:`run_dashboard_url` as a query member here rather
@@ -620,6 +628,8 @@ class CloudClient:
620
628
  expected: tuple[int, ...] = (200,),
621
629
  absent: str | None = None,
622
630
  maximum: int = MAX_TOKEN_RESPONSE_BYTES,
631
+ table_values: bool = False,
632
+ max_members: int = MAX_CANONICAL_MEMBERS,
623
633
  ) -> dict[str, Any]:
624
634
  """Make one Cloud call and return the object it answered with, or raise the refusal.
625
635
 
@@ -633,6 +643,14 @@ class CloudClient:
633
643
 
634
644
  ``absent`` is what this route's own 404 means, in the caller's words; see
635
645
  :func:`cloud_refusal` for why one sentence could not serve both routes.
646
+
647
+ ``table_values`` is for a route whose answer carries a table's own values. It admits a
648
+ finite fractional number, because a DOUBLE column or an average is not an integer, and it
649
+ reads an integer literal outside the signed 64-bit range as a float, because Cloud writes
650
+ a whole-valued double without a fraction. ``max_members`` is that answer's member ceiling,
651
+ for a route that states how large a page it may be handed. Every other hardening is
652
+ unchanged, and the default refuses all of it as it does for every answer this lane reads
653
+ as a document of its own.
636
654
  """
637
655
 
638
656
  headers = {
@@ -651,7 +669,11 @@ class CloudClient:
651
669
  maximum,
652
670
  )
653
671
  try:
654
- parsed = parse_json(raw)
672
+ parsed = (
673
+ parse_foreign_json(raw, max_members=max_members, wide_integers=True)
674
+ if table_values
675
+ else parse_json(raw)
676
+ )
655
677
  except CanonicalJSONError:
656
678
  parsed = None
657
679
  if status not in expected:
@@ -668,6 +690,7 @@ __all__ = [
668
690
  "CLOUD_APPROVAL_DASHBOARD_PATH",
669
691
  "CLOUD_DATASETS_LIST_PATH",
670
692
  "CLOUD_DATASET_ACCESS_PATH",
693
+ "CLOUD_DATASET_ARCHIVE_PATH",
671
694
  "CLOUD_DATASET_DASHBOARD_PATH",
672
695
  "CLOUD_DATASET_RESOLVED_SCHEMA",
673
696
  "CLOUD_DATASET_RESOLVE_PATH",
@@ -36,12 +36,14 @@ FRESH_READ_CONTROL = "fresh_read_control"
36
36
  SNAPSHOT_REFRESH = "snapshot_refresh"
37
37
  #: Served by every 4.15.0 API, which admits ``batch`` on a per-row request. An older API omits it.
38
38
  ROW_REQUEST_BATCH = "row_request_batch"
39
+ KEYLESS_WINDOW = "keyless_window"
39
40
  CAPABILITY_NAMES: tuple[str, ...] = (
40
41
  ROW_FILTER,
41
42
  PER_ROW_REVISIT_INTERVAL,
42
43
  FRESH_READ_CONTROL,
43
44
  SNAPSHOT_REFRESH,
44
45
  ROW_REQUEST_BATCH,
46
+ KEYLESS_WINDOW,
45
47
  )
46
48
 
47
49
  #: What a Studio that serves neither explanation route serves.
@@ -96,6 +98,14 @@ def document_capabilities(client: StudioV4Client, document: Mapping[str, Any]) -
96
98
  read. Studio judges the document alone and writes nothing.
97
99
  """
98
100
 
101
+ from mostlyright.data_harness.thin.recipe_explain import (
102
+ explain_document_at_studio,
103
+ gated_grammar,
104
+ )
105
+
106
+ if any(capability == KEYLESS_WINDOW for _, capability in gated_grammar(document)):
107
+ # Share the older-schema 422 translation with registration and file explanation.
108
+ return served_capabilities(explain_document_at_studio(client, document))
99
109
  try:
100
110
  answer = client.explain_recipe_refresh(document)
101
111
  except ThinLaneError as refusal:
@@ -109,6 +119,7 @@ __all__ = [
109
119
  "CAPABILITY_NAMES",
110
120
  "FRESH_READ_CONTROL",
111
121
  "GRAMMAR_NOT_SERVED_CODE",
122
+ "KEYLESS_WINDOW",
112
123
  "NO_CAPABILITIES",
113
124
  "PER_ROW_REVISIT_INTERVAL",
114
125
  "ROW_FILTER",
@@ -147,8 +147,8 @@ from mostlyright.data_harness.thin.transport import ThinLaneError
147
147
  #: ``JOB_INVALID`` outright, which in production was every refresh a collection epoch was offered
148
148
  #: for rather than only the collection runs. This package must not reach a Studio older than the
149
149
  #: commit it pins; ``docs/V4-WORKER-PROTOCOL.md`` states it beside the layout's own ordering rule.
150
- PINNED_V4_OPENAPI_SOURCE_SHA256 = "80ff61057468d16bd97a071ba8f0acfff0050deb834ca979a9489bfc5ef1da13"
151
- PINNED_V4_CONTRACT_VERSION = "4.19.0"
150
+ PINNED_V4_OPENAPI_SOURCE_SHA256 = "1f30e109b81f6aa55c00c8999fd877b6678b7e0bea617707d26e7a040380f98b"
151
+ PINNED_V4_CONTRACT_VERSION = "4.27.0"
152
152
 
153
153
  #: The ``schema_version`` every command body that predates 4.14.0 stamps: ``dataset activity``,
154
154
  #: ``table rename``, ``table archive`` and ``dataset archive``. It is the number the 0.25 line
@@ -127,6 +127,7 @@ from mostlyright.data_harness.thin.narrative import (
127
127
  from mostlyright.data_harness.thin.runs import new_idempotency_key
128
128
  from mostlyright.data_harness.thin.session import (
129
129
  CLOUD_DATASET_ACCESS_PATH,
130
+ CLOUD_DATASET_ARCHIVE_PATH,
130
131
  CLOUD_DATASET_IDENTITY_PATH,
131
132
  CLOUD_DATASET_RESOLVE_PATH,
132
133
  CLOUD_DATASET_RESOLVED_SCHEMA,
@@ -149,7 +150,6 @@ from mostlyright.data_harness.thin.stream import RunEventStream
149
150
  from mostlyright.data_harness.thin.transport import ThinLaneError, Transport, UrlLibTransport
150
151
  from mostlyright.data_harness.thin.v4 import (
151
152
  ARCHIVE_DATASET_PATH,
152
- COMMAND_REQUEST_VERSION_FLOOR,
153
153
  CREATE_DATASET_PATH,
154
154
  DATASET_CURRENT_STATE_PATH,
155
155
  DATASET_EVENT_HISTORY_PATH,
@@ -448,6 +448,14 @@ class CloudDatasetAccessClient(CloudClient):
448
448
  credential header and the refusal folding to drift.
449
449
  """
450
450
 
451
+ def archive_dataset(self, dataset_id: str, confirmation_name: str) -> dict[str, Any]:
452
+ return self.call(
453
+ "POST",
454
+ CLOUD_DATASET_ARCHIVE_PATH.format(dataset_id=dataset_id),
455
+ act="archive this dataset and withdraw its public listings",
456
+ body={"confirmation_name": confirmation_name},
457
+ )
458
+
451
459
  def sync_dataset_identity(self, dataset_id: str) -> dict[str, Any]:
452
460
  """Carry Studio's current metadata into Cloud's existing public projection."""
453
461
 
@@ -458,7 +466,6 @@ class CloudDatasetAccessClient(CloudClient):
458
466
  body={},
459
467
  )
460
468
 
461
-
462
469
  def list_datasets(self, *, limit: int | None = None) -> dict[str, Any]:
463
470
  """Name every dataset this workspace holds, most recently updated first.
464
471
 
@@ -1838,75 +1845,46 @@ def write(
1838
1845
 
1839
1846
 
1840
1847
  def archive(
1841
- args: argparse.Namespace, *, client: StudioV4DatasetClient | None = None
1848
+ args: argparse.Namespace, *, client: CloudDatasetAccessClient | None = None
1842
1849
  ) -> dict[str, Any]:
1843
- """``mr-data dataset archive DATASET_ID --confirm-name TITLE``: retire the page, free the name.
1844
-
1845
- ⚠ THE TYPED TITLE IS COMPARED HERE AS WELL AS BY STUDIO, WHICH IS THE OPPOSITE CHOICE FROM
1846
- ``mr-data table archive`` AND THE REASON IS THAT THE READ IS ALREADY HAPPENING. This write is
1847
- fenced on the dataset's version, so the page has to be read before it can be sent; the title
1848
- arrives in the same body, the comparison costs nothing, and the fence is what makes it safe --
1849
- a title that changed after the read moves the version, and the conditional refuses. Where a
1850
- read would have to be bought only to give a second opinion, this lane does not buy it.
1851
-
1852
- ⚠ AND IT IS NOT A DELETION. Runs, artifacts, versions and the decision record all stay. What
1853
- the command ends is the page's claim on its title and its place in the workspace's listing.
1854
- """
1855
-
1850
+ """Use the UI's Cloud archive ceremony; a Studio-only tombstone is incomplete."""
1856
1851
  dataset_id = identifier(getattr(args, "dataset_id", None) or "", "dataset identifier")
1857
1852
  confirmation = getattr(args, "confirm_name", None)
1858
1853
  if not isinstance(confirmation, str) or not confirmation.strip():
1859
1854
  raise ThinLaneError(
1860
1855
  "THIN_REQUEST_INVALID",
1861
1856
  "mr-data dataset archive needs the dataset's current title typed out: "
1862
- f"--confirm-name TITLE. mr-data dataset show {dataset_id} prints the title Studio "
1863
- "holds",
1857
+ "--confirm-name TITLE",
1864
1858
  )
1865
1859
  stated = confirmation.strip()
1866
- selected = client or _client(args)
1867
- record = selected.dataset(dataset_id)
1868
- version = _version(record)
1869
- held = record.get("name")
1870
- if isinstance(held, str) and held.strip() != stated:
1860
+ if client is None:
1861
+ credentials = resolve_cloud_credentials()
1862
+ require_device_key(credentials)
1863
+ selected = CloudDatasetAccessClient(credentials)
1864
+ elif isinstance(client, CloudDatasetAccessClient):
1865
+ selected = client
1866
+ else:
1871
1867
  raise ThinLaneError(
1872
- "THIN_CONFIRMATION_MISMATCH",
1873
- f"this dataset is called {held!r} and --confirm-name said {stated!r}. Archiving "
1874
- "releases the title, so the title is what confirms it: type the one on the page, or "
1875
- "check you have the dataset you meant",
1876
- )
1877
- try:
1878
- archived = selected.archive_dataset(
1879
- dataset_id,
1880
- body={
1881
- "schema_version": COMMAND_REQUEST_VERSION_FLOOR,
1882
- "workspace_id": str(selected.session.workspace_id),
1883
- "confirmation_name": stated,
1884
- },
1885
- expected_version=version,
1868
+ "THIN_REQUEST_INVALID", "dataset archive requires the Cloud archive client"
1886
1869
  )
1887
- except ThinLaneError as refusal:
1888
- if not stale_version(refusal):
1889
- raise
1890
- # ⚠ NAMED, AND NOT RETRIED, WHICH IS THE ONE PLACE THIS DIFFERS FROM `write` AND `rename`.
1891
- # Those two replay at the new version because what they write does not depend on what
1892
- # changed underneath them. This does: the title typed to confirm the archive is exactly
1893
- # what a racing `dataset set` may have just rewritten, so a silent retry could archive a
1894
- # page under a title that is no longer its own. What it gets instead is the same sentence
1895
- # the other two end on, because it is the same thing that happened.
1870
+ answer = selected.archive_dataset(dataset_id, stated)
1871
+ if (
1872
+ answer.get("schema_version") != "mostlyright-dataset-archive.v1"
1873
+ or answer.get("status") != "dataset_archived"
1874
+ or answer.get("dataset_id") != dataset_id
1875
+ or answer.get("name") != stated
1876
+ ):
1896
1877
  raise ThinLaneError(
1897
- "THIN_DATASET_VERSION_CONFLICT",
1898
- f"the dataset moved between the read at version {version} and this archive; somebody "
1899
- f"else is editing it. Read it with mr-data dataset show {dataset_id}, check the title "
1900
- "is still the one you typed, and archive again",
1901
- ) from refusal
1878
+ "THIN_RESPONSE_INVALID",
1879
+ "Cloud did not confirm this dataset's complete archive",
1880
+ )
1902
1881
  return {
1903
1882
  "schema_version": ARCHIVE_SCHEMA,
1904
1883
  "status": "dataset_archived",
1905
1884
  "lane": "hosted",
1906
- "dataset_id": _dataset_id(archived, verb="archive") if archived else dataset_id,
1885
+ "dataset_id": dataset_id,
1907
1886
  "name": stated,
1908
- "version": archived.get("version"),
1909
- "dashboard_url": dataset_dashboard_url(selected.session.cloud_url, dataset_id),
1887
+ "dashboard_url": dataset_dashboard_url(selected.cloud_url, dataset_id),
1910
1888
  "note": (
1911
1889
  f"The dataset is retired and the title {stated!r} is free for another one. Nothing "
1912
1890
  "built was deleted: every run, artifact, version and decision cell stays where it is."
@@ -2488,7 +2466,6 @@ def watch(
2488
2466
  }
2489
2467
 
2490
2468
 
2491
-
2492
2469
  def listing(
2493
2470
  args: argparse.Namespace, *, client: CloudDatasetAccessClient | None = None
2494
2471
  ) -> dict[str, Any]:
@@ -867,6 +867,20 @@ CLOUD_PUBLIC_TABLE_DOWNLOAD_URL_PATH = "/api/cli/public/tables/{table_id}/downlo
867
867
  QUERY_SCHEMA = f"{THIN_SCHEMA_PREFIX}-v4-public-query.v1"
868
868
  DOWNLOAD_URL_SCHEMA = f"{THIN_SCHEMA_PREFIX}-v4-public-download-url.v1"
869
869
 
870
+ #: Cloud bounds one credentialled query answer at 10 000 rows of at most 64 columns, and at 8 MiB
871
+ #: of JSON for the rows. The ceilings here admit the widest page those bounds allow: the row bytes
872
+ #: plus room for the members around them, and one member per value, per row and per member around
873
+ #: the rows.
874
+ MAX_PUBLIC_QUERY_ROWS = 10_000
875
+ MAX_PUBLIC_QUERY_COLUMNS = 64
876
+ MAX_PUBLIC_QUERY_ROW_BYTES = 8 * 1024 * 1024
877
+ MAX_PUBLIC_QUERY_ENVELOPE_BYTES = 1024 * 1024
878
+ MAX_PUBLIC_QUERY_ENVELOPE_MEMBERS = 16_384
879
+ MAX_PUBLIC_QUERY_RESPONSE_BYTES = MAX_PUBLIC_QUERY_ROW_BYTES + MAX_PUBLIC_QUERY_ENVELOPE_BYTES
880
+ MAX_PUBLIC_QUERY_RESPONSE_MEMBERS = (
881
+ MAX_PUBLIC_QUERY_ROWS * (MAX_PUBLIC_QUERY_COLUMNS + 1) + MAX_PUBLIC_QUERY_ENVELOPE_MEMBERS
882
+ )
883
+
870
884
 
871
885
  class PublicQueryClient(CloudClient):
872
886
  """Authenticated public table query + download-url over the device-key client."""
@@ -878,6 +892,9 @@ class PublicQueryClient(CloudClient):
878
892
  act="query a public table",
879
893
  body=body,
880
894
  absent="no public table has that id",
895
+ maximum=MAX_PUBLIC_QUERY_RESPONSE_BYTES,
896
+ table_values=True,
897
+ max_members=MAX_PUBLIC_QUERY_RESPONSE_MEMBERS,
881
898
  )
882
899
 
883
900
  def download_url(self, table_id: str) -> dict[str, Any]:
@@ -1091,6 +1108,8 @@ __all__ = [
1091
1108
  "FETCH_SCHEMA",
1092
1109
  "LOOKUP_SCHEMA",
1093
1110
  "MAX_LIMIT",
1111
+ "MAX_PUBLIC_QUERY_RESPONSE_BYTES",
1112
+ "MAX_PUBLIC_QUERY_RESPONSE_MEMBERS",
1094
1113
  "MAX_SAMPLE_LIMIT",
1095
1114
  "QUERY_SCHEMA",
1096
1115
  "SAMPLE_SCHEMA",
@@ -910,6 +910,11 @@ def _validate_private_file(
910
910
  def _windows_acl_is_private(path: Path, *, require_confidential: bool = True) -> bool:
911
911
  """Prove only the current user, SYSTEM, or Administrators can mutate a Windows path.
912
912
 
913
+ The path must be owned by the current user, which is what lets an OWNER RIGHTS entry count as
914
+ the current user: it names whoever owns the path. CPython 3.12.4 and later give a folder made
915
+ with ``mkdir(mode=0o700)`` three entries, for SYSTEM, Administrators and OWNER RIGHTS, and none
916
+ for the user by name.
917
+
913
918
  With ``require_confidential`` they must also be the only accounts that can read it. Only the
914
919
  explicit secure-file credential needs that, when it is read or staged; replacing or removing it
915
920
  exposes nothing. The folder, the lock file, the backend manifest and the DPAPI blob hold no
@@ -957,7 +962,8 @@ def _windows_acl_is_private(path: Path, *, require_confidential: bool = True) ->
957
962
  return False
958
963
  allowed = [current_sid]
959
964
  allocated: list[ctypes.c_void_p] = []
960
- for text in ("S-1-5-18", "S-1-5-32-544"): # SYSTEM, Administrators
965
+ # SYSTEM, Administrators, and OWNER RIGHTS, which stands for the owner proved above.
966
+ for text in ("S-1-5-18", "S-1-5-32-544", "S-1-3-4"):
961
967
  sid = ctypes.c_void_p()
962
968
  if not advapi32.ConvertStringSidToSidW(text, ctypes.byref(sid)) or not sid.value:
963
969
  return False