mostlyright-data 0.28.0__tar.gz → 0.29.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/PKG-INFO +1 -1
  2. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/pyproject.toml +1 -1
  3. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/SKILL.md +14 -1
  4. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/commands.md +7 -0
  5. mostlyright_data-0.29.0/skills/mr-data-build/references/preparing-table-search.md +143 -0
  6. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/table_manifest.py +13 -1
  7. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/router.py +7 -0
  8. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4.py +13 -2
  9. mostlyright_data-0.29.0/src/mostlyright/data_harness/thin/v4_search.py +279 -0
  10. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/vocabulary.py +5 -0
  11. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/.gitignore +0 -0
  12. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/README.md +0 -0
  13. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/scripts/hatch_build.py +0 -0
  14. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/agents/openai.yaml +0 -0
  15. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  16. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  17. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  18. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  19. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  20. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  21. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  22. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  23. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  24. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/agent-protocol.md +0 -0
  25. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  26. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  27. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/boundaries.md +0 -0
  28. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  29. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  30. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/installation-parity.md +0 -0
  31. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/live-run.md +0 -0
  32. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  33. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  34. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  35. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/one-install.md +0 -0
  36. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/prediction-labels.md +0 -0
  37. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/promote.md +0 -0
  38. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/readers.md +0 -0
  39. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/receipts.md +0 -0
  40. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  41. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  42. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/reference-pages.md +0 -0
  43. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/required-protocol.md +0 -0
  44. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/source-credentials.md +0 -0
  45. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/sources.md +0 -0
  46. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  47. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/transforms.md +0 -0
  48. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  49. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  50. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  51. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/__init__.py +0 -0
  52. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  53. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/canonical.py +0 -0
  54. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/formats.py +0 -0
  55. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  56. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/key_seam.py +0 -0
  57. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/page_coverage.py +0 -0
  58. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  59. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/session_probes.py +0 -0
  60. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/skill_assets.py +0 -0
  61. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  62. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  63. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  64. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/activity.py +0 -0
  65. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  66. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/categories.py +0 -0
  67. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/commands.py +0 -0
  68. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  69. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/download.py +0 -0
  70. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  71. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  72. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/parity.py +0 -0
  73. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/probe.py +0 -0
  74. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  75. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  76. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/propose.py +0 -0
  77. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  78. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  79. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  80. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  81. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  82. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/research.py +0 -0
  83. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  84. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/runs.py +0 -0
  85. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/session.py +0 -0
  86. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/stream.py +0 -0
  87. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  88. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  89. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/transport.py +0 -0
  90. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  91. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  92. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  93. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  94. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  95. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  96. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  97. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  98. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  99. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_public.py +0 -0
  100. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  101. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  102. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  103. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  104. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  105. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  106. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  107. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  108. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  109. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  110. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  111. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  112. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  113. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  114. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  115. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  116. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  117. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  118. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/login.py +0 -0
  119. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  120. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  121. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  122. {mostlyright_data-0.28.0 → mostlyright_data-0.29.0}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.28.0
3
+ Version: 0.29.0
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.28.0"
3
+ version = "0.29.0"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -46,6 +46,14 @@ references. Load the reference for the current stage only, not the entire librar
46
46
  5. State the selected sources, joins, coverage and material limitations. Ask for a decision only
47
47
  if existing authorization does not cover it. Read the
48
48
  [recipe reference](references/5-draft-one-recipe-document-one-call.md) before authoring fields.
49
+ For newly authored tables, read [search preparation](references/preparing-table-search.md).
50
+ Check availability with `mr-data search inspect --recipe RECIPE.json --json`; when supported,
51
+ propose/validate and add explicit `table.search` intent before registration. A brief that does
52
+ not mention search treats it as optional while the deployment lacks this capability; when
53
+ supported, default search intent remains. Follow the reference's fallback and decision-note
54
+ rules. If semantic search or JEV interpretation into filters/sorts is explicitly required,
55
+ do not register or queue a recipe that omits it; report the capability limit and wait.
56
+ Do not add search to existing recipes or rebuild existing datasets without an explicit request.
49
57
  Bind `dataset.id`, describe columns, declare checks and units, and register with
50
58
  `mr-data recipe RECIPE.json --json`. Repair all lint findings before queuing work.
51
59
  The server returns the recipe digest; do not compute or invent it.
