mostlyright-data 0.31.9__tar.gz → 0.31.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/PKG-INFO +3 -2
  2. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/README.md +2 -1
  3. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/scripts/hatch_build.py +1 -1
  4. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/making-a-table-incremental.md +12 -4
  5. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/readers.md +3 -2
  6. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/source-credentials.md +2 -1
  7. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/sources.md +72 -5
  8. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe.py +10 -6
  9. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_explain.py +94 -20
  10. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_lint.py +90 -14
  11. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/studio_capabilities.py +8 -1
  12. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/table_search_contract.json +87 -18
  13. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4.py +2 -2
  14. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/.gitignore +0 -0
  15. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/pyproject.toml +0 -0
  16. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/SKILL.md +0 -0
  17. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/agents/openai.yaml +0 -0
  18. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  19. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  20. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  21. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  22. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  23. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  24. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  25. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  26. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  27. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/agent-protocol.md +0 -0
  28. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  29. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  30. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/boundaries.md +0 -0
  31. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  32. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/commands.md +0 -0
  33. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  34. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
  35. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/installation-parity.md +0 -0
  36. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/live-run.md +0 -0
  37. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  38. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  39. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/one-install.md +0 -0
  40. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/patterns.md +0 -0
  41. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/prediction-labels.md +0 -0
  42. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  43. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/promote.md +0 -0
  44. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/receipts.md +0 -0
  45. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recipe-test.md +0 -0
  46. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  47. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  48. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/reference-pages.md +0 -0
  49. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/required-protocol.md +0 -0
  50. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  51. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/transforms.md +0 -0
  52. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  53. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  54. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  55. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/__init__.py +0 -0
  56. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  57. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/canonical.py +0 -0
  58. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/formats.py +0 -0
  59. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  60. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/key_seam.py +0 -0
  61. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/page_coverage.py +0 -0
  62. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  63. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/session_probes.py +0 -0
  64. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/skill_assets.py +0 -0
  65. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/table_manifest.py +0 -0
  66. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  67. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  68. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  69. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/activity.py +0 -0
  70. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  71. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/backoff.py +0 -0
  72. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/categories.py +0 -0
  73. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/commands.py +0 -0
  74. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  75. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/delivery.py +0 -0
  76. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
  77. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/download.py +0 -0
  78. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
  79. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  80. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
  81. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  82. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/parity.py +0 -0
  83. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/probe.py +0 -0
  84. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  85. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  86. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/propose.py +0 -0
  87. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  88. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  89. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
  90. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
  91. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/release_check.py +0 -0
  92. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/research.py +0 -0
  93. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/router.py +0 -0
  94. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  95. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
  96. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/runs.py +0 -0
  97. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/session.py +0 -0
  98. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/stream.py +0 -0
  99. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  100. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/support.py +0 -0
  101. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/table_view.py +0 -0
  102. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/transport.py +0 -0
  103. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  104. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  105. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  106. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  107. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  108. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  109. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  110. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  111. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
  112. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  113. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
  114. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  115. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  116. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  117. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  118. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
  119. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  120. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  121. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  122. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  123. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  124. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  125. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  126. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  127. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  128. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  129. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  130. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  131. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  132. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  133. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  134. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  135. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/login.py +0 -0
  136. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  137. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  138. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  139. {mostlyright_data-0.31.9 → mostlyright_data-0.31.10}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.31.9
3
+ Version: 0.31.10
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -216,10 +216,11 @@ There is no `dev` extra to ask for -- `uv run --extra dev` answers
216
216
  tooling is the default dependency group, which `uv sync` installs.
217
217
 
218
218
  `make lint` is the repository's one lint set (`uv run ruff check .` plus `uv lock --check`); it is
219
- the first thing `make verify` runs and the same target the Linux leg of `thin-client-smoke.yml`
219
+ the first thing `make verify` runs and the same target the checkout leg of `thin-client-smoke.yml`
220
220
  runs, so the two cannot disagree about what is linted.
221
221
 
222
222
  The test suite checks client behavior, contract compatibility and thin package ownership. Backend
223
223
  execution and worker-boundary tests run in Studio. The client supports Linux, macOS and Windows.
224
+ CI runs on Linux only, on the AWS runner pool, so macOS and Windows are not tested there.
224
225
 
225
226
  To update the public Studio contract pin, run `make repin STUDIO_GIT=/path/to/studio STUDIO_REF=<commit>`. It reads committed OpenAPI and JSON Schema bytes, records their hashes and source commit in `vendor/studio-contracts/client-v4-pin.json`, and updates the thin client. Run the cross-repository tests against that Studio revision before release. Worker/client generation and backend job-contract repins now belong to Studio.
@@ -201,10 +201,11 @@ There is no `dev` extra to ask for -- `uv run --extra dev` answers
201
201
  tooling is the default dependency group, which `uv sync` installs.
202
202
 
203
203
  `make lint` is the repository's one lint set (`uv run ruff check .` plus `uv lock --check`); it is
