mostlyright-data 0.31.11__tar.gz → 0.31.13__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 (140) hide show
  1. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/PKG-INFO +8 -1
  2. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/README.md +7 -0
  3. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/preparing-table-search.md +47 -3
  4. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/router.py +2 -0
  5. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/runs.py +2 -0
  6. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/session.py +3 -1
  7. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/stream.py +2 -0
  8. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/table_search_contract.json +44 -1
  9. mostlyright_data-0.31.13/src/mostlyright/data_harness/thin/user_agent.py +234 -0
  10. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4.py +2 -2
  11. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_public.py +3 -1
  12. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_query.py +2 -0
  13. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_runs.py +10 -1
  14. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_search_contract.py +54 -3
  15. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/attendance.py +11 -6
  16. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/cloud_auth.py +2 -2
  17. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/clarify.py +10 -5
  18. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/login.py +7 -2
  19. mostlyright_data-0.31.11/src/mostlyright/data_harness/thin/user_agent.py +0 -76
  20. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/.gitignore +0 -0
  21. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/pyproject.toml +0 -0
  22. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/scripts/hatch_build.py +0 -0
  23. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/SKILL.md +0 -0
  24. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/agents/openai.yaml +0 -0
  25. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  26. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  27. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  28. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  29. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  30. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  31. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  32. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  33. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  34. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/agent-protocol.md +0 -0
  35. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  36. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  37. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/boundaries.md +0 -0
  38. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  39. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/commands.md +0 -0
  40. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  41. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
  42. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/installation-parity.md +0 -0
  43. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/live-run.md +0 -0
  44. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  45. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  46. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  47. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/one-install.md +0 -0
  48. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/patterns.md +0 -0
  49. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/prediction-labels.md +0 -0
  50. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/promote.md +0 -0
  51. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/readers.md +0 -0
  52. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/receipts.md +0 -0
  53. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recipe-test.md +0 -0
  54. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  55. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  56. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/reference-pages.md +0 -0
  57. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/required-protocol.md +0 -0
  58. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/source-credentials.md +0 -0
  59. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/sources.md +0 -0
  60. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  61. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/transforms.md +0 -0
  62. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  63. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  64. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  65. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/__init__.py +0 -0
  66. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  67. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/canonical.py +0 -0
  68. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/formats.py +0 -0
  69. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  70. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/key_seam.py +0 -0
  71. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/page_coverage.py +0 -0
  72. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  73. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/session_probes.py +0 -0
  74. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/skill_assets.py +0 -0
  75. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/table_manifest.py +0 -0
  76. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  77. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  78. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  79. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/activity.py +0 -0
  80. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  81. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/backoff.py +0 -0
  82. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/categories.py +0 -0
  83. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/commands.py +0 -0
  84. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  85. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/delivery.py +0 -0
  86. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
  87. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/download.py +0 -0
  88. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
  89. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  90. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
  91. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  92. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/parity.py +0 -0
  93. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/probe.py +0 -0
  94. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  95. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  96. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/propose.py +0 -0
  97. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  98. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  99. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  100. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  101. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  102. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
  103. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
  104. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/release_check.py +0 -0
  105. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/research.py +0 -0
  106. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  107. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
  108. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  109. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  110. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/support.py +0 -0
  111. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/table_view.py +0 -0
  112. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/transport.py +0 -0
  113. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  114. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  115. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  116. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  117. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  118. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  119. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  120. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
  121. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  122. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  123. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  124. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  125. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  126. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  127. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  128. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  129. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  130. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  131. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  132. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  133. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  134. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  135. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  136. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  137. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  138. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  139. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  140. {mostlyright_data-0.31.11 → mostlyright_data-0.31.13}/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.11
3
+ Version: 0.31.13
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/
@@ -46,6 +46,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
46
46
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
47
47
  switches that off.
48
48
 
49
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
50
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
51
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
52
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
53
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
54
+ switches both off.
55
+
49
56
  ## Sign in
50
57
 
51
58
  ```bash
@@ -31,6 +31,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
31
31
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
32
32
  switches that off.
33
33
 
34
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
35
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
36
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
37
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
38
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
39
+ switches both off.
40
+
34
41
  ## Sign in
35
42
 
36
43
  ```bash
@@ -194,7 +194,46 @@ column names, the descriptions and the planner. Never bind a word that two colum
194
194
  such as "growth", "change" or "best", unless the brief names the metric. "Highest growth"