@@ -133,7 +141,11 @@ merely to obtain a green run. Read [recovery details](references/agent-protocol.
133
141
  repair, which is why the decision belongs in step 5. A table left not refresh-ready never
134
142
  refreshes unless it declares `rebuild`; it does not become ready later.
135
143
  Download and verify artifacts when the user requested bytes.
136
- 10. Report rows and coverage actually verified, checks, limitations and the stable dataset link.
144
+ 10. For new tables with search intent, inspect search readiness and test meaningful queries against
145
+ the observed version as [search preparation](references/preparing-table-search.md) describes.
146
+ Search preparation runs independently: pending or failed indexing never means the data build
147
+ failed, and declared metadata alone never proves observed search quality.
148
+ Report rows and coverage actually verified, checks, limitations and the stable dataset link.
137
149
  Read [delivery](references/9-present-only-what-survived-inspection-with-caveats.md) for details.
138
150
 
139
151
  ## Source-specific references
@@ -151,6 +163,7 @@ merely to obtain a green run. Read [recovery details](references/agent-protocol.
151
163
  | How each source continues, `rebuild`, and the cost of a refresh | [Making a table incremental](references/making-a-table-incremental.md) |
152
164
  | Refresh cadence | [Promotion and cadence](references/promote.md) |
153
165
  | Dataset notes and evidence blocks | [Decision records](references/writing-a-decision-record.md) |
166
+ | Search configuration, example queries and readiness | [Search preparation](references/preparing-table-search.md) |
154
167
  | Exact schemas and worked recipes | [Reference index](references/reference-pages.md) |
155
168
 
156
169
  ## Needs Studio 4.14
@@ -122,3 +122,10 @@ performs. Most of the survivors take a run identifier where the deleted local tw
122
122
  a hosted build has no folder, and the run identifier is what the submission printed and what the
123
123
  dashboard address contains. The deleted names and their replacements are written down only in the
124
124
  Harness repository's own `docs/HOSTED-PARITY.md`, which is not installed beside this document.
125
+
126
+ ## Search preparation
127
+
128
+ `mr-data search <verb>` prepares and tests search on new opted-in tables. See
129
+ [search preparation](preparing-table-search.md) and `mr-data search --help` for the supported
130
+ operations. `inspect`, `propose` and `validate` accept `--recipe FILE` before registration; these
131
+ report declared schema only. Index work runs in Studio and does not hold up data publication.
@@ -0,0 +1,143 @@
1
+ # Prepare search for a newly authored table
2
+
3
+ Use `mr-data search --help` and the relevant verb's help. These tools call Studio; no embedding
4
+ model, database, index or workload credential is installed on the agent's machine.
5
+
6
+ ## Before registration: declared intent
7
+
8
+ For each new table, describe searchable text, exact categorical aliases and meaningful sort terms
9
+ using the table's purpose, column descriptions and source evidence. First check the deployed
10
+ recipe authoring capability with a recipe-target request, before adding `table.search`:
11
+
12
+ ```sh
13
+ mr-data search inspect --recipe recipe.json --json
14
+ ```
15
+
16
+ Local CLI help describes installed commands; it does not prove the deployed server serves them.
17
+ For the recipe-target probe above, read JSON refusals from stderr: `schema_version` is
18
+ `mr-data-error.v1`, `code` is the typed discriminator, and `error` is the detail. There is no
19
+ public `http_status` field in that envelope. Treat `code: THIN_NOT_FOUND` as absent search
20
+ registration support only when `error` begins exactly with
21
+ `Studio has no such resource for /v4/search/recipe-inspection`; the CLI maps this route's HTTP 404
22
+ that way. An explicit `code: THIN_SEARCH_CAPABILITY_UNAVAILABLE` also states a capability limit.
23
+ A generic refusal or a rendered HTTP 405 alone is not this typed evidence; preserve the error
24
+ and resolve it rather than guessing that the deployment lacks search.
25
+
26
+ This rule applies only to `search inspect --recipe`, which needs no table ID. A missing table
27
+ or version on a table-target request is not capability evidence. Neither are authorization
28
+ failures (401/403), rate limits (429), timeouts, invalid response schemas, or invalid recipe
29
+ diagnostics: resolve the actual error. Missing capability metadata is not evidence either.
30
+
31
+ When the agreed brief does not mention search, treat search as optional only while this
32
+ capability is unavailable. When supported, preserve the default of useful search intent for
33
+ new tables. For an optional-search build on an unsupported deployment, omit `table.search`,
34
+ keep the drafted search intent separately alongside the recipe, and record the capability
35
+ limit in the dataset's Sources and decisions note and the matching chat message. Do not put
36
+ this operational limitation into the dataset description, title, or topics. Preserve ordinary
37
+ recipe checks and use supported data inspection tools. Do not emulate search locally, claim
38
+ readiness, or automatically backfill later; adding that retained intent to the resulting table
39
+ requires an explicit request.
40
+
41
+ If the user explicitly requires semantic retrieval or JEV interpretation into filters/sorts,
42
+ do not register or queue a build for any recipe that omits the required search. Report the
43
+ capability limit and wait for backend support. Do not silently downgrade, including an interim
44
+ baseline build. Independent source research and recipe drafting may continue.
45
+
46
+ When inspection is supported, continue with propose and the validate step below; both require
47
+ the deployed search authoring capability:
48
+
49
+ ```sh
50
+ mr-data search propose --recipe recipe.json --json
51
+ ```
52
+
53
+ The replies have `scope=declared_schema`. They show declarations, not observed vocabulary, rows,
54
+ completeness, latency or index quality. Propose does not rewrite the file. Explicitly insert the
55
+ returned spec into the relevant `table.search`, refine it, then validate the resulting recipe:
56
+
57
+ ```sh
58
+ mr-data search validate --recipe recipe-with-search.json --json
59
+ mr-data recipe recipe-with-search.json --json
60
+ ```
61
+
62
+ Repair diagnostics before registration. Keep column descriptions and units in their normal recipe
63
+ fields. Do not invent meanings such as “best” or “largest” when the brief does not identify a
64
+ metric. Keep that wording unresolved, or define separate explicit aliases such as “highest revenue”
65
+ and “most employees”. Never manufacture observed category values from documentation.
66
+
67
+ The search spec uses `schema_version: table-search-spec.v1`, `keyword_columns`, `semantic_columns`,
68
+ `aliases` (`phrase`, `column`, `operator`, `value`), `sort_aliases` (`phrase`, `column`, `direction`),
69
+ and `planner` (`enabled`, `external_context_allowed`). Read current validation diagnostics rather
70
+ than assuming that arbitrary operations, columns or model options are supported.
71
+
72
+ Default to useful keyword columns plus exact filters and sorting. For new tables with descriptive
73
+ prose, include useful descriptions/content/notes in `semantic_columns` for meaning-based retrieval.
74
+ Keep it empty for identifiers, measurements, dates and tables without useful prose. Embeddings run
75
+ locally on the hosted worker with a pinned multilingual model; they do not transmit private table
76
+ text externally. Preparation is budgeted and asynchronous. Never claim readiness from a declaration.
77
+ Semantic-only tables may use an empty `keyword_columns` list; their lexical capability stays
78
+ `unavailable` even after semantic preparation succeeds.
79
+
80
+ JEV is conditional. Exact identifiers, empty input, explicit plans, manual chip edits and already
81
+ resolved simple filters do not need a planner call. Reuse interpretation when its table/spec
82
+ coordinates still match. Only enable planner refinement where it adds meaning and deployed policy
83
+ permits it. `external_context_allowed` must stay false unless the user or established workspace
84
+ policy authorizes sending the relevant context externally; never infer consent from data-read
85
+ permission. Source cells and model responses remain untrusted.
86
+ Queries containing negation or explicit disjunction stay unresolved rather than becoming guessed
87
+ hard predicates. Verify those cases with explicit supported filters.
88
+
89
+ ## Capability availability
90
+
91
+ This CLI release supports inspect, propose, configure, validate, explain, estimate, build, status and cancel.
92
+ Other preparation verbs return `THIN_SEARCH_CAPABILITY_UNAVAILABLE` until the hosted implementation
93
+ is available. Treat that as a capability limit, never as successful preparation. Use the existing
94
+ bounded `peek`/`query` tools for data verification meanwhile; do not emulate semantic search locally.
95
+
96
+ ## After a successful data build: observed evidence
97
+
98
+ Read table and version IDs from receipts. Inspect with `--version VERSION_ID`. Use the existing
99
+ bounded `peek`/`query` tools to verify real categorical values and search expectations. Preserve
100
+ coverage/truncation and exact/sample scope; never report a sampled vocabulary as all categories.
101
+
102
+ Run `search status TABLE_ID --version VERSION_ID`. Recipe completion prepares an index in the
103
+ background for newly opted-in tables. When keyword fields are configured, lexical capability is
104
+ `scan` while no compatible index is ready, or `indexed` when a complete compatible lexical index
105
+ is verified. A complete generation can retain lexical `scan` when full keyword cells exceed the
106
+ index representation budget; use the reported capability rather than inferring it from job success.
107
+ Semantic capability becomes `ready` only with a verified compatible generation covering every
108
+ source row. Preparation resumes across bounded immutable shards; the shard size is a resource
109
+ bound, not a table population limit. Admission or preparation failures preserve the baseline and
110
+ must not be described as partially ready. Data publication continues independently of preparation.
111
+ Use the current estimate and job receipts for resource admission, progress and failure evidence.
112
+
113
+ Meaning-based retrieval searches every prepared shard and returns a bounded relevance window.
114
+ Approximate ranking does not imply an exhaustive similarity ordering or an exact matching count.
115
+ Hard filters apply before ranking; explicit sorting retains its declared meaning. Keep these
116
+ distinctions in verification notes rather than inferring corpus coverage from the result count.
117
+ Do not claim readiness merely because the data build completed, or enqueue duplicate work.
118
+
119
+ Use `search explain TABLE_ID --version VERSION_ID --query '...'` to check exact identity,
120
+ category, numeric boundaries, sort meanings and ambiguous language. Keep a few meaningful checks
121
+ in the dataset's verification notes with actual results from the hosted query tools. Search sample,
122
+ values, cases, preview and evaluation commands are not available in this CLI release.
123
+
124
+ ## Draft revisions
125
+
126
+ For authorized search-only work on a registered table, inspect, configure, validate and explain a
127
+ draft revision using returned immutable coordinates. Configure requires `--version`, `--spec`
128
+ and `--expected-revision`; `none` denotes an explicitly empty draft selection. Metadata exposes the
129
+ selected draft coordinate for the next compare-and-swap. Configure creates a draft and does not
130
+ activate an index or change data publication. Use an explicit `--idempotency-key` when retrying the
131
+ same configuration; never change its body under that key.
132
+
133
+ For explicitly requested preparation, `search build TABLE_ID --version VERSION_ID` queues the
134
+ immutable opted-in version's own recipe configuration, then activates only complete compatible
135
+ coverage automatically. It cannot build or activate a draft revision. Follow its receipt with
136
+ `search status --job JOB_ID`; pending work exits with code 3. `search cancel JOB_ID` revokes search
137
+ work only; it does not undo an already successful generation. Activation and rollback commands
138
+ remain unavailable. Do not compute authoritative digests or bypass server resource admission. Preserve
139
+ unresolved choices for the user; reinterpretation cannot silently replace manual edits or broaden
140
+ an exact filter.
141
+
142
+ This workflow opts in newly authored recipes only. It does not migrate old datasets, reauthor users'
143
+ existing recipes, launch a full data rebuild or imply successful indexing of every table.
@@ -56,6 +56,7 @@ __all__ = [
56
56
  "COMPRESSION",
57
57
  "DROPPED_ALL",
58
58
  "MANIFEST_MERGE_KINDS",
59
+ "MANIFEST_PARTITION_ENCODINGS",
59
60
  "MAX_MANIFEST_BYTES",
60
61
  "MAX_PARTS_PER_RUN",
61
62
  "MAX_PARTS_PER_VERSION",
@@ -94,6 +95,15 @@ TABLE_MANIFEST_SCHEMA = "mostlyright-table-manifest.v1"
94
95
  TABLE_MANIFEST_SCHEMA_V2 = "mostlyright-table-manifest.v2"
95
96
  _SUPPORTED_MANIFEST_SCHEMAS = frozenset((TABLE_MANIFEST_SCHEMA, TABLE_MANIFEST_SCHEMA_V2))
96
97
 
98
+ #: The closed spellings a v2 manifest may bind a partition field to: exactly the ones Studio's
99
+ #: table sink writes and Keel's seal binds, and exactly the recipe's ``table.partition.encoding``
100
+ #: vocabulary. A reader narrower than the writers refuses a table the service sealed, which is how
101
+ #: ``iso_month_prefix`` tables read as TABLE_MANIFEST_INVALID on 0.28.0 (planning#807).
102
+ #: ``tests/test_thin_table_manifest_encodings.py`` holds this equal to the vendored recipe schema.
103
+ MANIFEST_PARTITION_ENCODINGS: frozenset[str] = frozenset(
104
+ {"value", "iso_date_prefix", "iso_date_value", "iso_month_prefix"}
105
+ )
106
+
97
107
  #: The two media types the format adds to the closed artifact vocabulary.
98
108
  TABLE_MANIFEST_MEDIA_TYPE = "application/vnd.mostlyright.table-manifest+json"
99
109
  TABLE_PART_MEDIA_TYPE = "application/vnd.apache.parquet"
@@ -744,8 +754,10 @@ class TableManifest:
744
754
  raise _refuse(
745
755
  "TABLE_MANIFEST_INVALID", "a v2 manifest binds every partition encoding"
746
756
  )
757
+ # ``isinstance`` first: a JSON array or object is unhashable, and a set lookup on one
758
+ # would escape as a TypeError rather than as this refusal.
747
759
  if any(
748
- value not in {"value", "iso_date_prefix", "iso_date_value"}
760
+ not isinstance(value, str) or value not in MANIFEST_PARTITION_ENCODINGS
749
761
  for value in self.partition_encodings.values()
750
762
  ):
751
763
  raise _refuse("TABLE_MANIFEST_INVALID", "a v2 partition encoding is not closed")
@@ -125,6 +125,7 @@ HOSTED_SUMMARY = dict(
125
125
  ("run", "start one run against a registered recipe, under the ceilings you give it"),
126
126
  ("status", "report what one run is doing, what it delivered, and why it failed"),
127
127
  ("runs", "report this workspace's own runs, narrowed by status and by mode"),
128
+ ("search", "prepare, test and manage versioned table search"),
128
129
  ("query", "ask one bounded read-only question of the table one run produced"),
129
130
  ("verify", "check one run's sealed table against its manifest, part by part with --deep"),
130
131
  (
@@ -282,6 +283,7 @@ def _hosted_parser() -> argparse.ArgumentParser:
282
283
  v4_query,
283
284
  v4_reader,
284
285
  v4_runs,
286
+ v4_search,
285
287
  v4_stream,
286
288
  )
287
289
 
@@ -300,6 +302,7 @@ def _hosted_parser() -> argparse.ArgumentParser:
300
302
  # Declared in their own modules, and `cli`'s parser calls the same two functions, so the two
301
303
  # front doors cannot disagree about an argument.
302
304
  v4_query.declare_arguments(_add("query"))
305
+ v4_search.declare_arguments(_add("search"))
303
306
  probe_module.declare_arguments(_add("probe"))
304
307
  v4_reader.declare_reader_arguments(_add("reader"))
305
308
  v4_reader.declare_source_arguments(_add("source"))
@@ -433,6 +436,7 @@ def _handlers() -> dict[str, Callable[[argparse.Namespace], dict[str, Any]]]:
433
436
  v4_query,
434
437
  v4_reader,
435
438
  v4_runs,
439
+ v4_search,
436
440
  v4_secrets,
437
441
  v4_stream,
438
442
  v4_tables,
@@ -453,6 +457,7 @@ def _handlers() -> dict[str, Callable[[argparse.Namespace], dict[str, Any]]]:
453
457
  "run": v4_runs.start,
454
458
  "runs": v4_runs.runs,
455
459
  "query": v4_query.query,
460
+ "search": v4_search.search,
456
461
  # ⚠ THE ONE HANDLER STILL ON THE V3 WIRE, and deliberately. There is no v4 research-session
457
462
  # surface; the owner ruling keeps the v3 session routes mounted-but-frozen and makes
458
463
  # `probe` one command over them, with the migration scheduled after this milestone.
@@ -523,6 +528,7 @@ def _exit_codes() -> dict[str, Callable[[dict[str, Any]], int]]:
523
528
  v4_datasets,
524
529
  v4_query,
525
530
  v4_runs,
531
+ v4_search,
526
532
  v4_stream,
527
533
  v4_tables,
528
534
  )
@@ -534,6 +540,7 @@ def _exit_codes() -> dict[str, Callable[[dict[str, Any]], int]]:
534
540
  "checks": v4_artifacts.checks_exit_code,
535
541
  "verify": v4_artifacts.verify_exit_code,
536
542
  "query": v4_query.query_exit_code,
543
+ "search": v4_search.search_exit_code,
537
544
  "probe": probe_module.probe_exit_code,
538
545
  "table": v4_tables.lifecycle_exit_code,
539
546
  "demote": v4_tables.lifecycle_exit_code,
@@ -147,8 +147,8 @@ from mostlyright.data_harness.thin.transport import ThinLaneError
147
147
  #: ``JOB_INVALID`` outright, which in production was every refresh a collection epoch was offered
148
148
  #: for rather than only the collection runs. This package must not reach a Studio older than the
149
149
  #: commit it pins; ``docs/V4-WORKER-PROTOCOL.md`` states it beside the layout's own ordering rule.
150
- PINNED_V4_OPENAPI_SOURCE_SHA256 = "807ac03fba8d3907429ba0f56eb52c23e52a56609e9c38092c010a597aeeb9bf"
151
- PINNED_V4_CONTRACT_VERSION = "4.17.0"
150
+ PINNED_V4_OPENAPI_SOURCE_SHA256 = "d4aad481488ae29e53575024baf9028e9941cd8235f58e91c7f1bcaccfdf4968"
151
+ PINNED_V4_CONTRACT_VERSION = "4.18.0"
152
152
 
153
153
  #: The ``schema_version`` every command body that predates 4.14.0 stamps: ``dataset activity``,
154
154
  #: ``table rename``, ``table archive`` and ``dataset archive``. It is the number the 0.25 line
@@ -815,6 +815,17 @@ DECLARED_V4_PATHS: frozenset[str] = frozenset(
815
815
  EXPLAIN_RECIPE_REFRESH_PATH,
816
816
  RECIPE_FORECAST_PATH,
817
817
  PLATFORM_CREDENTIALS_PATH,
818
+ # Search authoring routes introduced with new-table opt-in search preparation.
819
+ "/v4/search/recipe-inspection",
820
+ "/v4/tables/{table_id}/search",
821
+ "/v4/tables/{table_id}/search/revisions",
822
+ "/v4/tables/{table_id}/search/jobs",
823
+ "/v4/search/jobs/{job_id}",
824
+ "/v4/search/jobs/{job_id}/cancel",
825
+ *(
826
+ f"/v4/tables/{{table_id}}/search/{verb}"
827
+ for verb in ("inspect", "propose", "validate", "explain", "estimate")
828
+ ),
818
829
  }
819
830
  )
820
831
 
@@ -0,0 +1,279 @@
1
+ """Hosted search preparation. Studio owns interpretation, indexes and activation authority."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ from mostlyright.data_harness.thin import THIN_SCHEMA_PREFIX
11
+ from mostlyright.data_harness.thin.recipe import read_document
12
+ from mostlyright.data_harness.thin.session import open_studio_session
13
+ from mostlyright.data_harness.thin.transport import ThinLaneError
14
+ from mostlyright.data_harness.thin.v4 import StudioV4Client, identifier
15
+ from mostlyright.data_harness.ux.credentials import resolve_cloud_credentials
16
+
17
+ COMMAND_HELP = "prepare, test and manage versioned table search"
18
+ VERBS = (
19
+ "inspect",
20
+ "sample",
21
+ "values",
22
+ "propose",
23
+ "configure",
24
+ "validate",
25
+ "add-cases",
26
+ "explain",
27
+ "preview",
28
+ "evaluate",
29
+ "estimate",
30
+ "build",
31
+ "status",
32
+ "activate",
33
+ "cancel",
34
+ "rollback",
35
+ )
36
+ RECIPE_VERBS = frozenset({"inspect", "propose", "validate"})
37
+
38
+
39
+ def declare_arguments(parser: argparse.ArgumentParser) -> None:
40
+ parser.description = (
41
+ "Prepare search through Studio. No local model or index runs. Inspect/propose/validate "
42
+ "accept --recipe FILE before registration. Recipe inspection is "
43
+ "declared-schema evidence; readiness requires built data. Builds return promptly with job "
44
+ "coordinates. Unsupported preparation operations return a capability-unavailable error."
45
+ )
46
+ subparsers = parser.add_subparsers(
47
+ dest="search_operation", required=True, help="the search preparation operation"
48
+ )
49
+ for verb in VERBS:
50
+ sub = subparsers.add_parser(verb)
51
+ sub.add_argument("--json", action="store_true", default=argparse.SUPPRESS)
52
+ sub.add_argument("target", nargs="?", help="table ID, or job ID for cancel")
53
+ if verb in RECIPE_VERBS:
54
+ sub.add_argument(
55
+ "--recipe", type=Path, help="local recipe submitted without editing it"
56
+ )
57
+ if verb != "cancel":
58
+ sub.add_argument("--version", dest="table_version_id")
59
+ if verb not in {"configure", "status", "build"}:
60
+ sub.add_argument("--revision", dest="search_revision_id")
61
+ if verb in {"sample", "values", "preview"}:
62
+ sub.add_argument("--limit", type=int, default=20)
63
+ if verb == "sample":
64
+ sub.add_argument("--columns", help="comma-separated projected columns")
65
+ if verb == "values":
66
+ sub.add_argument("--column", required=True)
67
+ sub.add_argument("--prefix")
68
+ if verb == "configure":
69
+ sub.add_argument("--spec", type=Path, required=True)
70
+ sub.add_argument("--expected-revision", required=True, help="revision ID or none")
71
+ if verb == "add-cases":
72
+ sub.add_argument("--cases", type=Path, required=True)
73
+ if verb in {"explain", "preview"}:
74
+ group = sub.add_mutually_exclusive_group(required=True)
75
+ group.add_argument("--query")
76
+ if verb == "preview":
77
+ group.add_argument("--plan", type=Path)
78
+ if verb == "evaluate":
79
+ sub.add_argument("--cases-revision", dest="cases_revision_id")
80
+ if verb == "status":
81
+ sub.add_argument("--job", dest="job_id")
82
+ if verb in {"activate", "rollback"}:
83
+ sub.add_argument("--generation", dest="generation_id")
84
+ sub.add_argument("--expected-active", required=True, help="active revision ID or none")
85
+ if verb in {
86
+ "configure",
87
+ "add-cases",
88
+ "evaluate",
89
+ "activate",
90
+ "rollback",
91
+ }:
92
+ sub.add_argument(
93
+ "--idempotency-key", help="reuse this key when retrying the same write"
94
+ )
95
+
96
+
97
+ def _required(value: Any, name: str) -> Any:
98
+ if value is None or value == "":
99
+ raise ThinLaneError("THIN_ARGUMENT_INVALID", f"search requires {name}")
100
+ return value
101
+
102
+
103
+ def search_exit_code(payload: dict[str, Any]) -> int:
104
+ """Stable gate outcomes: 0 complete, 1 failed, 3 incomplete, 4 ambiguous."""
105
+ result = payload.get("result", {})
106
+ if not isinstance(result, dict):
107
+ return 1
108
+ if result.get("valid") is False or result.get("status") in {"failed", "cancelled", "invalid"}:
109
+ return 1
110
+ if result.get("status") in {"ambiguous", "clarification_required"}:
111
+ return 4
112
+ if result.get("status") in {
113
+ "queued",
114
+ "dispatched",
115
+ "running",
116
+ "pending",
117
+ "cancellation_requested",
118
+ }:
119
+ return 3
120
+ estimate = result.get("estimate")
121
+ plan = result.get("plan")
122
+ if estimate is not None and not isinstance(estimate, dict):
123
+ return 1
124
+ if plan is not None and not isinstance(plan, dict):
125
+ return 1
126
+ if isinstance(estimate, dict) and estimate.get("admitted") is False:
127
+ return 3
128
+ if isinstance(plan, dict) and plan.get("unresolved"):
129
+ return 4
130
+ return 0
131
+
132
+
133
+ SEARCH_JOB_SCHEMAS = {"table-search-job.v1", "table-search-job.v2"}
134
+
135
+ RECIPE_INSPECTION_PATH = "/v4/search/recipe-inspection"
136
+ TABLE_SEARCH_PATH = "/v4/tables/{table_id}/search"
137
+ TABLE_AUTHORING_PATHS = {
138
+ verb: TABLE_SEARCH_PATH + "/" + verb
139
+ for verb in ("inspect", "propose", "validate", "explain", "estimate")
140
+ }
141
+ TABLE_REVISIONS_PATH = TABLE_SEARCH_PATH + "/revisions"
142
+ TABLE_JOBS_PATH = TABLE_SEARCH_PATH + "/jobs"
143
+ JOB_STATUS_PATH = "/v4/search/jobs/{job_id}"
144
+ JOB_CANCEL_PATH = JOB_STATUS_PATH + "/cancel"
145
+ SEARCH_PATHS = frozenset(
146
+ {
147
+ RECIPE_INSPECTION_PATH,
148
+ TABLE_SEARCH_PATH,
149
+ TABLE_REVISIONS_PATH,
150
+ TABLE_JOBS_PATH,
151
+ JOB_STATUS_PATH,
152
+ JOB_CANCEL_PATH,
153
+ *TABLE_AUTHORING_PATHS.values(),
154
+ }
155
+ )
156
+
157
+
158
+ def search(args: argparse.Namespace, *, client: StudioV4Client | None = None) -> dict[str, Any]:
159
+ """Forward bounded declared commands; never invent an index or successful capability."""
160
+ verb = args.search_operation
161
+ recipe = getattr(args, "recipe", None)
162
+ target = getattr(args, "target", None)
163
+ if recipe is not None:
164
+ if target is not None or any(
165
+ getattr(args, member, None) is not None
166
+ for member in ("table_version_id", "search_revision_id")
167
+ ):
168
+ raise ThinLaneError(
169
+ "THIN_ARGUMENT_INVALID", "--recipe cannot be mixed with table coordinates"
170
+ )
171
+ document = read_document(recipe)
172
+ method, path = "POST", RECIPE_INSPECTION_PATH
173
+ body = {"operation": verb, "recipe": document}
174
+ expected = (200,)
175
+ headers = None
176
+ elif verb == "cancel" or (verb == "status" and getattr(args, "job_id", None)):
177
+ if verb == "status" and (target is not None or getattr(args, "table_version_id", None)):
178
+ raise ThinLaneError("THIN_ARGUMENT_INVALID", "--job cannot mix table coordinates")
179
+ job_id = identifier(
180
+ _required(target if verb == "cancel" else args.job_id, "job ID"), "job ID"
181
+ )
182
+ method = "POST" if verb == "cancel" else "GET"
183
+ path = (JOB_CANCEL_PATH if verb == "cancel" else JOB_STATUS_PATH).format(job_id=job_id)
184
+ body, headers, expected = {}, None, (200,)
185
+ else:
186
+ table_id = identifier(
187
+ _required(target, "a table identifier or --recipe"), "table identifier"
188
+ )
189
+ body = {}
190
+ for member in ("table_version_id", "search_revision_id"):
191
+ value = getattr(args, member, None)
192
+ if value is not None:
193
+ body[member] = identifier(value, member)
194
+ headers = None
195
+ expected = (200,)
196
+ method = "POST"
197
+ if verb in TABLE_AUTHORING_PATHS:
198
+ path = TABLE_AUTHORING_PATHS[verb].format(table_id=table_id)
199
+ if verb == "explain":
200
+ query = _required(getattr(args, "query", None), "--query")
201
+ if len(query) > 512:
202
+ raise ThinLaneError(
203
+ "THIN_ARGUMENT_INVALID", "search query exceeds 512 characters"
204
+ )
205
+ body["query"] = query
206
+ elif verb == "configure":
207
+ _required(body.get("table_version_id"), "--version")
208
+ body["spec"] = read_document(args.spec)
209
+ revision = args.expected_revision
210
+ body["expected_revision"] = (
211
+ None if revision == "none" else identifier(revision, "revision")
212
+ )
213
+ path = TABLE_REVISIONS_PATH.format(table_id=table_id)
214
+ headers = {"Idempotency-Key": getattr(args, "idempotency_key", None) or str(uuid4())}
215
+ expected = (201,)
216
+ elif verb == "build":
217
+ _required(body.get("table_version_id"), "--version")
218
+ path = TABLE_JOBS_PATH.format(table_id=table_id)
219
+ expected = (202,)
220
+ elif verb == "status":
221
+ method, path = "GET", TABLE_SEARCH_PATH.format(table_id=table_id)
222
+ else:
223
+ raise ThinLaneError(
224
+ "THIN_SEARCH_CAPABILITY_UNAVAILABLE", f"Studio does not yet provide search {verb}"
225
+ )
226
+ selected = client or StudioV4Client(open_studio_session(resolve_cloud_credentials()))
227
+ answer = selected._call(
228
+ method,
229
+ path,
230
+ body=body if method == "POST" else None,
231
+ query=body if method == "GET" else None,
232
+ extra_headers=headers,
233
+ expected=expected,
234
+ )
235
+ if recipe is not None and (
236
+ answer.get("scope") != "declared_schema"
237
+ or answer.get("schema_version") != "table-search-authoring.v1"
238
+ or type(answer.get("valid")) is not bool
239
+ ):
240
+ raise ThinLaneError(
241
+ "THIN_RESPONSE_INVALID", "Studio returned invalid declared-search evidence"
242
+ )
243
+ job_operation = verb == "cancel" or (verb == "status" and getattr(args, "job_id", None))
244
+ if job_operation:
245
+ if (
246
+ answer.get("job_id") != job_id
247
+ or answer.get("status")
248
+ not in {"queued", "dispatched", "running", "succeeded", "failed", "cancelled"}
249
+ or (verb == "status" and answer.get("schema_version") not in SEARCH_JOB_SCHEMAS)
250
+ ):
251
+ raise ThinLaneError(
252
+ "THIN_RESPONSE_INVALID", "Studio returned invalid search job coordinates"
253
+ )
254
+ if recipe is None and not job_operation:
255
+ wanted_schemas = (
256
+ {"table-search-revision.v1"}
257
+ if verb == "configure"
258
+ else SEARCH_JOB_SCHEMAS
259
+ if verb == "build"
260
+ else {"table-search-metadata.v1"}
261
+ )
262
+ if answer.get("schema_version") not in wanted_schemas or answer.get("table_id") != table_id:
263
+ raise ThinLaneError(
264
+ "THIN_RESPONSE_INVALID", "Studio returned invalid table-search coordinates"
265
+ )
266
+ for member in ("table_version_id", "search_revision_id"):
267
+ requested = body.get(member)
268
+ if requested is not None and answer.get(member) != requested:
269
+ raise ThinLaneError(
270
+ "THIN_RESPONSE_INVALID", "Studio returned mismatched search coordinates"
271
+ )
272
+ return {
273
+ "schema_version": f"{THIN_SCHEMA_PREFIX}-v4-search.v1",
274
+ "status": "search_result",
275
+ "headline": f"Search {verb} result.",
276
+ "lane": "hosted",
277
+ "operation": verb,
278
+ "result": answer,
279
+ }
@@ -201,6 +201,11 @@ CLASSIFICATION: dict[str, Lane] = {
201
201
  "surface the MCP anonymous tools call — search, show, tables, schema, sample, "
202
202
  "lookup, fetch, query and download-url — with no device key and no Studio bearer",
203
203
  ),
204
+ "search": Lane(
205
+ HOSTED,
206
+ "it prepares and tests versioned search through Studio; index work runs in the hosted "
207
+ "engine, while draft configuration and activation remain server-authorized",
208
+ ),
204
209
  "query": Lane(
205
210
  HOSTED,
206
211
  "it asks one bounded read-only question of the table one run produced and prints the "