204
- the first thing `make verify` runs and the same target the Linux leg of `thin-client-smoke.yml`
204
+ the first thing `make verify` runs and the same target the checkout leg of `thin-client-smoke.yml`
205
205
  runs, so the two cannot disagree about what is linted.
206
206
 
207
207
  The test suite checks client behavior, contract compatibility and thin package ownership. Backend
208
208
  execution and worker-boundary tests run in Studio. The client supports Linux, macOS and Windows.
209
+ CI runs on Linux only, on the AWS runner pool, so macOS and Windows are not tested there.
209
210
 
210
211
  To update the public Studio contract pin, run `make repin STUDIO_GIT=/path/to/studio STUDIO_REF=<commit>`. It reads committed OpenAPI and JSON Schema bytes, records their hashes and source commit in `vendor/studio-contracts/client-v4-pin.json`, and updates the thin client. Run the cross-repository tests against that Studio revision before release. Worker/client generation and backend job-contract repins now belong to Studio.
@@ -15,7 +15,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
15
15
  #
16
16
  # A compiled member would not, which is why the tag is not the only thing this hook does. It also
17
17
  # REFUSES TO BUILD when it finds one, rather than falling back to a platform tag: a fallback is
18
- # the worst outcome on the list, because the any-OS smoke matrix would keep passing while quietly
18
+ # the worst outcome on the list, because the thin-client smoke would keep passing while quietly
19
19
  # testing a different artifact. A universal tag over a compiled payload is worse still -- it
20
20
  # installs everywhere and fails at import on all but one platform, on somebody else's machine.
21
21
  #
@@ -603,16 +603,24 @@ again. Ask the user before passing either flag. Never pass one to clear a refusa
603
603
 
604
604
  Check `studio_capabilities.keyless_window` is literal `true` before using this grammar.
605
605
  An absent or false capability means "not available on this Studio yet". Registration and
606
- file explanation refuse with `THIN_GRAMMAR_NOT_SERVED` before local lint or registration;
607
- an older API's schema 422 on the explanation also refuses. No dataset delegation is written.
606
+ file explanation refuse with `THIN_GRAMMAR_NOT_SERVED` before local lint or registration.
607
+ An older API may answer the explanation with a schema 422 before it reports capabilities. The
608
+ client then runs its local checks and lint. A local fault is reported as itself. Only a clean
609
+ document is refused as not served. That refusal marks each member with the capability it needs,
610
+ `keyless_post` members first. No dataset delegation is written.
608
611
  The capability covers `public.https.api@1.0.0`, `generic_api.auth.kind: none`, endpoint
609
612
  `epoch_millis`, the `epoch_millis_value` partition key, and the two request controls below. Existing credential-free file and collection
610
613
  adapters do not require this capability.
611
614
 
612
615
  For a public JSON/CSV API use `credential_mode: none`, an HTTPS `origin`, and `generic_api`
613
- with exact `auth: {"kind":"none"}`. Requests are GET-only, with no body or credential reference.
616
+ with exact `auth: {"kind":"none"}`. A request is a GET, or a POST with a `body_template` on
617
+ Studio 4.49. It states no credential reference. A POST needs `studio_capabilities.keyless_post`
618
+ literal `true`. Otherwise registration refuses with `THIN_GRAMMAR_NOT_SERVED` and names the
619
+ request `method` first, marked `(keyless_post)`. Send a GET in its place, or wait for the Studio
620
+ deploy.
614
621
  Authenticated adapters retain their existing credential requirements. Declare both window
615
- endpoints with `location: parameter` and corresponding required request parameters. Origin,
622
+ endpoints with `location: parameter` and corresponding required request parameters. A
623
+ parameter fills its placeholder in the query, or in the body of a POST. Origin,
616
624
  host allowlists and cursor next-URL restrictions still apply.
617
625
 
618
626
  For Binance BTCUSDT one-minute klines, start the backfill at `2017-08-17T00:00:00Z` and request
@@ -173,8 +173,9 @@ response matches the family's exact media-type, settings, and budget contract.
173
173
  ### Needs Studio 4.26
174
174
 
175
175
  Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
176
- Use `public.https.api@1.0.0` only when `keyless_window` is literal `true`; otherwise report
177
- "not available on this Studio yet". Registration and file explanation refuse with
176
+ Use `public.https.api@1.0.0` only when `keyless_window` is literal `true`. Otherwise report
177
+ "not available on this Studio yet". A POST request needs Studio 4.49 and `keyless_post` literal
178
+ `true` as well. Registration and file explanation refuse with
178
179
  `THIN_GRAMMAR_NOT_SERVED`, including an older API's schema 422 before capabilities.
179
180
 
180
181
  The new adapter uses the existing `generic_api.response` Reader contract. For Binance klines,