195
195
  pointed at a revenue column turns "highest growth last 30 days" into a revenue sort.
196
196
 
197
- Studio caps a spec at 128 filter aliases and 64 sort aliases, and `search validate` refuses more.
197
+ ### Declare a floor for every rate
198
+
199
+ A growth, change, ratio or per-unit column ranks nonsense first when its base is tiny. Revenue
200
+ that grew from $5,000 to $80 million is 1,600,000% growth, and a film with three votes has a
201
+ perfect rating. Sorting by such a column shows shells and placeholders, not the rows the reader
202
+ asked for. For each sortable rate column, declare the column it is a rate of, and the smallest
203
+ base that makes the rate meaningful, in `order_floors` (contract 4.54.0, up to 32 entries):
204
+
205
+ ```json
206
+ "order_floors": [
207
+ {"column": "revenue_growth_pct", "base_column": "revenue_prior_year_usd", "min": 10000000},
208
+ {"column": "revenue_cagr_5y_pct", "base_column": "revenue_5y_ago_usd", "min": 10000000},
209
+ {"column": "vote_average", "base_column": "vote_count", "min": 100}
210
+ ]
211
+ ```
212
+
213
+ - `column` is in `sortable_columns` or is a sort alias's column. `base_column` is another numeric
214
+ column in `filterable_columns`. List each base early in `filterable_columns`: a package keeps 64
215
+ filters, role and alias columns first and the rest in spec order, and a base past that cut
216
+ loses its floor. `search validate TABLE --spec` names such a floor in `warnings` under
217
+ `/order_floors/`; treat that warning as a floor that will not apply.
218
+ - `min` is a whole number in the base column's own unit, written without a decimal point or
219
+ exponent: `10000000` on a column in dollars, `10` on a column in millions.
220
+ - Floors are declared on a search revision with `mr-data search configure --spec`, not in the
221
+ recipe's `table.search`, which refuses the member. A revision configured from the recipe's own
222
+ spec carries none, and a new version of a private table keeps them only when you configure the
223
+ revision again. A plan made without the planner, such as a keystroke search, applies no floor.
224
+ - A Studio older than contract 4.54.0 refuses the member as unknown. Configure without it there.
225
+ - When a plan orders by `column`, in either direction, Studio adds `base_column >= min` as a
226
+ filter. The reader sees it in the search summary and can remove it. A query that already filters
227
+ `base_column` keeps its own filter.
228
+ - Prefer the true base (the earlier period's revenue, the vote count) over a size proxy such as
229
+ market cap. When the table has no base column, add one in the transform: it is cheaper than a
230
+ wrong ranking.
231
+ - Choose `min` from the built table with `mr-data query`: the smallest base at which the rate stops
232
+ being dominated by placeholders. State the floor at the start of the base column's
233
+ description, since the planner reads only the first 160 characters of a description.
234
+
235
+ A recipe's spec takes 128 filter aliases and 64 sort aliases, and a search revision 512 and 256.
236
+ `search validate` refuses more.
198
237
  Rank the phrases and keep the top of the list. Put first the words that name the kind of row, the
199
238
  facets users type most, the popularity sorts and the product's own vocabulary. A phrase you leave
200
239
  out maps to nothing. The planner may still recover it from the column names and values, and
@@ -225,8 +264,13 @@ roles. It never sees rows.
225
264
  `planner_detail.reason`. `external_context_not_allowed` means the recipe withholds consent.
226
265
  `private_source` means a source is not public.
227
266
 
228
- Write each column description as if the planner reads it. Say what the column holds, its unit,
229
- and the words people use for it.
267
+ Write each column description as if the planner reads it, because it does, and the page shows it
268
+ to the reader beside results ordered by that column. Say what the column holds, the period it
269
+ covers ("trailing twelve months", "latest fiscal year against the year before", "over the last
270
+ five fiscal years"), its unit, and the words people use for it, abbreviations included
271
+ ("compound annual growth rate, CAGR"). The planner picks the column a query asks about, named
272
+ bare ("revenue") or in a question ("what is the P/E ratio?"), from these descriptions. It reads
273
+ the first 160 characters of each, so lead with what the column measures and its period.
230
274
 
231
275
  ### Name and describe columns for search
232
276
 
@@ -52,6 +52,7 @@ from collections.abc import Callable, Mapping, Sequence
52
52
  from typing import Any
53
53
 
54
54
  from mostlyright.data_harness import skill_assets
55
+ from mostlyright.data_harness.thin import user_agent
55
56
  from mostlyright.data_harness.thin.transport import ThinLaneError
56
57
  from mostlyright.data_harness.thin.vocabulary import (
57
58
  CLASSIFICATION,
@@ -781,6 +782,7 @@ def main(argv: Sequence[str] | None = None) -> int:
781
782
  # not write, it never raises, and `MOSTLYRIGHT_SKILL_AUTOINSTALL=0` turns it off. A skill that
782
783
  # arrived only when somebody knew to ask for it helped exactly the people who did not need it.
783
784
  skill_assets.ensure_installed()
785
+ user_agent.begin_invocation()
784
786
 
785
787
  arguments = list(sys.argv[1:] if argv is None else argv)
786
788
  hosted_requested = _hosted_flag_present(arguments)
@@ -46,6 +46,7 @@ from mostlyright.data_harness.thin.transport import (
46
46
  UrlLibTransport,
47
47
  signed_transfer_url,
48
48
  )
49
+ from mostlyright.data_harness.thin.user_agent import agent_headers
49
50
 
50
51
  #: The contract version every payload below is written against. ``studio_boundary`` pins the
51
52
  #: generated client to the same 3.0.0 and to one OpenAPI source digest; this constant is the thin
@@ -279,6 +280,7 @@ class StudioApiClient:
279
280
  headers = {
280
281
  "Accept": "application/json",
281
282
  CLIENT_HEADER: CLIENT_NAME,
283
+ **agent_headers(),
282
284
  }
283
285
  if extra_headers:
284
286
  headers.update(extra_headers)
@@ -42,6 +42,7 @@ from mostlyright.data_harness.thin.transport import (
42
42
  UrlLibTransport,
43
43
  service_origin,
44
44
  )
45
+ from mostlyright.data_harness.thin.user_agent import agent_headers
45
46
  from mostlyright.data_harness.ux.credentials import ResolvedCloudCredentials
46
47
 
47
48
  #: Cloud's token-exchange route. Held identical to ``hosted_deploy.CLOUD_TOKEN_PATH`` by a test.
@@ -396,7 +397,7 @@ def open_studio_session(
396
397
  status, raw, headers = selected.request(
397
398
  "POST",
398
399
  f"{cloud_url}{CLOUD_TOKEN_PATH}",
399
- {"Accept": "application/json", "x-api-key": credentials.raw_key},
400
+ {"Accept": "application/json", "x-api-key": credentials.raw_key, **agent_headers()},
400
401
  b"",
401
402
  MAX_TOKEN_RESPONSE_BYTES,
402
403
  )
@@ -953,6 +954,7 @@ class CloudClient:
953
954
  headers = {
954
955
  "Accept": "application/json",
955
956
  "Authorization": f"Bearer {self._credentials.raw_key}",
957
+ **agent_headers(),
956
958
  }
957
959
  encoded: bytes | None = None
958
960
  if body is not None:
@@ -49,6 +49,7 @@ from mostlyright.data_harness.thin.session import (
49
49
  stated_sentence,
50
50
  )
51
51
  from mostlyright.data_harness.thin.transport import ThinLaneError, Transport, UrlLibTransport
52
+ from mostlyright.data_harness.thin.user_agent import agent_headers
52
53
 
53
54
  #: Upstream's frame names. The first two are the tick/proof partition; ``end`` is its
54
55
  #: end-of-stream notice.
@@ -593,6 +594,7 @@ class RunEventStream:
593
594
  headers = {
594
595
  "Accept": "text/event-stream",
595
596
  CLIENT_HEADER: CLIENT_NAME,
597
+ **agent_headers(),
596
598
  **session.authorization(),
597
599
  }
598
600
  # Resume in Studio's own cursor space when we have one. `from_seq` is only ever the
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "schema_version": "mostlyright-thin-table-search-contract.v1",
3
- "source_commit": "0dcecc1b8dfdd3f8960497fdfcffeeb72e240abd",
3
+ "source_commit": "5109da1f92eaaf4fc41ed35a03e5d010268d7c7b",
4
4
  "recipe_search_definition": "recipe_search",
5
5
  "$defs": {
6
+ "common.safe_integer": {
7
+ "description": "An integer exactly representable by both Python JSON integers and TypeScript numbers.",
8
+ "type": "integer",
9
+ "minimum": -9007199254740991,
10
+ "maximum": 9007199254740991
11
+ },
6
12
  "evaluation_case": {
7
13
  "type": "object",
8
14
  "additionalProperties": false,
@@ -119,6 +125,14 @@
119
125
  ]
120
126
  },
121
127
  "description": "Keel's recipe reader does not admit long_text_columns yet (4.42.0): a search revision declares it."
128
+ },
129
+ {
130
+ "not": {
131
+ "required": [
132
+ "order_floors"
133
+ ]
134
+ },
135
+ "description": "The recipe readers do not admit order_floors (4.54.0): a search revision declares it."
122
136
  }
123
137
  ]
124
138
  },
@@ -252,6 +266,35 @@
252
266
  "direction"
253
267
  ]
