cassis-cli 1.1.0__tar.gz → 1.2.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.
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Polymorph SAS
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ Cassis CLI
2
+ Copyright 2026 Polymorph SAS
3
+
4
+ This product includes software developed at Polymorph SAS (https://getcassis.com).
@@ -1,8 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: cassis-cli
3
+ Version: 1.2.0
4
+ Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
5
+ License: Apache-2.0
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Keywords: cassis,ontology,ci,text-to-sql
9
+ Author: Cassis
10
+ Author-email: tech.admin@getcassis.com
11
+ Requires-Python: >=3.10,<4.0
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Database
16
+ Requires-Dist: httpx (>=0.24,<1.0)
17
+ Requires-Dist: typer (>=0.12,<1.0)
18
+ Project-URL: Documentation, https://github.com/GetCassis/cassis-cli#readme
19
+ Project-URL: Homepage, https://getcassis.com
20
+ Project-URL: Repository, https://github.com/GetCassis/cassis-cli
21
+ Description-Content-Type: text/markdown
22
+
1
23
  # Cassis CLI
2
24
 
3
25
  Run Cassis actions from your CI pipelines:
4
26
 
5
- - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub.
27
+ - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** — advisory only (the object may simply not be built or synced yet), never a failed check.
28
+ - `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
6
29
  - `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
7
30
  - `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
8
31
  - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
@@ -11,6 +34,7 @@ Run Cassis actions from your CI pipelines:
11
34
  - `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
12
35
  - `cassis ontology test` runs individual questions through the text-to-SQL agent using your local ontology files, so you can check that a change actually works (e.g. a new column gets picked) — where `eval run` only checks for regressions on existing eval cases.
13
36
  - `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
37
+ - `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
14
38
 
15
39
  ## Install
16
40
 
@@ -32,7 +56,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
32
56
 
33
57
  1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
34
58
  2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
35
- 3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case`: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID in the project's URL).
59
+ 3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
36
60
 
37
61
  ## Usage
38
62
 
@@ -79,6 +103,13 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
79
103
  # Add a gold case to the eval suite (rejected if the exact question already exists):
80
104
  cassis eval add-case --project ... -q "How much was refunded last month?" \
81
105
  --gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
106
+
107
+ # List the suite's cases (id + question; --json adds the gold SQL), then prune one:
108
+ cassis eval list-cases --project ...
109
+ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
110
+
111
+ # Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
112
+ cassis schema pull
82
113
  ```
83
114
 
84
115
  Configuration (flags take precedence over env vars):
@@ -88,7 +119,7 @@ Configuration (flags take precedence over env vars):
88
119
  | `--api-key` | `CASSIS_API_KEY` | — (required) |
89
120
  | `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
90
121
  | `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
91
- | `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` | (required) |
122
+ | `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
92
123
 
93
124
  `cassis eval run` also accepts `--label` (run label in the Evals page; defaults
94
125
  to the branch name from the CI environment or the local git checkout; rejected
@@ -117,7 +148,7 @@ cassis ontology fmt --check
117
148
  | Code | Meaning |
118
149
  | ---- | ------------------------------------------------------------------------------ |
119
150
  | 0 | Ontology is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test — whatever its outcome; probes are informational, don't gate CI on them) |
120
- | 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed) |
151
+ | 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case: no such case in the project) |
121
152
  | 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
122
153
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
123
154
 
@@ -211,3 +242,19 @@ ontology-publish:
211
242
  CASSIS_API_KEY: $CASSIS_API_KEY
212
243
  CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
213
244
  ```
245
+
246
+ ## About this repository
247
+
248
+ [github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
249
+ read-only mirror, synced automatically from the Cassis monorepo where the CLI is
250
+ developed. Issues are welcome and watched; pull requests can't be merged here, so
251
+ open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
252
+ with credit.
253
+
254
+ Only the CLI is open source. The Cassis backend it talks to is proprietary and
255
+ requires an account.
256
+
257
+ ## License
258
+
259
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
260
+
@@ -1,28 +1,9 @@
1
- Metadata-Version: 2.4
2
- Name: cassis-cli
3
- Version: 1.1.0
4
- Summary: Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines
5
- License: Proprietary
6
- Keywords: cassis,ontology,ci,text-to-sql
7
- Author: Cassis
8
- Author-email: tech.admin@getcassis.com
9
- Requires-Python: >=3.10,<4.0
10
- Classifier: License :: Other/Proprietary License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Programming Language :: Python :: 3.14
17
- Requires-Dist: httpx (>=0.24,<1.0)
18
- Requires-Dist: typer (>=0.12,<1.0)
19
- Description-Content-Type: text/markdown
20
-
21
1
  # Cassis CLI
22
2
 
23
3
  Run Cassis actions from your CI pipelines:
24
4
 
25
- - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub.
5
+ - `cassis ontology check` validates the ontology files in your repository with the exact same checks as the Cassis GitHub PR check (YAML parsing, round-trip, import validation) — so you can gate merges in any CI system, not just GitHub. In a checkout bound to a project (`project.yml`, `--project`, or `CASSIS_PROJECT_ID`), it also cross-checks the tree against the project's source schema: references to tables or columns the warehouse doesn't have print as **warnings** — advisory only (the object may simply not be built or synced yet), never a failed check.
6
+ - `cassis schema pull` downloads the data source's full source schema (as Cassis last introspected it) into `<base-path>/.schema.json` — a **gitignored** local snapshot (the command maintains the ignore entry) with a `pulled_at` stamp. The warehouse stays authoritative; the snapshot is a cache for offline/bulk work — e.g. a coding agent grepping table and column names during a modeling pass instead of paging through the MCP `get_source_schema` tool. Re-run to refresh.
26
7
  - `cassis ontology fmt` rewrites the ontology files in canonical form (think `black`/`gofmt` for the ontology), so hand or agent edits pass the round-trip check.
27
8
  - `cassis ontology upload` uploads the ontology files to a Cassis project (full replace) and, by default, publishes them immediately as a new version — so a merge to your main branch can go live in one CI step.
28
9
  - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local ontology files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
@@ -31,6 +12,7 @@ Run Cassis actions from your CI pipelines:
31
12
  - `cassis eval run` runs the project's eval suite against your local ontology files (scored in-memory — nothing is pushed to Cassis) and prints per-question results, so you can test the changes on your git branch before merging.
32
13
  - `cassis ontology test` runs individual questions through the text-to-SQL agent using your local ontology files, so you can check that a change actually works (e.g. a new column gets picked) — where `eval run` only checks for regressions on existing eval cases.
33
14
  - `cassis eval add-case` adds a gold question/SQL case to the project's eval suite — after fixing an ontology issue, add the question users were failing on so `eval run` guards it from regressing.
15
+ - `cassis eval list-cases` and `cassis eval delete-case` maintain the suite: list the current cases with their ids, and prune one that is stale or wrong (e.g. its gold SQL encodes a definition the ontology has since changed).
34
16
 
35
17
  ## Install
36
18
 
@@ -52,7 +34,7 @@ The ontology tree under `<base-path>` (default `cassis/`) is:
52
34
 
53
35
  1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
54
36
  2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
55
- 3. For `pull`, `upload`, `eval run`, `ontology test`, and `eval add-case`: the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID in the project's URL).
37
+ 3. For `pull`, `upload`, `schema pull`, `eval run`, `ontology test`, and the `eval` case commands (`add-case`, `list-cases`, `delete-case`): the project ID (UUID) is taken from `<base-path>/project.yml` in the checkout (written by `pull` and by publishing) — so once a repo is pulled you don't need to pass it. To override, or before the first pull, set `CASSIS_PROJECT_ID` or pass `--project` (find the UUID in the project's URL). `ontology check` uses the same resolution but treats it as optional: unbound checkouts get the project-less validation (no schema reference warnings).
56
38
 
57
39
  ## Usage
58
40
 
@@ -99,6 +81,13 @@ cassis ontology test --project ... -q "How much was refunded last month?" -q "Ne
99
81
  # Add a gold case to the eval suite (rejected if the exact question already exists):
100
82
  cassis eval add-case --project ... -q "How much was refunded last month?" \
101
83
  --gold-sql "SELECT SUM(refunded_cents) / 100.0 FROM public.orders WHERE ..."
84
+
85
+ # List the suite's cases (id + question; --json adds the gold SQL), then prune one:
86
+ cassis eval list-cases --project ...
87
+ cassis eval delete-case 019f0000-0000-7000-8000-0000000000ca --project ...
88
+
89
+ # Pull the source schema into <base-path>/.schema.json (gitignored local snapshot):
90
+ cassis schema pull
102
91
  ```
103
92
 
104
93
  Configuration (flags take precedence over env vars):
@@ -108,7 +97,7 @@ Configuration (flags take precedence over env vars):
108
97
  | `--api-key` | `CASSIS_API_KEY` | — (required) |
109
98
  | `--api-url` | `CASSIS_API_URL` | `https://app.getcassis.com` |
110
99
  | `--base-path` | `CASSIS_BASE_PATH` | `cassis` — must match the project's git-sync "Path" setting |
111
- | `--project` (pull, upload, eval run, eval add-case, test) | `CASSIS_PROJECT_ID` | (required) |
100
+ | `--project` (check, pull, upload, schema pull, eval run, eval add-case, eval list-cases, eval delete-case, test) | `CASSIS_PROJECT_ID` | the id in `<base-path>/project.yml` (required before the first pull; `check` alone falls back to the project-less validation when unbound) |
112
101
 
113
102
  `cassis eval run` also accepts `--label` (run label in the Evals page; defaults
114
103
  to the branch name from the CI environment or the local git checkout; rejected
@@ -137,7 +126,7 @@ cassis ontology fmt --check
137
126
  | Code | Meaning |
138
127
  | ---- | ------------------------------------------------------------------------------ |
139
128
  | 0 | Ontology is valid (check) / pulled (pull) / uploaded (upload) / eval run completed all-passed (eval run) / every probe completed (test — whatever its outcome; probes are informational, don't gate CI on them) |
140
- | 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed) |
129
+ | 1 | Validation failed (check: findings printed; upload: nothing imported; eval run: invalid tree, failed cases, or failed/cancelled run; test: invalid tree or a probe failed; add-case: duplicate question or gold SQL that does not run; delete-case: no such case in the project) |
141
130
  | 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
142
131
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
143
132
 
@@ -232,3 +221,17 @@ ontology-publish:
232
221
  CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
233
222
  ```
234
223
 
224
+ ## About this repository
225
+
226
+ [github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
227
+ read-only mirror, synced automatically from the Cassis monorepo where the CLI is
228
+ developed. Issues are welcome and watched; pull requests can't be merged here, so
229
+ open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
230
+ with credit.
231
+
232
+ Only the CLI is open source. The Cassis backend it talks to is proprietary and
233
+ requires an account.
234
+
235
+ ## License
236
+
237
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
@@ -1,3 +1,3 @@
1
1
  """Cassis CLI — run Cassis actions from your CI pipelines."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "1.2.0"
@@ -134,10 +134,19 @@ def post_ontology_check(
134
134
  api_url: str,
135
135
  api_key: str,
136
136
  files: dict[str, str],
137
+ project_id: Optional[str] = None,
137
138
  transport: Optional[httpx.BaseTransport] = None,
138
139
  ) -> dict[str, Any]:
139
- """POST the ontology tree to /api/ci/ontology-check and return the response body."""
140
- url = api_url.rstrip("/") + "/api/ci/ontology-check"
140
+ """POST the ontology tree to the check endpoint and return the response body.
141
+
142
+ With ``project_id``, calls the project-scoped route, which additionally
143
+ cross-checks the tree against the project's source schema and returns
144
+ advisory ``warnings``; without it, the pure tree check.
145
+ """
146
+ if project_id:
147
+ url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/check"
148
+ else:
149
+ url = api_url.rstrip("/") + "/api/ci/ontology-check"
141
150
  try:
142
151
  with _client(transport=transport) as client:
143
152
  response = client.post(
@@ -150,6 +159,8 @@ def post_ontology_check(
150
159
 
151
160
  if response.status_code == 401:
152
161
  raise AuthError("The Cassis API rejected the API key (invalid or expired).")
162
+ if project_id and response.status_code in (403, 404):
163
+ raise _project_scope_error(response)
153
164
  if response.status_code >= 400:
154
165
  raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
155
166
  result = _parse_json_response(response, url)
@@ -232,6 +243,47 @@ def get_ontology_export(
232
243
  return result["files"]
233
244
 
234
245
 
246
+ class NoSourceSchemaError(ApiError):
247
+ """The project's data source has no introspected schema to pull."""
248
+
249
+
250
+ def get_schema_export(
251
+ *,
252
+ api_url: str,
253
+ api_key: str,
254
+ project_id: str,
255
+ transport: Optional[httpx.BaseTransport] = None,
256
+ ) -> dict[str, Any]:
257
+ """GET /api/ci/projects/{project_id}/schema and return the response body."""
258
+ url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/schema"
259
+ try:
260
+ with _client(transport=transport) as client:
261
+ response = client.get(url, headers={"Authorization": f"Bearer {api_key}"})
262
+ except httpx.HTTPError as exc:
263
+ raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
264
+
265
+ if response.status_code == 401:
266
+ raise AuthError("The Cassis API rejected the API key (invalid or expired).")
267
+ # "No source schema" is the marker the server's export endpoint puts in its
268
+ # 404 detail (backend endpoints/ci.py::export_source_schema — reworded only
269
+ # with a paired CLI release). Without this routing, a schema-less project
270
+ # would surface as the misleading "check --project / key access" hint below.
271
+ if response.status_code == 404 and "No source schema" in response.text:
272
+ raise NoSourceSchemaError(str(_detail_or_text(response)))
273
+ if response.status_code in (400, 403, 404):
274
+ raise _project_scope_error(response)
275
+ if response.status_code >= 400:
276
+ raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
277
+ result = _parse_json_response(response, url)
278
+ if (
279
+ not isinstance(result, dict)
280
+ or not isinstance(result.get("tables"), list)
281
+ or not isinstance(result.get("schema_version"), dict)
282
+ ):
283
+ raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
284
+ return result
285
+
286
+
235
287
  def post_eval_run_start(
236
288
  *,
237
289
  api_url: str,
@@ -321,6 +373,54 @@ def post_eval_case_create(
321
373
  return result
322
374
 
323
375
 
376
+ class EvalCaseNotFoundError(ApiError):
377
+ """The project has no current eval case with this id."""
378
+
379
+
380
+ def get_eval_cases(
381
+ *,
382
+ api_url: str,
383
+ api_key: str,
384
+ project_id: str,
385
+ transport: Optional[httpx.BaseTransport] = None,
386
+ ) -> list[dict[str, Any]]:
387
+ """GET /api/ci/projects/{project_id}/eval/cases and return the case list."""
388
+ url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases"
389
+ result = _get_eval_json(url, api_key, transport)
390
+ if not isinstance(result, list):
391
+ raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
392
+ return result
393
+
394
+
395
+ def delete_eval_case(
396
+ *,
397
+ api_url: str,
398
+ api_key: str,
399
+ project_id: str,
400
+ case_id: str,
401
+ transport: Optional[httpx.BaseTransport] = None,
402
+ ) -> None:
403
+ """DELETE /api/ci/projects/{project_id}/eval/cases/{case_id}."""
404
+ url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/eval/cases/{case_id}"
405
+ try:
406
+ with _client(transport=transport) as client:
407
+ response = client.delete(url, headers={"Authorization": f"Bearer {api_key}"})
408
+ except httpx.HTTPError as exc:
409
+ raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
410
+
411
+ if response.status_code == 401:
412
+ raise AuthError("The Cassis API rejected the API key (invalid or expired).")
413
+ # Exact-match wire contract with the DELETE endpoint's 404 detail (see
414
+ # `delete_eval_case` in backend/app/endpoints/ci.py): it distinguishes a
415
+ # missing case (exit 1) from a project-scope 404 (exit 3).
416
+ if response.status_code == 404 and _detail_or_text(response) == "Eval case not found":
417
+ raise EvalCaseNotFoundError(f"No current eval case {case_id} in this project (already deleted, or wrong id?).")
418
+ if response.status_code in (403, 404):
419
+ raise _project_scope_error(response)
420
+ if response.status_code >= 400:
421
+ raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
422
+
423
+
324
424
  def _get_eval_json(url: str, api_key: str, transport: Optional[httpx.BaseTransport]) -> Any:
325
425
  """GET an eval-run URL with the shared error mapping."""
326
426
  try:
@@ -93,20 +93,27 @@ def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
93
93
  return None
94
94
 
95
95
 
96
- def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
96
+ def resolve_project_id(
97
+ project_id: Optional[str], ontology_dir: Path, *, optional: bool = False, quiet: bool = False
98
+ ) -> Optional[str]:
97
99
  """Resolve the target project id, defaulting to the checkout's ``project.yml``.
98
100
 
99
101
  Precedence: an explicit ``--project`` / ``CASSIS_PROJECT_ID`` wins; otherwise
100
102
  the ``project_id`` recorded in ``<base-path>/project.yml`` (written by
101
103
  ``pull`` / publish) is used, and where it came from is noted on stderr so a
102
- stale value in a copied repo is visible. Exits 2 (usage) when neither is
103
- available or the value isn't a UUID.
104
+ stale value in a copied repo is visible (``quiet`` suppresses the note for
105
+ machine-readable output). Exits 2 (usage) when the value isn't a UUID, or —
106
+ unless ``optional`` — when no value is available at all; with ``optional``,
107
+ an unbound checkout returns None (``check`` falls back to the project-less
108
+ validation).
104
109
  """
105
110
  from_file = False
106
111
  if not project_id:
107
112
  project_id = read_project_id_from_dir(ontology_dir)
108
113
  from_file = project_id is not None
109
114
  if not project_id:
115
+ if optional:
116
+ return None
110
117
  typer.secho(
111
118
  f"No project. Pass --project (or set CASSIS_PROJECT_ID), or run in a checkout whose "
112
119
  f"{ontology_dir.name}/project.yml records it (written by `cassis ontology pull` or a publish).",
@@ -119,7 +126,7 @@ def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
119
126
  except ValueError:
120
127
  typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
121
128
  raise typer.Exit(EXIT_USAGE)
122
- if from_file:
129
+ if from_file and not quiet:
123
130
  typer.secho(f"Using project {project_id} from {ontology_dir.name}/project.yml.", fg=typer.colors.CYAN, err=True)
124
131
  return project_id
125
132
 
@@ -8,6 +8,7 @@ import subprocess
8
8
  import time
9
9
  from pathlib import Path
10
10
  from typing import Any, Optional
11
+ from uuid import UUID
11
12
 
12
13
  import typer
13
14
  from cassis_cli.api import (
@@ -16,8 +17,11 @@ from cassis_cli.api import (
16
17
  AuthError,
17
18
  EvalCaseExistsError,
18
19
  EvalCaseGoldSqlError,
20
+ EvalCaseNotFoundError,
19
21
  EvalRunActiveError,
20
22
  EvalStartValidationError,
23
+ delete_eval_case,
24
+ get_eval_cases,
21
25
  get_eval_run,
22
26
  get_eval_run_results,
23
27
  post_eval_case_create,
@@ -221,6 +225,134 @@ def add_case(
221
225
  raise typer.Exit(EXIT_OK)
222
226
 
223
227
 
228
+ @app.command(name="list-cases")
229
+ def list_cases(
230
+ path: Path = typer.Argument(
231
+ Path("."),
232
+ help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
233
+ ),
234
+ project_id: Optional[str] = typer.Option(
235
+ None,
236
+ "--project",
237
+ envvar="CASSIS_PROJECT_ID",
238
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
239
+ ),
240
+ api_key: Optional[str] = typer.Option(
241
+ None,
242
+ "--api-key",
243
+ envvar="CASSIS_API_KEY",
244
+ help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
245
+ ),
246
+ api_url: str = typer.Option(
247
+ DEFAULT_API_URL,
248
+ "--api-url",
249
+ envvar="CASSIS_API_URL",
250
+ help="Cassis API base URL.",
251
+ ),
252
+ base_path: str = typer.Option(
253
+ DEFAULT_BASE_PATH,
254
+ "--base-path",
255
+ envvar="CASSIS_BASE_PATH",
256
+ help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
257
+ ),
258
+ json_output: bool = typer.Option(False, "--json", help="Print the cases as raw JSON (includes gold SQL)."),
259
+ ) -> None:
260
+ """List the project's eval cases: the suite `cassis eval run` scores.
261
+
262
+ Prints each case's id and question (--json adds the gold SQL); the id is
263
+ what `cassis eval delete-case` takes. Exits 0 on success, 2 on usage
264
+ errors, 3 on transport/API errors.
265
+ """
266
+ api_key = require_api_key(api_key)
267
+ project_id = resolve_project_id(project_id, path / Path(base_path))
268
+
269
+ try:
270
+ cases = get_eval_cases(api_url=api_url, api_key=api_key, project_id=project_id)
271
+ except AuthError as exc:
272
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
273
+ raise typer.Exit(EXIT_TRANSPORT) from exc
274
+ except ApiError as exc:
275
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
276
+ raise typer.Exit(EXIT_TRANSPORT) from exc
277
+
278
+ if json_output:
279
+ typer.echo(json.dumps(cases, indent=2))
280
+ else:
281
+ if not cases:
282
+ typer.echo("No eval cases yet — add one with `cassis eval add-case`.")
283
+ for case in cases:
284
+ question = str(case.get("question", "")).replace("\n", " ")
285
+ typer.echo(f"{case.get('id')} {question}")
286
+ raise typer.Exit(EXIT_OK)
287
+
288
+
289
+ @app.command(name="delete-case")
290
+ def delete_case(
291
+ case_id: str = typer.Argument(
292
+ ...,
293
+ help="Id of the eval case to delete (shown by `cassis eval list-cases`).",
294
+ ),
295
+ path: Path = typer.Option(
296
+ Path("."),
297
+ "--path",
298
+ help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
299
+ ),
300
+ project_id: Optional[str] = typer.Option(
301
+ None,
302
+ "--project",
303
+ envvar="CASSIS_PROJECT_ID",
304
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
305
+ ),
306
+ api_key: Optional[str] = typer.Option(
307
+ None,
308
+ "--api-key",
309
+ envvar="CASSIS_API_KEY",
310
+ help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
311
+ ),
312
+ api_url: str = typer.Option(
313
+ DEFAULT_API_URL,
314
+ "--api-url",
315
+ envvar="CASSIS_API_URL",
316
+ help="Cassis API base URL.",
317
+ ),
318
+ base_path: str = typer.Option(
319
+ DEFAULT_BASE_PATH,
320
+ "--base-path",
321
+ envvar="CASSIS_BASE_PATH",
322
+ help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
323
+ ),
324
+ ) -> None:
325
+ """Delete an eval case from the project's suite.
326
+
327
+ For pruning a case that is stale or wrong — e.g. its gold SQL encodes a
328
+ definition the ontology has since changed. Exits 0 on deletion, 1 when
329
+ the case does not exist in the project, 2 on usage errors, 3 on
330
+ transport/API errors.
331
+ """
332
+ api_key = require_api_key(api_key)
333
+ project_id = resolve_project_id(project_id, path / Path(base_path))
334
+ try:
335
+ UUID(case_id)
336
+ except ValueError:
337
+ typer.secho(f"CASE_ID must be a UUID, got {case_id!r}.", fg=typer.colors.RED, err=True)
338
+ raise typer.Exit(EXIT_USAGE)
339
+
340
+ try:
341
+ delete_eval_case(api_url=api_url, api_key=api_key, project_id=project_id, case_id=case_id)
342
+ except EvalCaseNotFoundError as exc:
343
+ typer.secho(str(exc), fg=typer.colors.YELLOW, err=True)
344
+ raise typer.Exit(EXIT_VALIDATION_FAILED) from exc
345
+ except AuthError as exc:
346
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
347
+ raise typer.Exit(EXIT_TRANSPORT) from exc
348
+ except ApiError as exc:
349
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
350
+ raise typer.Exit(EXIT_TRANSPORT) from exc
351
+
352
+ typer.secho(f"✓ Deleted eval case {case_id}.", fg=typer.colors.GREEN)
353
+ raise typer.Exit(EXIT_OK)
354
+
355
+
224
356
  @app.command()
225
357
  def run(
226
358
  path: Path = typer.Argument(
@@ -31,7 +31,7 @@ GUIDE_FILENAME = "AGENTS.md"
31
31
  # Monotonic version of the doctrine text below. Bump it whenever
32
32
  # ontology_design_guide.md changes (a backend test enforces the pairing) — it
33
33
  # is what lets an older writer recognize a newer guide and leave it alone.
34
- DOCTRINE_VERSION = 3
34
+ DOCTRINE_VERSION = 5
35
35
 
36
36
  # Must stay byte-identical to backend/app/services/ontology_guide.py::_BANNER —
37
37
  # the server-side git export writes the same file, and differing banners would
@@ -6,6 +6,7 @@ import typer
6
6
  from cassis_cli import __version__
7
7
  from cassis_cli.eval import app as eval_app
8
8
  from cassis_cli.ontology import app as ontology_app
9
+ from cassis_cli.schema import app as schema_app
9
10
 
10
11
  app = typer.Typer(
11
12
  no_args_is_help=True,
@@ -13,6 +14,7 @@ app = typer.Typer(
13
14
  )
14
15
  app.add_typer(ontology_app, name="ontology")
15
16
  app.add_typer(eval_app, name="eval")
17
+ app.add_typer(schema_app, name="schema")
16
18
 
17
19
 
18
20
  @app.command()
@@ -56,6 +56,12 @@ def check(
56
56
  Path("."),
57
57
  help="Repository checkout root (the directory containing the ontology export path).",
58
58
  ),
59
+ project_id: Optional[str] = typer.Option(
60
+ None,
61
+ "--project",
62
+ envvar="CASSIS_PROJECT_ID",
63
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
64
+ ),
59
65
  api_key: Optional[str] = typer.Option(
60
66
  None,
61
67
  "--api-key",
@@ -79,14 +85,27 @@ def check(
79
85
  """Validate the ontology files in a repository checkout.
80
86
 
81
87
  Runs the same checks as the Cassis GitHub PR check (YAML parsing,
82
- round-trip, import validation). Exits 0 when valid, 1 when validation
83
- fails, 2 on usage errors, 3 on transport/API errors.
88
+ round-trip, import validation). When the checkout is bound to a project
89
+ (``project.yml``, ``--project``, or ``CASSIS_PROJECT_ID``), the tree is
90
+ additionally cross-checked against the project's source schema; unmatched
91
+ references print as warnings and never fail the check. Exits 0 when valid,
92
+ 1 when validation fails, 2 on usage errors, 3 on transport/API errors.
84
93
  """
85
94
  api_key = _require_api_key(api_key)
86
95
  files, base_path = _collect_tree(path, base_path)
87
96
 
97
+ # Soft resolution: an unbound checkout is not an error — the check falls
98
+ # back to the project-less route (no schema reference stage).
99
+ project_id = _resolve_project_id(project_id, path / base_path, optional=True, quiet=json_output)
100
+ if not project_id and not json_output:
101
+ typer.secho(
102
+ "No project binding — schema reference checks skipped (bind with `cassis ontology pull` or --project).",
103
+ fg=typer.colors.CYAN,
104
+ err=True,
105
+ )
106
+
88
107
  try:
89
- result = post_ontology_check(api_url=api_url, api_key=api_key, files=files)
108
+ result = post_ontology_check(api_url=api_url, api_key=api_key, files=files, project_id=project_id)
90
109
  except AuthError as exc:
91
110
  typer.secho(str(exc), fg=typer.colors.RED, err=True)
92
111
  raise typer.Exit(EXIT_TRANSPORT) from exc
@@ -94,10 +113,21 @@ def check(
94
113
  typer.secho(str(exc), fg=typer.colors.RED, err=True)
95
114
  raise typer.Exit(EXIT_TRANSPORT) from exc
96
115
 
116
+ warnings = result.get("warnings") or []
97
117
  if json_output:
98
118
  typer.echo(json.dumps(result, indent=2))
99
119
  elif result["passed"]:
100
120
  typer.secho(f"✓ {result['summary']}", fg=typer.colors.GREEN)
121
+ # Disambiguate silence: "no warnings" must never read as "references
122
+ # verified" when the stage didn't run.
123
+ if project_id and result.get("references_checked") and not warnings:
124
+ typer.secho("✓ Schema references resolve against the source schema.", fg=typer.colors.GREEN)
125
+ elif project_id and not result.get("references_checked"):
126
+ typer.secho(
127
+ "Schema reference check skipped — the project has no source schema yet.",
128
+ fg=typer.colors.YELLOW,
129
+ err=True,
130
+ )
101
131
  else:
102
132
  typer.secho(result["title"], fg=typer.colors.RED, bold=True)
103
133
  typer.echo(result["summary"])
@@ -105,6 +135,16 @@ def check(
105
135
  location = f"{base_path}/{finding.get('path')}: " if finding.get("path") else ""
106
136
  typer.echo(f" {location}{finding.get('message', '')} ({finding.get('stage', '?')})")
107
137
 
138
+ if warnings and not json_output:
139
+ typer.secho(
140
+ f"{len(warnings)} schema reference warning(s) — advisory, expected if the objects "
141
+ "haven't been built or synced yet:",
142
+ fg=typer.colors.YELLOW,
143
+ bold=True,
144
+ )
145
+ for warning in warnings:
146
+ typer.secho(f" {warning.get('message', '')}", fg=typer.colors.YELLOW)
147
+
108
148
  raise typer.Exit(EXIT_OK if result["passed"] else EXIT_VALIDATION_FAILED)
109
149
 
110
150
 
@@ -357,6 +357,16 @@ description, so a wrong example is worse than none.
357
357
  real mechanic diverges.
358
358
  - **Don't describe things that don't exist.** Every table, column, and value you
359
359
  reference must be real.
360
+ - **A hedge is not a safeguard.** "Likely `'M:SS.mmm'`", "probably", "appears to
361
+ be" reach the agent as the claim itself; the qualifier does not survive into
362
+ its reasoning. If a storage format, a unit, or a reason for nullability is not
363
+ in the source, leave it out and record the open question instead.
364
+ - **Never invent a contrast with a column you did not read.** When a new column
365
+ shares a name with one on another table (`position_order`, `status_id`,
366
+ `lap`), the tempting sentence is "here it means X, unlike there". A schema
367
+ dump cannot support that. Open the sibling's description and reuse it if it
368
+ applies; if you can't confirm the relationship, say it's unverified. A false
369
+ distinction drawn confidently survives review as a documented decision.
360
370
 
361
371
  ---
362
372
 
@@ -386,7 +396,31 @@ description, so a wrong example is worse than none.
386
396
 
387
397
  ---
388
398
 
389
- ## 12. Working in a git-synced repo
399
+ ## 12. Extension passes: structure before content
400
+
401
+ Adding a source or a whole subject area inverts the scoping bullet above. §11 is
402
+ written for maintenance edits, where a structural change is genuinely adjacent;
403
+ on an extension pass the tree *is* the change — every table and metric names a
404
+ `domain_path`, so content filled into a hierarchy you already doubt all has to
405
+ move. Same human opt-in, front-loaded:
406
+
407
+ 1. **Scope.** Agree what is being added, from which sources, in what order.
408
+ 2. **Structure.** Evaluate the existing hierarchy against what is arriving and
409
+ propose the tree changes — new domains, splits, moves — for approval *before*
410
+ any content. On approval, create the new domain files frontmatter-only (empty
411
+ body), so every `domain_path` resolves while you fill.
412
+ 3. **Fill bottom-up.** Column and table facts, then metrics and joins, then each
413
+ domain README body last, written from what is left over. That residue is by
414
+ construction the cross-table connective tissue domain level owns, so the order
415
+ enforces the one-home rule instead of leaving it to vigilance. Bottom-up is an
416
+ authoring order, not a holding rule: a fact learned out of order still lands at
417
+ its owning layer immediately (§11). If filling shows the approved tree is wrong
418
+ — a domain overloads, or wants to split — stop and return to 2 rather than keep
419
+ filling into it.
420
+
421
+ ---
422
+
423
+ ## 13. Working in a git-synced repo
390
424
 
391
425
  The repository is the source of truth. Edit the files, then verify before opening
392
426
  a pull request — the CLI runs the same checks the platform does, from your
@@ -0,0 +1,129 @@
1
+ """`cassis schema` — local snapshot of the data source's source schema.
2
+
3
+ The source schema is OBSERVED state (the warehouse is authoritative), so the
4
+ snapshot is a gitignored cache, never a committed file: `pull` writes
5
+ `<base-path>/.schema.json` and keeps it out of git via the ontology dir's
6
+ `.gitignore`. Agents working in a checkout grep it instead of paging through
7
+ the MCP `get_source_schema` tool; `pulled_at` records how stale it is.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ import typer
18
+ from cassis_cli.api import DEFAULT_API_URL, ApiError, AuthError, get_schema_export
19
+ from cassis_cli.common import (
20
+ DEFAULT_BASE_PATH,
21
+ EXIT_OK,
22
+ EXIT_TRANSPORT,
23
+ EXIT_USAGE,
24
+ require_api_key,
25
+ resolve_project_id,
26
+ )
27
+
28
+ app = typer.Typer(help="Pull a local, gitignored snapshot of the data source's schema.")
29
+
30
+ SNAPSHOT_FILENAME = ".schema.json"
31
+ _GITIGNORE_HEADER = "# Cassis local caches (observed state — never commit)"
32
+
33
+
34
+ @app.command()
35
+ def pull(
36
+ path: Path = typer.Argument(
37
+ Path("."),
38
+ help="Repository checkout root (the directory containing the ontology export path).",
39
+ ),
40
+ project_id: Optional[str] = typer.Option(
41
+ None,
42
+ "--project",
43
+ envvar="CASSIS_PROJECT_ID",
44
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
45
+ ),
46
+ api_key: Optional[str] = typer.Option(
47
+ None,
48
+ "--api-key",
49
+ envvar="CASSIS_API_KEY",
50
+ help="Cassis API key (sk-k6-...). Create one in Organization settings -> API keys.",
51
+ ),
52
+ api_url: str = typer.Option(
53
+ DEFAULT_API_URL,
54
+ "--api-url",
55
+ envvar="CASSIS_API_URL",
56
+ help="Cassis API base URL.",
57
+ ),
58
+ base_path: str = typer.Option(
59
+ DEFAULT_BASE_PATH,
60
+ "--base-path",
61
+ envvar="CASSIS_BASE_PATH",
62
+ help="Repository directory the ontology is exported under (the project's git-sync Path setting).",
63
+ ),
64
+ ) -> None:
65
+ """Download the source schema into `<base-path>/.schema.json` (gitignored).
66
+
67
+ The snapshot is the schema as Cassis last introspected it from the
68
+ warehouse (or parsed from an uploaded DDL) — every table with its columns
69
+ and types, plus a `pulled_at` stamp so staleness is visible. Re-run after
70
+ a warehouse sync to refresh. Exits 0 on success, 2 on usage errors, 3 on
71
+ transport/API errors.
72
+ """
73
+ api_key = require_api_key(api_key)
74
+ ontology_dir = path / base_path
75
+ project_id = resolve_project_id(project_id, ontology_dir)
76
+
77
+ try:
78
+ result = get_schema_export(api_url=api_url, api_key=api_key, project_id=project_id)
79
+ except (AuthError, ApiError) as exc:
80
+ # NoSourceSchemaError lands here too: the server's message already says
81
+ # what to do (sync or upload a DDL); the class exists so api.py doesn't
82
+ # bury it under the misleading project-scope hint.
83
+ typer.secho(str(exc), fg=typer.colors.RED, err=True)
84
+ raise typer.Exit(EXIT_TRANSPORT) from exc
85
+
86
+ snapshot = {
87
+ "pulled_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
88
+ "project_id": project_id,
89
+ "schema_version": result["schema_version"],
90
+ "tables": result["tables"],
91
+ }
92
+
93
+ try:
94
+ ontology_dir.mkdir(parents=True, exist_ok=True)
95
+ snapshot_path = ontology_dir / SNAPSHOT_FILENAME
96
+ snapshot_path.write_text(json.dumps(snapshot, indent=2) + "\n", encoding="utf-8")
97
+ ensure_gitignored(ontology_dir)
98
+ except OSError as exc:
99
+ # Usage-class exit (2), like the other local-file failures in the exit
100
+ # table — the API call succeeded, the checkout is what's broken.
101
+ typer.secho(f"Could not write the snapshot: {exc}", fg=typer.colors.RED, err=True)
102
+ raise typer.Exit(EXIT_USAGE) from exc
103
+
104
+ table_count = len(result["tables"])
105
+ column_count = sum(len(t.get("columns") or []) for t in result["tables"])
106
+ version = result["schema_version"].get("version")
107
+ typer.secho(
108
+ f"✓ Pulled source schema v{version}: {table_count} tables, {column_count} columns "
109
+ f"-> {snapshot_path} (gitignored)",
110
+ fg=typer.colors.GREEN,
111
+ )
112
+
113
+
114
+ def ensure_gitignored(ontology_dir: Path) -> None:
115
+ """Make sure the snapshot never lands in git: keep `.gitignore` covering it.
116
+
117
+ Appends to (or creates) the ontology dir's own `.gitignore` — local to the
118
+ export directory, so it survives repo-level `.gitignore` rewrites and needs
119
+ no knowledge of the checkout layout.
120
+ """
121
+ gitignore = ontology_dir / ".gitignore"
122
+ try:
123
+ existing = gitignore.read_text(encoding="utf-8")
124
+ except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
125
+ existing = ""
126
+ if SNAPSHOT_FILENAME in existing.splitlines():
127
+ return
128
+ prefix = "" if not existing else existing.rstrip("\n") + "\n"
129
+ gitignore.write_text(f"{prefix}{_GITIGNORE_HEADER}\n{SNAPSHOT_FILENAME}\n", encoding="utf-8")
@@ -1,11 +1,17 @@
1
1
  [project]
2
2
  name = "cassis-cli"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  description = "Cassis CLI — run Cassis actions (ontology validation, upload and publish) from your CI pipelines"
5
5
  readme = "README.md"
6
- license = { text = "Proprietary" }
6
+ license = { text = "Apache-2.0" }
7
7
  authors = [{ name = "Cassis", email = "tech.admin@getcassis.com" }]
8
8
  keywords = ["cassis", "ontology", "ci", "text-to-sql"]
9
+ classifiers = [
10
+ "License :: OSI Approved :: Apache Software License",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "Topic :: Database",
14
+ ]
9
15
  # Customer CI images — keep this floor low and the code 3.10-compatible,
10
16
  # regardless of the monorepo's own Python pin.
11
17
  requires-python = ">=3.10,<4.0"
@@ -14,6 +20,11 @@ dependencies = [
14
20
  "httpx (>=0.24,<1.0)",
15
21
  ]
16
22
 
23
+ [project.urls]
24
+ Homepage = "https://getcassis.com"
25
+ Repository = "https://github.com/GetCassis/cassis-cli"
26
+ Documentation = "https://github.com/GetCassis/cassis-cli#readme"
27
+
17
28
  [project.scripts]
18
29
  cassis = "cassis_cli.main:app"
19
30