@@ -33,7 +33,8 @@ the user — the source needs a credential and which one — and nothing about t
33
33
  A [per-row request](sources.md#authoring-a-per-row-request) presents a key by its source's
34
34
  `connector.credential_mode`.
35
35
 
36
- - `none` presents nothing, and the request is a `GET`.
36
+ - `none` presents nothing. The request is a `GET`. Needs Studio 4.49: it may be a `POST` with a
37
+ `body_template` when `studio_capabilities.keyless_post` is literal `true`.
37
38
  - `opaque_reference` presents the secret named in `credential.secret_name`, the way
38
39
  `request.auth` says. A saved connection cannot stand in for it.
39
40
  - `platform` presents a key Mostly Right holds, so the user enrols nothing.
@@ -254,10 +254,15 @@ breaks a rule above.
254
254
 
255
255
  | `credential_mode` | State | Use it for |
256
256
  | --- | --- | --- |
257
- | `none` | no `credential`, no `auth`, method `GET` | a public API that takes no key |
257
+ | `none` | no `credential` and no `auth`. A `GET`, or a `POST` with `body_template` (Needs Studio 4.49) | a public API that takes no key |
258
258
  | `opaque_reference` | `credential.secret_name` and `request.auth` | a key the user enrolled with `mr-data keys set` |
259
259
  | `platform` | no `credential` and no `auth` | an endpoint Mostly Right holds a key for |
260
260
 
261
+ A keyless per-row `POST` needs Studio 4.49. Use it only when `keyless_post` is literal `true` in
262
+ `studio_capabilities`. Otherwise the client refuses with `THIN_GRAMMAR_NOT_SERVED` and names the
263
+ request `method` first, marked `(keyless_post)`. Send a GET in its place, or wait for the Studio
264
+ deploy.
265
+
261
266
  The platform credential needs no key from anyone, and any subscribed workspace may use it. Each
262
267
  binding is one route and one pinned model, and `mr-data models --json` lists the ones this
263
268
  deployment serves: Clef for classification (`classify`), TypeSafe's Jev through OpenRouter
@@ -378,18 +383,80 @@ full grammar.
378
383
  ### Needs Studio 4.26
379
384
 
380
385
  Check first: run `mr-data recipe explain RECIPE.json --json` and read `studio_capabilities`.
381
- Use the following only when `keyless_window` is literal `true`; otherwise report "not available
382
- on this Studio yet". The client refuses with `THIN_GRAMMAR_NOT_SERVED` before registration,
383
- including when an older API rejects the explanation document with schema 422.
386
+ Use the following only when `keyless_window` is literal `true`. Otherwise report "not available
387
+ on this Studio yet". The client refuses with `THIN_GRAMMAR_NOT_SERVED` before registration.
388
+ An older API may reject the explanation document with a schema 422 before it reports
389
+ capabilities. The client then runs its local checks and lint. A local fault is reported as
390
+ itself. Only a clean document is refused as not served. That refusal marks each member with the
391
+ capability it needs, `keyless_post` members first.
384
392
 
385
393
  `public.https.api@1.0.0` is the credential-free generic HTTPS API adapter. State
386
394
  `credential_mode: none`, `origin`, and `generic_api` with exact `auth: {"kind":"none"}`.
387
- Use GET only: no request body, credential reference, token, secret header or secret query value.
395
+ A request is a GET, or a POST with a `body_template` on Studio 4.49. It presents no credential:
396
+ no credential reference, token, secret header or secret query value.
388
397
  This adapter does not relax authentication on the keyed adapters. The owned HTTPS origin,
389
398
  host allowlist, bounded request budgets and cursor next-URL restrictions remain in force.
390
399
  The existing generic response mapping and JSON/CSV pagination apply; a cursor needs the
391
400
  publisher's actual next-token pointer. JSON-pointer pagination does not extract XML OAI-PMH tokens.
392
401
 
402
+ Needs Studio 4.49. Some APIs take the date range in a JSON body. Send a POST and state the body
403
+ in `body_template`. Use it only when `keyless_post` is literal `true` in `studio_capabilities`.
404
+ Otherwise the client refuses with `THIN_GRAMMAR_NOT_SERVED` and names
405
+ `generic_api/request/method` first, marked `(keyless_post)`. Send a GET in its place, or wait for
406
+ the Studio deploy.
407
+ Declare both window endpoints with `location: parameter`. Each endpoint fills the body
408
+ placeholder of the parameter it names. One request covers one window unit, so state
409
+ `pagination: {"kind": "none", "max_pages": 1}`. The values frozen in `connector.parameters` are
410
+ the first day of the window, the day `start_at` names. This response is a root array, so
411
+ `records_pointer` is `""`. Write `response.reader` the way [Readers](readers.md#needs-studio-426)
412
+ shows. The publisher here is invented:
413
+
414
+ ```json
415
+ {
416
+ "connector": {
417
+ "adapter_id": "public.https.api@1.0.0",
418
+ "credential_mode": "none",
419
+ "origin": "https://api.prices.example",
420
+ "parameters": [
421
+ {"name": "from_date", "value": "2026-01-01"},
422
+ {"name": "to_date", "value": "2026-01-01"}
423
+ ],
424
+ "generic_api": {
425
+ "request": {
426
+ "method": "POST",
427
+ "path_template": "/v1/prices/query",
428
+ "static_query": [],
429
+ "static_headers": [],
430
+ "body_template": {"market": "day_ahead", "from": "{from_date}", "to": "{to_date}"},
431
+ "parameters": [
432
+ {"name": "from_date", "type": "string", "required": true},
433
+ {"name": "to_date", "type": "string", "required": true}
434
+ ]
435
+ },
436
+ "auth": {"kind": "none"},
437
+ "response": {"format": "json", "records_pointer": ""},
438
+ "pagination": {"kind": "none", "max_pages": 1}
439
+ }
440
+ },
441
+ "window": {
442
+ "start_at": "2026-01-01T00:00:00Z",
443
+ "granularity": "day",
444
+ "timezone": "UTC",
445
+ "lookback_seconds": 86400,
446
+ "max_span_seconds": 31622400,
447
+ "request": {
448
+ "start": {"encoding": "iso_date", "bound": "inclusive", "location": "parameter", "parameter": "from_date"},
449
+ "end": {"encoding": "iso_date", "bound": "inclusive", "location": "parameter", "parameter": "to_date"}
450
+ },
451
+ "merge": {
452
+ "materialization": "partition_replace",
453
+ "partition": {"column": "date", "key": "iso_date_value"},
454
+ "row_identity": ["date", "hour"]
455
+ }
456
+ }
457
+ }
458
+ ```
459
+
393
460
  For Binance klines use parameter-windowed `startTime`/`endTime`, millisecond endpoints and
394
461
  12-hour slices at one request per second. Follow the backfill and hourly-refresh example in
395
462
  [making a table incremental](making-a-table-incremental.md#keyless-api-windows-studio-426)
@@ -168,7 +168,7 @@ _BOUNDED_STREAMING_POLICY = "bounded-streaming.v1"
168
168
  FILE_DECODER_COORDINATES: frozenset[tuple[str, str]] = frozenset(
169
169
  (family, version)
170
170
  for family, versions in {
171
- "archive.gzip": ("1.0.0", "1.1.0", "2.0.0"),
171
+ "archive.gzip": ("1.0.0", "1.1.0", "2.0.0", "2.1.0"),
172
172
  "archive.tar": ("1.0.0", "1.1.0", "3.0.0", "4.0.0"),
173
173
  "archive.zip": ("1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "3.0.0"),
174
174
  "delimited_text": ("1.0.0", "1.1.0", "2.0.0"),
@@ -1431,9 +1431,10 @@ def register(
1431
1431
  ) -> dict[str, Any]:
1432
1432
  """``mr-data recipe FILE``: register one document and print what the server derived from it.
1433
1433
 
1434
- Existing documents are linted before a credential is resolved. A keyless-window document
1435
- asks what Studio serves before local lint, because an older schema may reject the grammar
1436
- before reporting capabilities. The brief and delegation gates follow; registration is last.
1434
+ Existing documents are linted before a credential is resolved. A document using keyless
1435
+ grammar (``keyless_window`` or ``keyless_post``) asks what Studio serves before local lint,
1436
+ because an older schema may reject the grammar before reporting capabilities. The brief and
1437
+ delegation gates follow, and registration is last.
1437
1438
  """
1438
1439
 
1439
1440
  document = read_document(Path(args.target))
@@ -1442,13 +1443,16 @@ def register(
1442
1443
  v4.recipe_registration_body(document, "00000000-0000-0000-0000-000000000000")
1443
1444
  # New grammar must be refused as unserved before local lint can diagnose its shape.
1444
1445
  keyless = any(
1445
- capability == "keyless_window" for _, capability in recipe_explain.gated_grammar(document)
1446
+ capability in recipe_explain.KEYLESS_CAPABILITIES
1447
+ for _, capability in recipe_explain.gated_grammar(document)
1446
1448
  )
1447
1449
  selected = client
1448
1450
  served = []
1449
1451
  if keyless:
1450
1452
  selected = selected or _client(args)
1451
- served = recipe_explain.registration_gate(selected, document)
1453
+ served = recipe_explain.registration_gate(
1454
+ selected, document, lint=not getattr(args, "no_lint", False)
1455
+ )
1452
1456
  check_document(document)
1453
1457
  if not getattr(args, "no_lint", False):
1454
1458
  refusal = lint_refusal(
@@ -35,6 +35,13 @@ from urllib.parse import parse_qsl, urlsplit
35
35
  from uuid import UUID
36
36
 
37
37
  from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX, numbered
38
+ from mostlyright.data_harness.thin.recipe_lint import (
39
+ KEYLESS_API_ADAPTER_ID,
40
+ ROW_REQUEST_ADAPTER_ID,
41
+ lint_document,
42
+ lint_refusal,
43
+ )
44
+ from mostlyright.data_harness.thin.studio_capabilities import KEYLESS_CAPABILITIES, KEYLESS_POST
38
45
  from mostlyright.data_harness.thin.transport import ThinLaneError
39
46
  from mostlyright.data_harness.thin.v4 import (
40
47
  EXPLAIN_AND_FORECAST_REQUEST_VERSION,
@@ -131,6 +138,7 @@ CAPABILITY_NAMES: tuple[str, ...] = (
131
138
  "snapshot_refresh",
132
139
  "row_request_batch",
133
140
  "keyless_window",
141
+ "keyless_post",
134
142
  )
135
143
 
136
144
  #: What a Studio that answers 404 or 405 from the explanation routes serves (README D27).
@@ -1228,12 +1236,17 @@ def _explanation(answer: Any, path: str) -> dict[str, Any]:
1228
1236
 
1229
1237
 
1230
1238
  def explain_document_at_studio(
1231
- client: StudioV4Client, document: Mapping[str, Any]
1239
+ client: StudioV4Client, document: Mapping[str, Any], *, lint: bool = True
1232
1240
  ) -> dict[str, Any] | None:
1233
1241
  """``POST /v4/recipes:explain-refresh``, or ``None`` when this Studio does not serve it.
1234
1242
 
1235
1243
  Studio judges the document alone: it reads no predecessor, registers nothing and writes
1236
1244
  nothing, which is what makes it safe to ask before a registration.
1245
+
1246
+ An older API's schema answers 422 to a keyless document, and also to a document that is
1247
+ simply wrong. So that 422 is checked against the local document rules and, unless ``lint`` is
1248
+ false, the local lint. A local fault is raised as itself. Only a document that passes both is
1249
+ reported as not served.
1237
1250
  """
1238
1251
 
1239
1252
  body = {
@@ -1254,23 +1267,66 @@ def explain_document_at_studio(
1254
1267
  if route_unavailable(refusal):
1255
1268
  return None
1256
1269
  keyless = [
1257
- pointer
1270
+ (pointer, capability)
1258
1271
  for pointer, capability in gated_grammar(document)
1259
- if capability == "keyless_window"
1272
+ if capability in KEYLESS_CAPABILITIES
1260
1273
  ]
1261
1274
  if keyless and getattr(refusal, "http_status", None) == 422:
1275
+ _raise_local_fault(document, lint=lint, cause=refusal)
1262
1276
  # Older API schemas reject the document before they can answer capabilities.
1263
1277
  # A schema rejection is never evidence that this grammar is served.
1264
1278
  raise ThinLaneError(
1265
1279
  GRAMMAR_NOT_SERVED_CODE,
1266
- f"not available on this Studio yet: {', '.join(keyless)} "
1267
- "(Studio rejected the explanation document before reporting capabilities). "
1268
- "Nothing was registered. Wait for the Studio deploy, or remove the member",
1280
+ not_served_detail(
1281
+ keyless,
1282
+ "Studio rejected the explanation document before reporting capabilities, "
1283
+ "and the document passes the local checks",
1284
+ known=False,
1285
+ ),
1269
1286
  ) from refusal
1270
1287
  raise
1271
1288
  return _explanation(answer, EXPLAIN_REFRESH_PATH)
1272
1289
 
1273
1290
 
1291
+ def _raise_local_fault(document: Mapping[str, Any], *, lint: bool, cause: ThinLaneError) -> None:
1292
+ """Raise the local finding a schema 422 may stand for, or return when there is none."""
1293
+
1294
+ from mostlyright.data_harness.thin import recipe as recipe_module
1295
+
1296
+ try:
1297
+ recipe_module.check_document(dict(document))
1298
+ except ThinLaneError as fault:
1299
+ raise fault from cause
1300
+ if lint:
1301
+ fault = lint_refusal(lint_document(document), subject="the recipe document")
1302
+ if fault is not None:
1303
+ raise fault from cause
1304
+
1305
+
1306
+ def not_served_detail(missing: Sequence[tuple[str, str]], why: str, *, known: bool) -> str:
1307
+ """The not-served sentence: each pointer with the capability it needs, ``keyless_post`` first.
1308
+
1309
+ A keyless POST is the newest gated grammar, so where one is listed its method is named first
1310
+ and its repair is a GET in its place. ``known`` says whether Studio stated that the other
1311
+ members are unserved. After a schema 422 it did not, so they are not told to be removed.
1312
+ """
1313
+
1314
+ ordered = sorted(missing, key=lambda item: item[1] != KEYLESS_POST)
1315
+ listed = ", ".join(f"{pointer} ({capability})" for pointer, capability in ordered)
1316
+ post = any(capability == KEYLESS_POST for _, capability in missing)
1317
+ others = any(capability != KEYLESS_POST for _, capability in missing)
1318
+ if post and others and known:
1319
+ repair = "send each keyless_post request as a GET, and remove the other members"
1320
+ elif post:
1321
+ repair = "send each keyless_post request as a GET"
1322
+ else:
1323
+ repair = "remove the member"
1324
+ return (
1325
+ f"not available on this Studio yet: {listed}. {why}. Nothing was registered. Wait for "
1326
+ f"the Studio deploy, or {repair}"
1327
+ )
1328
+
1329
+
1274
1330
  def explain_table_at_studio(client: StudioV4Client, table_id: str) -> dict[str, Any] | None:
1275
1331
  """``GET /v4/tables/{table_id}/refresh-explanation``, or ``None`` when it is not served."""
1276
1332
 
@@ -1308,7 +1364,9 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
1308
1364
  ``row_filter`` on any collection discovery, and a per-row request collection whose
1309
1365
  ``pages.revisit`` states ``interval`` (4.14.0). A revisit on any other collection is old
1310
1366
  grammar. ``batch`` on a per-row request (4.15.0). The public generic API, no-auth kind,
1311
- millisecond endpoints and slice/pacing members (4.26.0) each need ``keyless_window``.
1367
+ millisecond endpoints and slice/pacing members (4.26.0) each need ``keyless_window``. A POST
1368
+ that presents no credential (4.49.0) needs ``keyless_post``: a ``public.https.api@1.0.0``
1369
+ request, or a per-row request under ``credential_mode: none``. Its pointer is the method.
1312
1370
  """
1313
1371
 
1314
1372
  gated: list[tuple[str, str]] = []
@@ -1323,12 +1381,16 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
1323
1381
  prefix = f"/sources/{index}"
1324
1382
  connector = source.get("connector")
1325
1383
  if isinstance(connector, Mapping):
1326
- if connector.get("adapter_id") == "public.https.api@1.0.0":
1384
+ keyless_api = connector.get("adapter_id") == KEYLESS_API_ADAPTER_ID
1385
+ if keyless_api:
1327
1386
  gated.append((f"{prefix}/connector/adapter_id", "keyless_window"))
1328
1387
  generic = connector.get("generic_api")
1329
1388
  auth = generic.get("auth") if isinstance(generic, Mapping) else None
1330
1389
  if isinstance(auth, Mapping) and auth.get("kind") == "none":
1331
1390
  gated.append((f"{prefix}/connector/generic_api/auth/kind", "keyless_window"))
1391
+ request = generic.get("request") if isinstance(generic, Mapping) else None
1392
+ if keyless_api and isinstance(request, Mapping) and request.get("method") == "POST":
1393
+ gated.append((f"{prefix}/connector/generic_api/request/method", "keyless_post"))
1332
1394
  window = source.get("window")
1333
1395
  merge = window.get("merge") if isinstance(window, Mapping) else None
1334
1396
  partition = merge.get("partition") if isinstance(merge, Mapping) else None
@@ -1363,6 +1425,14 @@ def gated_grammar(document: Mapping[str, Any]) -> list[tuple[str, str]]:
1363
1425
  gated.append(
1364
1426
  (f"/sources/{index}/collection/discovery/request/batch", "row_request_batch")
1365
1427
  )
1428
+ if (
1429
+ isinstance(connector, Mapping)
1430
+ and connector.get("adapter_id") == ROW_REQUEST_ADAPTER_ID
1431
+ and connector.get("credential_mode") == "none"
1432
+ and isinstance(request, Mapping)
1433
+ and request.get("method") == "POST"
1434
+ ):
1435
+ gated.append((f"/sources/{index}/collection/discovery/request/method", "keyless_post"))
1366
1436
  return gated
1367
1437
 
1368
1438
 
@@ -1381,7 +1451,9 @@ def snapshot_sources(document: Mapping[str, Any]) -> list[str]:
1381
1451
  return named
1382
1452
 
1383
1453
 
1384
- def registration_gate(client: StudioV4Client, document: Mapping[str, Any]) -> list[str]:
1454
+ def registration_gate(
1455
+ client: StudioV4Client, document: Mapping[str, Any], *, lint: bool = True
1456
+ ) -> list[str]:
1385
1457
  """Refuse grammar this Studio does not serve, and return the snapshot warning when it applies.
1386
1458
 
1387
1459
  Asks the document route only when the answer can change what happens: the document uses
@@ -1389,14 +1461,15 @@ def registration_gate(client: StudioV4Client, document: Mapping[str, Any]) -> li
1389
1461
  other document registers exactly as before, with no extra request.
1390
1462
 
1391
1463
  ⚠ REFUSED BEFORE ANYTHING IS WRITTEN. The caller runs this before the brief gate's delegation
1392
- append and before the registration, so a refusal here leaves nothing behind.
1464
+ append and before the registration, so a refusal here leaves nothing behind. ``lint`` is
1465
+ false under ``--no-lint``, so an older schema's 422 is then not checked against local lint.
1393
1466
  """
1394
1467
 
1395
1468
  gated = gated_grammar(document)
1396
1469
  snapshots = snapshot_sources(document)
1397
1470
  if not gated and not snapshots:
1398
1471
  return []
1399
- answer = explain_document_at_studio(client, document)
1472
+ answer = explain_document_at_studio(client, document, lint=lint)
1400
1473
  return grammar_gate(document, answer)
1401
1474
 
1402
1475
 
@@ -1406,18 +1479,16 @@ def grammar_gate(document: Mapping[str, Any], answer: Mapping[str, Any] | None)
1406
1479
  gated = gated_grammar(document)
1407
1480
  snapshots = snapshot_sources(document)
1408
1481
  capabilities = served_capabilities(answer)
1409
- missing = [pointer for pointer, capability in gated if not capabilities[capability]]
1482
+ missing = [
1483
+ (pointer, capability) for pointer, capability in gated if not capabilities[capability]
1484
+ ]
1410
1485
  if missing:
1411
1486
  why = (
1412
- "this Studio does not serve the refresh explanation, so it predates 4.14.0"
1487
+ "This Studio does not serve the refresh explanation, so it predates 4.14.0"
1413
1488
  if answer is None
1414
- else "this Studio's capabilities say it does not serve it"
1415
- )
1416
- raise ThinLaneError(
1417
- GRAMMAR_NOT_SERVED_CODE,
1418
- f"not available on this Studio yet: {', '.join(missing)} ({why}). Nothing was "
1419
- "registered. Wait for the Studio deploy, or remove the member",
1489
+ else "This Studio's capabilities say it does not serve them"
1420
1490
  )
1491
+ raise ThinLaneError(GRAMMAR_NOT_SERVED_CODE, not_served_detail(missing, why, known=True))
1421
1492
  if snapshots and not capabilities["snapshot_refresh"]:
1422
1493
  return [
1423
1494
  f"{SNAPSHOT_WILL_NOT_REFRESH_YET}: {', '.join(repr(name) for name in snapshots)} "
@@ -1513,7 +1584,9 @@ def explain(args: argparse.Namespace, *, client: StudioV4Client | None = None) -
1513
1584
  else:
1514
1585
  document = recipe_module.read_document(Path(target))
1515
1586
  selected = client or recipe_module._client(args)
1516
- keyless = any(capability == "keyless_window" for _, capability in gated_grammar(document))
1587
+ keyless = any(
1588
+ capability in KEYLESS_CAPABILITIES for _, capability in gated_grammar(document)
1589
+ )
1517
1590
  if keyless:
1518
1591
  answer = explain_document_at_studio(selected, document)
1519
1592
  grammar_gate(document, answer)
@@ -1625,6 +1698,7 @@ __all__ = [
1625
1698
  "EXPLAIN_REFRESH_PATH",
1626
1699
  "EXPLAIN_SCHEMA",
1627
1700
  "GRAMMAR_NOT_SERVED_CODE",
1701
+ "KEYLESS_CAPABILITIES",
1628
1702
  "MIRRORED_RULE_CODES",
1629
1703
  "NO_CAPABILITIES",
1630
1704
  "ROUTES_REQUEST_VERSION",
@@ -161,6 +161,9 @@ COHERENCE_CREDENTIAL_MODES: frozenset[str] = frozenset({"none", "opaque_referenc
161
161
  #: ``request`` instead of ``url_column``, on a source declared through this adapter.
162
162
  ROW_REQUEST_ADAPTER_ID = "public.https.collection@2.0.0"
163
163
 
164
+ #: The generic API adapter that presents no credential (4.26.0).
165
+ KEYLESS_API_ADAPTER_ID = "public.https.api@1.0.0"
166
+
164
167
  #: One per-row template token, read left to right: an escaped brace, a ``{name}`` placeholder, a
165
168
  #: lone brace, or a run of text with no brace. The registration route and the worker read every
166
169
  #: per-row template with this same expression, and no per-row template pattern of
@@ -237,6 +240,7 @@ def lint_document(document: Mapping[str, Any]) -> list[Finding]:
237
240
  findings.extend(_refresh_trigger(document))
238
241
  findings.extend(_row_filters(document.get("sources")))
239
242
  findings.extend(_row_request_credentials(document.get("sources")))
243
+ findings.extend(_keyless_api_requests(document.get("sources")))
240
244
  findings.extend(_row_request_templates(document.get("sources")))
241
245
  findings.extend(_units(document.get("units")))
242
246
  findings.extend(_coherence(document, declared=declared))
@@ -908,7 +912,10 @@ def _row_request_credentials(sources: Any) -> list[Finding]:
908
912
  request's origin, method and path says which credential is presented and how.
909
913
  - ``opaque_reference`` names a workspace secret in ``credential.secret_name`` and states
910
914
  ``auth``, which says how that secret is presented.
911
- - ``none`` states no ``auth`` and sends a GET.
915
+ - ``none`` states no ``auth``.
916
+
917
+ Whatever the mode, a request sends a GET with no ``body_template``, or a POST with one.
918
+ ``batch`` stays GET-only, which :func:`_row_request_batch_coherence` holds.
912
919
 
913
920
  Each is a schema rule, so each finding carries :data:`DOCUMENT_INVALID`. Which binding admits
914
921
  a ``platform`` request is deployment configuration that no document carries, so that question
@@ -1006,16 +1013,87 @@ def _row_request_credentials(sources: Any) -> list[Finding]:
1006
1013
  "name one with 'opaque_reference', or drop it",
1007
1014
  )
1008
1015
  )
1009
- method = request.get("method")
1010
- if "method" in request and method != "GET":
1011
- findings.append(
1012
- Finding(
1013
- DOCUMENT_INVALID,
1014
- f"{at}/method",
1015
- f"is {method!r}, and a per-row request that presents no credential is a "
1016
- "GET",
1017
- )
1016
+ if request is not None:
1017
+ findings.extend(_request_method(request, at=at, subject="A per-row request"))
1018
+ return findings
1019
+
1020
+
1021
+ def _request_method(request: Mapping[str, Any], *, at: str, subject: str) -> list[Finding]:
1022
+ """The generic request rule: a GET states no ``body_template``, and a POST states one.
1023
+
1024
+ ``subject`` begins the sentence a wrong method earns. A request that states no method is the
1025
+ schema's missing-member fault, and nothing is read off it here.
1026
+ """
1027
+
1028
+ if "method" not in request:
1029
+ return []
1030
+ method = request.get("method")
1031
+ if _outside(method, frozenset({"GET", "POST"})):
1032
+ return [
1033
+ Finding(
1034
+ DOCUMENT_INVALID,
1035
+ f"{at}/method",
1036
+ f"is {method!r}. {subject} is a GET, or a POST with a body_template",
1037
+ )
1038
+ ]
1039
+ if method == "POST" and "body_template" not in request:
1040
+ return [
1041
+ Finding(
1042
+ DOCUMENT_INVALID,
1043
+ f"{at}/body_template",
1044
+ "is absent, and a POST sends the body this member renders",
1045
+ )
1046
+ ]
1047
+ if method == "GET" and "body_template" in request:
1048
+ return [
1049
+ Finding(
1050
+ DOCUMENT_INVALID,
1051
+ f"{at}/body_template",
1052
+ "is stated on a GET, which sends no body. Make the request a POST, or drop it",
1053
+ )
1054
+ ]
1055
+ return []
1056
+
1057
+
1058
+ def _keyless_api_requests(sources: Any) -> list[Finding]:
1059
+ """A ``public.https.api@1.0.0`` request, held to the rules Studio's parser holds it to.
1060
+
1061
+ The adapter presents no credential, so ``generic_api.auth`` is exactly ``{"kind": "none"}``.
1062
+ Its request is a GET with no ``body_template``, or a POST with one (4.49.0). Each is a schema
1063
+ rule, so each finding carries :data:`DOCUMENT_INVALID`.
1064
+ """
1065
+
1066
+ if not isinstance(sources, list):
1067
+ return []
1068
+ findings: list[Finding] = []
1069
+ for index, source in enumerate(sources):
1070
+ if not isinstance(source, Mapping):
1071
+ continue
1072
+ connector = source.get("connector")
1073
+ if not isinstance(connector, Mapping):
1074
+ continue
1075
+ generic = connector.get("generic_api")
1076
+ if connector.get("adapter_id") != KEYLESS_API_ADAPTER_ID or not isinstance(
1077
+ generic, Mapping
1078
+ ):
1079
+ continue
1080
+ at = f"/sources/{index}/connector/generic_api"
1081
+ if "auth" in generic and generic.get("auth") != {"kind": "none"}:
1082
+ findings.append(
1083
+ Finding(
1084
+ DOCUMENT_INVALID,
1085
+ f"{at}/auth",
1086
+ f"is {generic.get('auth')!r}. A {KEYLESS_API_ADAPTER_ID} source presents no "
1087
+ 'credential, so its auth is exactly {"kind": "none"}',
1088
+ )
1089
+ )
1090
+ request = generic.get("request")
1091
+ if isinstance(request, Mapping):
1092
+ findings.extend(
1093
+ _request_method(
1094
+ request, at=f"{at}/request", subject=f"A {KEYLESS_API_ADAPTER_ID} request"
1018
1095
  )
1096
+ )
1019
1097
  return findings
1020
1098
 
1021
1099
 
@@ -1292,8 +1370,7 @@ def _row_request_batch_coherence(sources: Any) -> list[Finding]:
1292
1370
  Finding(
1293
1371
  DOCUMENT_INCOHERENT,
1294
1372
  f"{at}/batch/parameter",
1295
- f"names {name!r}, which must be declared with required: true to be "
1296
- "batched",
1373
+ f"names {name!r}, which must be declared with required: true to be batched",
1297
1374
  )
1298
1375
  )
1299
1376
  sites = [
@@ -1354,8 +1431,7 @@ def _table_partition_coherence(document: Mapping[str, Any], *, known: set[str])
1354
1431
  sources = document.get("sources")
1355
1432
  sources = sources if isinstance(sources, list) else []
1356
1433
  if any(
1357
- isinstance(source, Mapping) and source.get("source_class") == "stream"
1358
- for source in sources
1434
+ isinstance(source, Mapping) and source.get("source_class") == "stream" for source in sources
1359
1435
  ):
1360
1436
  findings.append(
1361
1437
  Finding(