254
268
  },
269
+ "order_floors": {
270
+ "description": "Order floors (4.54.0): for a sortable column whose order means little on rows with a tiny or missing base (a growth rate on almost no revenue, a rating from a handful of votes), the base column and the least value of it at which a row answers that order. When a plan orders by `column`, either direction, and states no predicate on `base_column`, the planner adds `base_column >= min` (origin `parser`), which the search node runs and reports as any predicate. `column` is declared sortable (in `sortable_columns` or a sort alias's, even where a v3 package sorts numbers by default), `base_column` is another declared numeric column in `filterable_columns`, and each `column` has at most one floor. `min` is an integer (a request body carries no fractional or exponent number), in the base column's own unit. Admitted on a search revision; a recipe's `table.search` may not name it yet.",
271
+ "type": "array",
272
+ "maxItems": 32,
273
+ "items": {
274
+ "type": "object",
275
+ "additionalProperties": false,
276
+ "properties": {
277
+ "column": {
278
+ "type": "string",
279
+ "minLength": 1,
280
+ "maxLength": 128
281
+ },
282
+ "base_column": {
283
+ "type": "string",
284
+ "minLength": 1,
285
+ "maxLength": 128
286
+ },
287
+ "min": {
288
+ "$ref": "#/$defs/common.safe_integer"
289
+ }
290
+ },
291
+ "required": [
292
+ "column",
293
+ "base_column",
294
+ "min"
295
+ ]
296
+ }
297
+ },
255
298
  "planner": {
256
299
  "type": "object",
257
300
  "additionalProperties": false,
@@ -0,0 +1,234 @@
1
+ """The identity every first-party request to Mostly Right states.
2
+
3
+ Without it the standard library sends ``Python-urllib/3.x``, which is what every unattended
4
+ script on the internet sends: the CLI's own traffic could not be told apart from anything else in
5
+ a server log, so nothing downstream could measure it, rate-limit it separately, or exempt it.
6
+
7
+ The name follows the convention the two crawler lanes already use -- a product token, a version,
8
+ and the contact segment that says where to reach the people running it -- so an origin and our own
9
+ logs see one family of identities from this deployment rather than one per code path.
10
+
11
+ Two more headers ride beside it on first-party calls only (:func:`agent_headers`): which coding
12
+ agent, if any, is driving this process, and, under the ``mr-data`` command, one id shared by every
13
+ request that command makes. They go where ``X-Mostlyright-Client`` goes and on Cloud's own
14
+ routes, never through the transport, for the reason ``thin.runs.CLIENT_HEADER`` gives: the
15
+ transport also carries signed transfers to storage nobody here owns.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import secrets
22
+ from collections.abc import Mapping
23
+ from functools import cache
24
+ from importlib.metadata import PackageNotFoundError, version
25
+ from typing import NamedTuple
26
+
27
+ #: The contact segment ``hosted_worker.CRAWLER_CONTACT_SEGMENT`` states for the keyed-source
28
+ #: agents and ``hosted_crawler_fetch.PUBLIC_HTTPS_USER_AGENT`` carries for public sources, with
29
+ #: the measurement that makes it load-bearing recorded at the first of those.
30
+ #:
31
+ #: It is spelled again here rather than imported because both of those modules import ``pyarrow``
32
+ #: at module scope and this package installs with no compiled extension at all -- and they are
33
+ #: Studio's now, so importing them is not a choice this repository has. The spellings are held
34
+ #: against each other by ``tests/test_thin_user_agent.py`` under
35
+ #: ``make verify-cross-repo STUDIO_ROOT=...``, which reads the other one out of Studio's source.
36
+ CRAWLER_CONTACT_SEGMENT = "(+https://mostlyright.md/crawler; crawler@mostlyright.md)"
37
+
38
+ #: The distribution this client is installed as, and the product token it therefore reports.
39
+ DISTRIBUTION_NAME = "mostlyright-data"
40
+
41
+ #: The version reported from a source tree, where no distribution metadata exists to read. A
42
+ #: number rather than a word, so every value this header can carry parses the same way.
43
+ UNINSTALLED_VERSION = "0.0.0"
44
+
45
+ _USER_AGENT_HEADER = "User-Agent"
46
+
47
+ #: The header naming the agent driving this process, one of :data:`AGENTS`.
48
+ AGENT_HEADER = "X-Mostlyright-Agent"
49
+
50
+ #: The header carrying :func:`invocation_id`, sent only after :func:`begin_invocation`.
51
+ INVOCATION_HEADER = "X-Mostlyright-Invocation"
52
+
53
+ #: Set to anything but ``0`` or empty, and neither header above is sent.
54
+ NO_AGENT_HEADERS_ENV = "MOSTLYRIGHT_NO_AGENT_HEADERS"
55
+
56
+ AGENT_CLAUDE_CODE = "claude-code"
57
+ AGENT_CODEX = "codex"
58
+ AGENT_GEMINI_CLI = "gemini-cli"
59
+ AGENT_CURSOR = "cursor"
60
+ AGENT_CI = "ci"
61
+ AGENT_NONE = "none"
62
+
63
+
64
+ class AgentMarker(NamedTuple):
65
+ """One variable an agent host exports into the commands it runs.
66
+
67
+ ``conversation`` says whether the attendance check (``ux.attendance``) reads the variable as
68
+ a conversation existing. Only a marker every headless run of that host lacks may say so:
69
+ ``CODEX_SANDBOX`` is set by ``codex exec`` on a cron job too, so it names Codex here and is
70
+ not read as a person being there.
71
+ """
72
+
73
+ agent: str
74
+ variable: str
75
+ conversation: bool
76
+
77
+
78
+ #: Every marker, grouped by agent in precedence order.
79
+ #:
80
+ #: Presence of a non-empty value is the signal and the value is never read: these are the hosts'
81
+ #: variables, and their values are theirs to change. A coding agent outranks ``ci``, so
82
+ #: ``claude -p`` inside a GitHub Action reports ``claude-code``. Nothing that every terminal of an
83
+ #: editor exports is listed, because a person typing there would read as the agent.
84
+ AGENT_MARKERS: tuple[AgentMarker, ...] = (
85
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDECODE", True),
86
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDE_CODE_SESSION", True),
87
+ AgentMarker(AGENT_CODEX, "CODEX_SESSION", True),
88
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX", False),
89
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX_NETWORK_DISABLED", False),
90
+ AgentMarker(AGENT_GEMINI_CLI, "GEMINI_CLI", False),
91
+ AgentMarker(AGENT_CURSOR, "CURSOR_AGENT", False),
92
+ AgentMarker(AGENT_CI, "CI", False),
93
+ AgentMarker(AGENT_CI, "GITHUB_ACTIONS", False),
94
+ )
95
+
96
+ #: Every value :data:`AGENT_HEADER` can carry.
97
+ AGENTS: tuple[str, ...] = (*dict.fromkeys(marker.agent for marker in AGENT_MARKERS), AGENT_NONE)
98
+
99
+ #: What :func:`invocation_id` returns: 16 random bytes as 32 lowercase hex digits.
100
+ INVOCATION_ID_PATTERN = r"^[0-9a-f]{32}$"
101
+
102
+ _invocation: str | None = None
103
+
104
+
105
+ @cache
106
+ def client_user_agent() -> str:
107
+ """Return the ``User-Agent`` this client presents to Mostly Right.
108
+
109
+ Cached because the answer is a property of the installed environment, and reading distribution
110
+ metadata walks ``sys.path``; a command that makes twenty calls would otherwise pay for it
111
+ twenty times.
112
+ """
113
+
114
+ try:
115
+ installed = version(DISTRIBUTION_NAME)
116
+ except PackageNotFoundError:
117
+ installed = UNINSTALLED_VERSION
118
+ return f"{DISTRIBUTION_NAME}/{installed} {CRAWLER_CONTACT_SEGMENT}"
119
+
120
+
121
+ def with_user_agent(headers: Mapping[str, str]) -> dict[str, str]:
122
+ """Return ``headers`` as a new dict carrying this client's identity.
123
+
124
+ A caller that stated its own ``User-Agent`` keeps it, under whatever spelling it used: HTTP
125
+ header names are case-insensitive, so adding a second one under a different capitalization
126
+ would send two identities rather than override one.
127
+ """
128
+
129
+ resolved = dict(headers)
130
+ if any(name.lower() == "user-agent" for name in resolved):
131
+ return resolved
132
+ resolved[_USER_AGENT_HEADER] = client_user_agent()
133
+ return resolved
134
+
135
+
136
+ def agent_env(agent: str) -> tuple[str, ...]:
137
+ """Return the variables that name ``agent``; empty for ``none`` and for a name not listed."""
138
+
139
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.agent == agent)
140
+
141
+
142
+ def conversation_env() -> tuple[str, ...]:
143
+ """Return the markers the attendance check reads as a conversation existing."""
144
+
145
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.conversation)
146
+
147
+
148
+ def first_set(names: tuple[str, ...], environ: Mapping[str, str] | None = None) -> str | None:
149
+ """Return the first of ``names`` set to a non-empty value, or ``None``."""
150
+
151
+ table = os.environ if environ is None else environ
152
+ return next((name for name in names if (table.get(name) or "").strip()), None)
153
+
154
+
155
+ def detect_agent(environ: Mapping[str, str] | None = None) -> str:
156
+ """Return the agent driving this process, one of :data:`AGENTS`."""
157
+
158
+ table = os.environ if environ is None else environ
159
+ for marker in AGENT_MARKERS:
160
+ if (table.get(marker.variable) or "").strip():
161
+ return marker.agent
162
+ return AGENT_NONE
163
+
164
+
165
+ def begin_invocation() -> str:
166
+ """Draw a fresh invocation id for the command starting now, and return it.
167
+
168
+ Called by the ``mr-data`` entry point once per command. A program importing this package
169
+ never calls it, so its requests carry no invocation id rather than one that lasts as long as
170
+ the process does.
171
+ """
172
+
173
+ global _invocation
174
+ _invocation = secrets.token_hex(16)
175
+ return _invocation
176
+
177
+
178
+ def end_invocation() -> None:
179
+ """Forget the current invocation id, so later requests carry none."""
180
+
181
+ global _invocation
182
+ _invocation = None
183
+
184
+
185
+ def invocation_id() -> str | None:
186
+ """Return the current command's id (see :data:`INVOCATION_ID_PATTERN`), or ``None``."""
187
+
188
+ return _invocation
189
+
190
+
191
+ def agent_headers(environ: Mapping[str, str] | None = None) -> dict[str, str]:
192
+ """Return the agent and invocation headers, or none when :data:`NO_AGENT_HEADERS_ENV` is set.
193
+
194
+ The invocation header is present only inside a command :func:`begin_invocation` started. A
195
+ first-party call adds these itself; the transport never does.
196
+ """
197
+
198
+ table = os.environ if environ is None else environ
199
+ if (table.get(NO_AGENT_HEADERS_ENV) or "").strip() not in ("", "0"):
200
+ return {}
201
+ headers = {AGENT_HEADER: detect_agent(table)}
202
+ if _invocation is not None:
203
+ headers[INVOCATION_HEADER] = _invocation
204
+ return headers
205
+
206
+
207
+ __all__ = [
208
+ "AGENTS",
209
+ "AGENT_CI",
210
+ "AGENT_CLAUDE_CODE",
211
+ "AGENT_CODEX",
212
+ "AGENT_CURSOR",
213
+ "AGENT_GEMINI_CLI",
214
+ "AGENT_HEADER",
215
+ "AGENT_MARKERS",
216
+ "AGENT_NONE",
217
+ "CRAWLER_CONTACT_SEGMENT",
218
+ "DISTRIBUTION_NAME",
219
+ "INVOCATION_HEADER",
220
+ "INVOCATION_ID_PATTERN",
221
+ "NO_AGENT_HEADERS_ENV",
222
+ "UNINSTALLED_VERSION",
223
+ "AgentMarker",
224
+ "agent_env",
225
+ "agent_headers",
226
+ "begin_invocation",
227
+ "client_user_agent",
228
+ "conversation_env",
229
+ "detect_agent",
230
+ "end_invocation",
231
+ "first_set",
232
+ "invocation_id",
233
+ "with_user_agent",
234
+ ]
@@ -149,8 +149,8 @@ from mostlyright.data_harness.thin.v4_matching import CONTRACT_PATHS as MATCHING
149
149
  #: ``JOB_INVALID`` outright, which in production was every refresh a collection epoch was offered
150
150
  #: for rather than only the collection runs. This package must not reach a Studio older than the
151
151
  #: commit it pins; ``docs/V4-WORKER-PROTOCOL.md`` states it beside the layout's own ordering rule.
152
- PINNED_V4_OPENAPI_SOURCE_SHA256 = "b8f2ac008a714a12b88516d4c723ef8a7af18100c2678831fbdafd50fc6a5f20"
153
- PINNED_V4_CONTRACT_VERSION = "4.49.0"
152
+ PINNED_V4_OPENAPI_SOURCE_SHA256 = "e7bd4e994b32d62ae00b4d7fc2f8936e4964cd4160b7aa30ae010528ff57b1a8"
153
+ PINNED_V4_CONTRACT_VERSION = "4.54.0"
154
154
 
155
155
  #: The ``schema_version`` every command body that predates 4.14.0 stamps: ``dataset activity``,
156
156
  #: ``table rename``, ``table archive`` and ``dataset archive``. It is the number the 0.25 line
@@ -52,7 +52,7 @@ from mostlyright.data_harness.thin.transport import (
52
52
  UrlLibTransport,
53
53
  service_origin,
54
54
  )
55
- from mostlyright.data_harness.thin.user_agent import with_user_agent
55
+ from mostlyright.data_harness.thin.user_agent import agent_headers, with_user_agent
56
56
  from mostlyright.data_harness.ux.credentials import (
57
57
  CLOUD_URL_ENV,
58
58
  DEFAULT_CLOUD_URL,
@@ -245,6 +245,7 @@ class PublicCatalogClient:
245
245
  {
246
246
  "Accept": accept,
247
247
  CLIENT_HEADER: CLIENT_NAME,
248
+ **agent_headers(),
248
249
  }
249
250
  )
250
251
 
@@ -960,6 +961,7 @@ class PublicQueryClient(CloudClient):
960
961
  headers = {
961
962
  "Accept": "application/json",
962
963
  "Authorization": f"Bearer {self._credentials.raw_key}",
964
+ **agent_headers(),
963
965
  }
964
966
  status, raw, response_headers = self._transport.request(
965
967
  "GET",
@@ -75,6 +75,7 @@ from mostlyright.data_harness.thin.transport import (
75
75
  Transport,
76
76
  UrlLibTransport,
77
77
  )
78
+ from mostlyright.data_harness.thin.user_agent import agent_headers
78
79
  from mostlyright.data_harness.thin.v4 import (
79
80
  GET_RUN_PATH,
80
81
  GET_RUN_QUERY_PATH,
@@ -329,6 +330,7 @@ class CloudRunQueryClient(CloudClient):
329
330
  headers = {
330
331
  "Accept": "application/json",
331
332
  "Authorization": f"Bearer {self._credentials.raw_key}",
333
+ **agent_headers(),
332
334
  }
333
335
  encoded: bytes | None = None
334
336
  if body is not None:
@@ -1494,7 +1494,16 @@ def _retry_refusal_step(run: Mapping[str, Any]) -> str:
1494
1494
  #: A source that kept refusing. Studio admits a person's named retry of these once its wait has
1495
1495
  #: passed and answers ``RUN_RETRY_TOO_SOON`` with a Retry-After before then (planning #1234), so
1496
1496
  #: this command sends them and lets Studio say when.
1497
- PERSON_RETRY_CODES = frozenset({"SOURCE_RATE_LIMITED", "SOURCE_HTTP_5XX", "SOURCE_PROXY_BUSY"})
1497
+ PERSON_RETRY_CODES = frozenset(
1498
+ {
1499
+ "SOURCE_RATE_LIMITED",
1500
+ "SOURCE_HTTP_5XX",
1501
+ "SOURCE_PROXY_BUSY",
1502
+ "TRANSPORT_FAILURE",
1503
+ "TOTAL_TIMEOUT",
1504
+ "DNS_TEMPORARY_FAILURE",
1505
+ }
1506
+ )
1498
1507
 
1499
1508
  #: Failure codes a named retry may take without a recipe change. A hand copy of Studio's
1500
1509
  #: ``agent_recovery`` retry and platform sets. The server refuses ``retry_of_run_id`` for anything
@@ -54,10 +54,57 @@ def _recipe_search_definition(recipe: dict[str, Any]) -> dict[str, Any]:
54
54
  return localize(recipe["$defs"]["table_plan"]["properties"]["search"])
55
55
 
56
56
 
57
- def build_bundle(table_search: dict[str, Any], recipe: dict[str, Any], pin: dict[str, Any]) -> dict:
58
- """The closure of the definitions the client checks, from the vendored documents."""
57
+ #: A reference into the common schema is carried into the bundle as a local definition named
58
+ #: with this prefix, so the bundle stays one self-contained document.
59
+ COMMON_REF = "common.schema.json#/$defs/"
60
+ COMMON_PREFIX = "common."
59
61
 
60
- definitions = table_search["$defs"]
62
+
63
+ def _localize_common(value: Any) -> Any:
64
+ """``value`` with every common-schema reference pointing at its bundled local copy."""
65
+
66
+ if isinstance(value, list):
67
+ return [_localize_common(item) for item in value]
68
+ if not isinstance(value, dict):
69
+ return value
70
+ result = {}
71
+ for key, item in value.items():
72
+ if key == "$ref" and isinstance(item, str) and item.startswith(COMMON_REF):
73
+ result[key] = "#/$defs/" + COMMON_PREFIX + item[len(COMMON_REF) :]
74
+ else:
75
+ result[key] = _localize_common(item)
76
+ return result
77
+
78
+
79
+ def build_bundle(
80
+ table_search: dict[str, Any],
81
+ recipe: dict[str, Any],
82
+ pin: dict[str, Any],
83
+ common: dict[str, Any] | None = None,
84
+ ) -> dict:
85
+ """The closure of the definitions the client checks, from the vendored documents. A
86
+ definition the search schema takes from the common schema is copied in under
87
+ :data:`COMMON_PREFIX`."""
88
+
89
+ def within_common(value: Any) -> Any:
90
+ """A common definition with its own local references pointing at their copies."""
91
+
92
+ if isinstance(value, list):
93
+ return [within_common(item) for item in value]
94
+ if not isinstance(value, dict):
95
+ return value
96
+ return {
97
+ key: (
98
+ "#/$defs/" + COMMON_PREFIX + _ref_name(item)
99
+ if key == "$ref" and isinstance(item, str) and item.startswith("#/$defs/")
100
+ else within_common(item)
101
+ )
102
+ for key, item in value.items()
103
+ }
104
+
105
+ definitions = dict(table_search["$defs"])
106
+ for name, definition in ((common or {}).get("$defs") or {}).items():
107
+ definitions[COMMON_PREFIX + name] = within_common(definition)
61
108
  recipe_member = "recipe_search"
62
109
  wanted = [SPEC, recipe_member, CASES]
63
110
  selected: dict[str, Any] = {recipe_member: _recipe_search_definition(recipe)}
@@ -66,6 +113,8 @@ def build_bundle(table_search: dict[str, Any], recipe: dict[str, Any], pin: dict
66
113
  if isinstance(value, dict):
67
114
  ref = value.get("$ref")
68
115
  if isinstance(ref, str):
116
+ if ref.startswith(COMMON_REF):
117
+ ref = "#/$defs/" + COMMON_PREFIX + ref[len(COMMON_REF) :]
69
118
  name = _ref_name(ref)
70
119
  if name not in selected:
71
120
  selected[name] = definitions[name]
@@ -81,6 +130,7 @@ def build_bundle(table_search: dict[str, Any], recipe: dict[str, Any], pin: dict
81
130
  if name not in selected:
82
131
  selected[name] = definitions[name]
83
132
  collect(definitions[name])
133
+ selected = {name: _localize_common(definition) for name, definition in selected.items()}
84
134
  return {
85
135
  "schema_version": BUNDLE_SCHEMA_VERSION,
86
136
  "source_commit": pin["source_commit"],
@@ -95,6 +145,7 @@ def bundle_from_vendor(vendor: Path = VENDOR) -> dict[str, Any]:
95
145
  json.loads((schemas / "table-search.schema.json").read_text(encoding="utf-8")),
96
146
  json.loads((schemas / "recipe.schema.json").read_text(encoding="utf-8")),
97
147
  json.loads((vendor / "client-v4-pin.json").read_text(encoding="utf-8")),
148
+ json.loads((schemas / "common.schema.json").read_text(encoding="utf-8")),
98
149
  )
99
150
 
100
151