mostlyright-data 0.29.3__tar.gz → 0.29.4__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.4}/PKG-INFO +1 -1
  2. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/pyproject.toml +1 -1
  3. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/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.4}/skills/mr-data-build/references/making-a-table-incremental.md +67 -0
  5. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/readers.md +46 -1
  6. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/sources.md +24 -0
  7. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/canonical.py +23 -3
  8. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/table_manifest.py +1 -1
  9. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/recipe.py +15 -6
  10. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/recipe_explain.py +58 -3
  11. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/session.py +21 -2
  12. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/studio_capabilities.py +11 -0
  13. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4.py +2 -2
  14. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_public.py +19 -0
  15. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/credential_store.py +7 -1
  16. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/.gitignore +0 -0
  17. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/README.md +0 -0
  18. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/scripts/hatch_build.py +0 -0
  19. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/SKILL.md +0 -0
  20. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/agents/openai.yaml +0 -0
  21. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  22. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  23. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  24. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  25. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  26. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  27. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  28. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  29. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/agent-protocol.md +0 -0
  30. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  31. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  32. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/boundaries.md +0 -0
  33. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  34. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/commands.md +0 -0
  35. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  36. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/installation-parity.md +0 -0
  37. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/live-run.md +0 -0
  38. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  39. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  40. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/one-install.md +0 -0
  41. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/prediction-labels.md +0 -0
  42. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  43. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/promote.md +0 -0
  44. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/receipts.md +0 -0
  45. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  46. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  47. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/reference-pages.md +0 -0
  48. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/required-protocol.md +0 -0
  49. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/source-credentials.md +0 -0
  50. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  51. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/transforms.md +0 -0
  52. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  53. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  54. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  55. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/__init__.py +0 -0
  56. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  57. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/formats.py +0 -0
  58. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  59. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/key_seam.py +0 -0
  60. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/page_coverage.py +0 -0
  61. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  62. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/session_probes.py +0 -0
  63. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/skill_assets.py +0 -0
  64. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  65. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  66. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  67. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/activity.py +0 -0
  68. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  69. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/categories.py +0 -0
  70. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/commands.py +0 -0
  71. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  72. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/download.py +0 -0
  73. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  74. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  75. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/parity.py +0 -0
  76. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/probe.py +0 -0
  77. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  78. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  79. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/propose.py +0 -0
  80. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  81. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  82. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  83. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/research.py +0 -0
  84. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/router.py +0 -0
  85. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  86. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/runs.py +0 -0
  87. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/stream.py +0 -0
  88. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  89. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/transport.py +0 -0
  90. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  91. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  92. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  93. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  94. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  95. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  96. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  97. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  98. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  99. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  100. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  101. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  102. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  103. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  104. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  105. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  106. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  107. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  108. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  109. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  110. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  111. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  112. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  113. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  114. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  115. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  116. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  117. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.29.3 → mostlyright_data-0.29.4}/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.4
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.4"
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,
@@ -620,6 +625,8 @@ class CloudClient:
620
625
  expected: tuple[int, ...] = (200,),
621
626
  absent: str | None = None,
622
627
  maximum: int = MAX_TOKEN_RESPONSE_BYTES,
628
+ table_values: bool = False,
629
+ max_members: int = MAX_CANONICAL_MEMBERS,
623
630
  ) -> dict[str, Any]:
624
631
  """Make one Cloud call and return the object it answered with, or raise the refusal.
625
632
 
@@ -633,6 +640,14 @@ class CloudClient:
633
640
 
634
641
  ``absent`` is what this route's own 404 means, in the caller's words; see
635
642
  :func:`cloud_refusal` for why one sentence could not serve both routes.
643
+
644
+ ``table_values`` is for a route whose answer carries a table's own values. It admits a
645
+ finite fractional number, because a DOUBLE column or an average is not an integer, and it
646
+ reads an integer literal outside the signed 64-bit range as a float, because Cloud writes
647
+ a whole-valued double without a fraction. ``max_members`` is that answer's member ceiling,
648
+ for a route that states how large a page it may be handed. Every other hardening is
649
+ unchanged, and the default refuses all of it as it does for every answer this lane reads
650
+ as a document of its own.
636
651
  """
637
652
 
638
653
  headers = {
@@ -651,7 +666,11 @@ class CloudClient:
651
666
  maximum,
652
667
  )
653
668
  try:
654
- parsed = parse_json(raw)
669
+ parsed = (
670
+ parse_foreign_json(raw, max_members=max_members, wide_integers=True)
671
+ if table_values
672
+ else parse_json(raw)
673
+ )
655
674
  except CanonicalJSONError:
656
675
  parsed = None
657
676
  if status not in expected:
@@ -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 = "bf6312e73ba81725f9146dd6c8b10ff47ae57d1822ba695991aec03931d17d4d"
151
+ PINNED_V4_CONTRACT_VERSION = "4.26.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
@@ -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