cassis-cli 1.0.0__tar.gz → 1.1.1__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,3 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: cassis-cli
3
+ Version: 1.1.1
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:
@@ -5,8 +27,8 @@ Run Cassis actions from your CI pipelines:
5
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.
6
28
  - `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
29
  - `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
- - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local YAML files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
9
- - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (the CLI reads only `*.yml`/`*.yaml`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
30
+ - `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
+ - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
10
32
  - The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
11
33
  - `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
34
  - `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.
@@ -18,11 +40,21 @@ Run Cassis actions from your CI pipelines:
18
40
  pip install cassis-cli
19
41
  ```
20
42
 
43
+ ## Ontology file format
44
+
45
+ The ontology tree under `<base-path>` (default `cassis/`) is:
46
+
47
+ - **Project identity** — `project.yml`: the Cassis project id and format version. Written by `pull` and by server-side publish (the contexts that know the id); a local `fmt` won't create it.
48
+ - **Domains** — Markdown files: every domain is the `README.md` of its folder — `domains/README.md` for the root, `domains/<path>/README.md` for each sub-domain. Each has a small YAML frontmatter block (`type`, `title`, `description`) and a Markdown body carrying the domain's `context_md`; a generated section at the bottom links the domain's tables and metrics (kept current by `fmt`/`pull` — edit your prose above it, and the PR check fails if the links are stale, so re-run `fmt`). The layout is a Cassis profile inspired by [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf): the files render on GitHub and read in any Markdown editor, but Cassis validates them strictly (unknown keys are flagged, not preserved).
49
+ - **Tables, joins, metrics** — YAML, unchanged: `tables/<schema>/<table>.yml`, `joins.yml`, `metrics/<name>.yml`.
50
+
51
+ **Migrating an existing repo** (domains were YAML `_project.yml` / `_domain.yml` before cassis-cli 1.1.0): upgrade and run `cassis ontology fmt` (or `cassis ontology pull` if you have no local edits) — it rewrites the domain files to Markdown and removes the old ones. Review the diff and commit. Cassis reads the old YAML domain files too, so an un-migrated repo keeps working until you convert it. **Uploading requires cassis-cli ≥ 1.1.0** — the server rejects an older CLI (which would drop the Markdown domain files) with a clear upgrade error.
52
+
21
53
  ## Setup
22
54
 
23
55
  1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
24
56
  2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
25
- 3. For `pull`, `upload` and `eval run`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
57
+ 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).
26
58
 
27
59
  ## Usage
28
60
 
@@ -112,7 +144,7 @@ cassis ontology fmt --check
112
144
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
113
145
 
114
146
  Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
115
- 2000 YAML files / 5 MB total — far above real ontologies (a few hundred small
147
+ 2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
116
148
  files). Beyond that the CLI fails fast with exit 2 before uploading anything;
117
149
  double-check `--base-path` if you hit it.
118
150
 
@@ -201,3 +233,19 @@ ontology-publish:
201
233
  CASSIS_API_KEY: $CASSIS_API_KEY
202
234
  CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
203
235
  ```
236
+
237
+ ## About this repository
238
+
239
+ [github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
240
+ read-only mirror, synced automatically from the Cassis monorepo where the CLI is
241
+ developed. Issues are welcome and watched; pull requests can't be merged here, so
242
+ open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
243
+ with credit.
244
+
245
+ Only the CLI is open source. The Cassis backend it talks to is proprietary and
246
+ requires an account.
247
+
248
+ ## License
249
+
250
+ Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).
251
+
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: cassis-cli
3
- Version: 1.0.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:
@@ -25,8 +5,8 @@ Run Cassis actions from your CI pipelines:
25
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.
26
6
  - `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
7
  - `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
- - `cassis ontology pull` downloads the project's unpublished ontology into your repository checkout (full sync — stale local YAML files are pruned), so you can start editing from the current state, or bootstrap a repo that isn't git-synced (e.g. Bitbucket).
29
- - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (the CLI reads only `*.yml`/`*.yaml`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
8
+ - `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).
9
+ - `cassis ontology pull` and `cassis ontology fmt` also write `<base-path>/AGENTS.md`, the Cassis ontology modeling guide, into the checkout (default `cassis/AGENTS.md`) — a managed file (generated banner; the CLI overwrites local edits) so a repo-aware coding agent loads current Cassis modeling doctrine by convention. It sits inside the ontology directory but is not part of the ontology tree (which is the YAML files plus the domain Markdown files `domains/**/README.md`), so it is never uploaded, validated, or pruned. Commit it alongside your ontology changes. The guide text ships inside the CLI package, so its version tracks the **installed cassis-cli version** — upgrade the CLI (`pip install -U cassis-cli`) and re-run `fmt` to pick up doctrine updates; an unpinned `pip install cassis-cli` in CI gets them automatically. The banner stamps a doctrine version, and the CLI never *downgrades* the file: if the checkout's `AGENTS.md` was written by a newer doctrine (a newer CLI, or Cassis itself on a publish), `fmt`/`pull` leave it in place, print an upgrade notice, and `fmt --check` still passes.
30
10
  - The CLI identifies itself to the API (`User-Agent: cassis-cli/<version>`), and successful API responses advertise the newest published version — when you are behind, commands print a one-line upgrade notice on stderr (purely informational; output and exit codes are unchanged).
31
11
  - `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
12
  - `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.
@@ -38,11 +18,21 @@ Run Cassis actions from your CI pipelines:
38
18
  pip install cassis-cli
39
19
  ```
40
20
 
21
+ ## Ontology file format
22
+
23
+ The ontology tree under `<base-path>` (default `cassis/`) is:
24
+
25
+ - **Project identity** — `project.yml`: the Cassis project id and format version. Written by `pull` and by server-side publish (the contexts that know the id); a local `fmt` won't create it.
26
+ - **Domains** — Markdown files: every domain is the `README.md` of its folder — `domains/README.md` for the root, `domains/<path>/README.md` for each sub-domain. Each has a small YAML frontmatter block (`type`, `title`, `description`) and a Markdown body carrying the domain's `context_md`; a generated section at the bottom links the domain's tables and metrics (kept current by `fmt`/`pull` — edit your prose above it, and the PR check fails if the links are stale, so re-run `fmt`). The layout is a Cassis profile inspired by [OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf): the files render on GitHub and read in any Markdown editor, but Cassis validates them strictly (unknown keys are flagged, not preserved).
27
+ - **Tables, joins, metrics** — YAML, unchanged: `tables/<schema>/<table>.yml`, `joins.yml`, `metrics/<name>.yml`.
28
+
29
+ **Migrating an existing repo** (domains were YAML `_project.yml` / `_domain.yml` before cassis-cli 1.1.0): upgrade and run `cassis ontology fmt` (or `cassis ontology pull` if you have no local edits) — it rewrites the domain files to Markdown and removes the old ones. Review the diff and commit. Cassis reads the old YAML domain files too, so an un-migrated repo keeps working until you convert it. **Uploading requires cassis-cli ≥ 1.1.0** — the server rejects an older CLI (which would drop the Markdown domain files) with a clear upgrade error.
30
+
41
31
  ## Setup
42
32
 
43
33
  1. Create an API key in Cassis under **Organization settings → API keys** (keys start with `sk-k6-`).
44
34
  2. Store it as a CI secret and expose it as `CASSIS_API_KEY`.
45
- 3. For `pull`, `upload` and `eval run`: find the project ID (UUID) in the project's URL and expose it as `CASSIS_PROJECT_ID` (or pass `--project`).
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).
46
36
 
47
37
  ## Usage
48
38
 
@@ -132,7 +122,7 @@ cassis ontology fmt --check
132
122
  | 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another run already active, out of credits, or `--timeout` reached |
133
123
 
134
124
  Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
135
- 2000 YAML files / 5 MB total — far above real ontologies (a few hundred small
125
+ 2000 ontology files / 5 MB total — far above real ontologies (a few hundred small
136
126
  files). Beyond that the CLI fails fast with exit 2 before uploading anything;
137
127
  double-check `--base-path` if you hit it.
138
128
 
@@ -222,3 +212,17 @@ ontology-publish:
222
212
  CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
223
213
  ```
224
214
 
215
+ ## About this repository
216
+
217
+ [github.com/GetCassis/cassis-cli](https://github.com/GetCassis/cassis-cli) is a
218
+ read-only mirror, synced automatically from the Cassis monorepo where the CLI is
219
+ developed. Issues are welcome and watched; pull requests can't be merged here, so
220
+ open an issue (or mail tech.admin@getcassis.com) and we'll port the patch upstream
221
+ with credit.
222
+
223
+ Only the CLI is open source. The Cassis backend it talks to is proprietary and
224
+ requires an account.
225
+
226
+ ## License
227
+
228
+ 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.0.0"
3
+ __version__ = "1.1.1"
@@ -191,6 +191,8 @@ def post_ontology_import(
191
191
  raise AuthError("The Cassis API rejected the API key (invalid or expired).")
192
192
  if response.status_code == 400:
193
193
  raise UploadValidationError(str(_detail_or_text(response)))
194
+ if response.status_code == 426: # this CLI is too old for the server's ontology format
195
+ raise UploadValidationError(str(_detail_or_text(response)))
194
196
  if response.status_code in (403, 404):
195
197
  raise _project_scope_error(response)
196
198
  if response.status_code >= 400:
@@ -0,0 +1,177 @@
1
+ """Helpers shared by the `cassis` subcommands (tree collection, auth, exit codes)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from uuid import UUID
9
+
10
+ import typer
11
+
12
+ # Repository directory the ontology tree is exported under. Must match the
13
+ # project's git-sync "Path" setting in Cassis (default "cassis").
14
+ DEFAULT_BASE_PATH = "cassis"
15
+
16
+ # Exit codes (documented in the README; stable contract for CI scripts).
17
+ EXIT_OK = 0
18
+ EXIT_VALIDATION_FAILED = 1
19
+ EXIT_USAGE = 2
20
+ EXIT_TRANSPORT = 3
21
+
22
+ # Request ceilings of the /api/ci file-tree endpoints, mirrored so oversized
23
+ # trees fail fast with a clear message before any upload. Source of truth:
24
+ # backend/app/schemas/ci.py (the server's 422 remains the backstop).
25
+ MAX_FILES = 2000
26
+ MAX_TOTAL_BYTES = 5 * 1024 * 1024
27
+
28
+
29
+ def is_ontology_file(rel_path: str) -> bool:
30
+ """Whether a base-relative path is an ontology file the server reads.
31
+
32
+ YAML (``project.yml``, tables, joins, metrics, legacy domains) plus domain
33
+ Markdown — every domain is the ``README.md`` of its folder, root included
34
+ (``domains/README.md``), so ``domains/**/README.md`` covers them all. Mirrors
35
+ the server's ``ontology_fs.is_ontology_tree_file`` (kept in sync by hand —
36
+ the CLI can't import the backend). Excludes the managed ``AGENTS.md`` and any
37
+ stray Markdown note, so neither is uploaded nor deleted by ``pull --prune``.
38
+ """
39
+ if rel_path.endswith((".yml", ".yaml")):
40
+ return True
41
+ return rel_path.startswith("domains/") and rel_path.endswith("/README.md")
42
+
43
+
44
+ def is_legacy_domain_file(rel_path: str) -> bool:
45
+ """Whether a base-relative path is a legacy (pre-Markdown) domain file.
46
+
47
+ Used to report the one-time migration to the Markdown domain format, when
48
+ ``pull``/``fmt`` remove a ``_project.yml``/``_domain.yml`` and write the
49
+ ``domains/README.md`` (and sub-domain ``README.md``) that replaces it.
50
+ """
51
+ return rel_path == "_project.yml" or rel_path.endswith("/_domain.yml")
52
+
53
+
54
+ def collect_files(ontology_dir: Path) -> dict[str, str]:
55
+ """Read every ontology file under the ontology dir, keyed by posix relpath.
56
+
57
+ Ontology files are YAML and domain Markdown (see ``is_ontology_file``); the
58
+ managed ``AGENTS.md`` and stray notes are skipped. Exits 2 (usage) on an
59
+ unreadable or non-UTF-8 file — a local checkout problem, reported before
60
+ anything is sent to the API.
61
+ """
62
+ files: dict[str, str] = {}
63
+ for pattern in ("**/*.yml", "**/*.yaml", "**/*.md"):
64
+ for file in sorted(ontology_dir.glob(pattern)):
65
+ if not file.is_file():
66
+ continue
67
+ rel = file.relative_to(ontology_dir).as_posix()
68
+ if not is_ontology_file(rel):
69
+ continue
70
+ try:
71
+ files[rel] = file.read_text(encoding="utf-8")
72
+ except (UnicodeDecodeError, OSError) as exc:
73
+ typer.secho(f"Cannot read {file}: {exc}", fg=typer.colors.RED, err=True)
74
+ raise typer.Exit(EXIT_USAGE) from exc
75
+ return files
76
+
77
+
78
+ # project.yml is a two-line machine-written file (`cassis_format_version`, `project_id`);
79
+ # match the id line directly rather than pull in a YAML parser just for this.
80
+ _PROJECT_ID_LINE = re.compile(r"^project_id:\s*['\"]?([^'\"\s]+)['\"]?\s*$")
81
+
82
+
83
+ def read_project_id_from_dir(ontology_dir: Path) -> Optional[str]:
84
+ """Return the ``project_id`` recorded in ``<ontology_dir>/project.yml``, or None."""
85
+ try:
86
+ text = (ontology_dir / "project.yml").read_text(encoding="utf-8")
87
+ except (OSError, UnicodeDecodeError) as _exc: # `as` keeps black from stripping the parens (3.14-only syntax)
88
+ return None
89
+ for line in text.splitlines():
90
+ match = _PROJECT_ID_LINE.match(line.strip())
91
+ if match:
92
+ return match.group(1)
93
+ return None
94
+
95
+
96
+ def resolve_project_id(project_id: Optional[str], ontology_dir: Path) -> str:
97
+ """Resolve the target project id, defaulting to the checkout's ``project.yml``.
98
+
99
+ Precedence: an explicit ``--project`` / ``CASSIS_PROJECT_ID`` wins; otherwise
100
+ the ``project_id`` recorded in ``<base-path>/project.yml`` (written by
101
+ ``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
+ """
105
+ from_file = False
106
+ if not project_id:
107
+ project_id = read_project_id_from_dir(ontology_dir)
108
+ from_file = project_id is not None
109
+ if not project_id:
110
+ typer.secho(
111
+ f"No project. Pass --project (or set CASSIS_PROJECT_ID), or run in a checkout whose "
112
+ f"{ontology_dir.name}/project.yml records it (written by `cassis ontology pull` or a publish).",
113
+ fg=typer.colors.RED,
114
+ err=True,
115
+ )
116
+ raise typer.Exit(EXIT_USAGE)
117
+ try:
118
+ UUID(project_id)
119
+ except ValueError:
120
+ typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
121
+ raise typer.Exit(EXIT_USAGE)
122
+ if from_file:
123
+ typer.secho(f"Using project {project_id} from {ontology_dir.name}/project.yml.", fg=typer.colors.CYAN, err=True)
124
+ return project_id
125
+
126
+
127
+ def require_api_key(api_key: Optional[str]) -> str:
128
+ """Exit 2 (usage) when no API key was provided."""
129
+ if not api_key:
130
+ typer.secho(
131
+ "No API key. Set CASSIS_API_KEY or pass --api-key "
132
+ "(create one in Cassis under Organization settings -> API keys).",
133
+ fg=typer.colors.RED,
134
+ err=True,
135
+ )
136
+ raise typer.Exit(EXIT_USAGE)
137
+ return api_key
138
+
139
+
140
+ def collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
141
+ """Resolve the ontology dir under the checkout and read its file tree.
142
+
143
+ Returns ``(files, normalized_base_path)``. Exits 2 (usage) on an empty or
144
+ missing dir, or a tree beyond the API request ceilings — all local checkout
145
+ problems, reported before anything is sent to the API.
146
+ """
147
+ base_path = base_path.strip().strip("/")
148
+ if not base_path:
149
+ typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
150
+ raise typer.Exit(EXIT_USAGE)
151
+
152
+ ontology_dir = path / Path(base_path)
153
+ if not ontology_dir.is_dir():
154
+ typer.secho(
155
+ f"No {base_path}/ directory found under {path}. "
156
+ "If the project exports to a custom path, pass it with --base-path (or CASSIS_BASE_PATH).",
157
+ fg=typer.colors.RED,
158
+ err=True,
159
+ )
160
+ raise typer.Exit(EXIT_USAGE)
161
+
162
+ files = collect_files(ontology_dir)
163
+ if not files:
164
+ typer.secho(f"No ontology files found under {ontology_dir}.", fg=typer.colors.RED, err=True)
165
+ raise typer.Exit(EXIT_USAGE)
166
+
167
+ total_bytes = sum(len(content.encode()) for content in files.values())
168
+ if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
169
+ typer.secho(
170
+ f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
171
+ f"(limits: {MAX_FILES} files / {MAX_TOTAL_BYTES // (1024 * 1024)} MB). "
172
+ "Check that --base-path points at the ontology directory, not a larger tree.",
173
+ fg=typer.colors.RED,
174
+ err=True,
175
+ )
176
+ raise typer.Exit(EXIT_USAGE)
177
+ return files, base_path
@@ -8,7 +8,6 @@ import subprocess
8
8
  import time
9
9
  from pathlib import Path
10
10
  from typing import Any, Optional
11
- from uuid import UUID
12
11
 
13
12
  import typer
14
13
  from cassis_cli.api import (
@@ -33,6 +32,7 @@ from cassis_cli.common import (
33
32
  EXIT_VALIDATION_FAILED,
34
33
  collect_tree,
35
34
  require_api_key,
35
+ resolve_project_id,
36
36
  )
37
37
 
38
38
  app = typer.Typer(no_args_is_help=True, help="Eval commands.")
@@ -144,11 +144,15 @@ def _print_summary(run: dict[str, Any]) -> None:
144
144
 
145
145
  @app.command(name="add-case")
146
146
  def add_case(
147
- project_id: str = typer.Option(
148
- ...,
147
+ path: Path = typer.Argument(
148
+ Path("."),
149
+ help="Repository checkout root (holds <base-path>/project.yml for the --project default).",
150
+ ),
151
+ project_id: Optional[str] = typer.Option(
152
+ None,
149
153
  "--project",
150
154
  envvar="CASSIS_PROJECT_ID",
151
- help="Target Cassis project ID (UUID, shown in the project's URL).",
155
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
152
156
  ),
153
157
  question: str = typer.Option(
154
158
  ...,
@@ -173,6 +177,12 @@ def add_case(
173
177
  envvar="CASSIS_API_URL",
174
178
  help="Cassis API base URL.",
175
179
  ),
180
+ base_path: str = typer.Option(
181
+ DEFAULT_BASE_PATH,
182
+ "--base-path",
183
+ envvar="CASSIS_BASE_PATH",
184
+ help="Repository directory the ontology is exported under (holds project.yml for the --project default).",
185
+ ),
176
186
  json_output: bool = typer.Option(False, "--json", help="Print the created case as raw JSON."),
177
187
  ) -> None:
178
188
  """Add a gold test case to the project's eval suite.
@@ -185,11 +195,7 @@ def add_case(
185
195
  run, 2 on usage errors, 3 on transport/API errors.
186
196
  """
187
197
  api_key = require_api_key(api_key)
188
- try:
189
- UUID(project_id)
190
- except ValueError:
191
- typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
192
- raise typer.Exit(EXIT_USAGE)
198
+ project_id = resolve_project_id(project_id, path / Path(base_path))
193
199
  if not question.strip() or not gold_sql.strip():
194
200
  typer.secho("--question and --gold-sql must not be empty.", fg=typer.colors.RED, err=True)
195
201
  raise typer.Exit(EXIT_USAGE)
@@ -221,11 +227,11 @@ def run(
221
227
  Path("."),
222
228
  help="Repository checkout root (the directory containing the ontology export path).",
223
229
  ),
224
- project_id: str = typer.Option(
225
- ...,
230
+ project_id: Optional[str] = typer.Option(
231
+ None,
226
232
  "--project",
227
233
  envvar="CASSIS_PROJECT_ID",
228
- help="Target Cassis project ID (UUID, shown in the project's URL).",
234
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
229
235
  ),
230
236
  api_key: Optional[str] = typer.Option(
231
237
  None,
@@ -272,18 +278,13 @@ def run(
272
278
  ) -> None:
273
279
  """Run the project's eval suite against your local ontology files.
274
280
 
275
- Uploads the local YAML tree and scores it in-memory — nothing is pushed or
281
+ Uploads the local ontology file tree and scores it in-memory — nothing is pushed or
276
282
  persisted in Cassis besides the eval run itself. With --branch, runs against
277
283
  an existing Cassis branch instead (no files are sent). Exits 0 when the run
278
284
  completes with every case passed, 1 on any failed case / failed run /
279
285
  invalid tree, 2 on usage errors, 3 on transport errors or --timeout.
280
286
  """
281
287
  api_key = require_api_key(api_key)
282
- try:
283
- UUID(project_id)
284
- except ValueError:
285
- typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
286
- raise typer.Exit(EXIT_USAGE)
287
288
  if branch is not None and label is not None:
288
289
  typer.secho(
289
290
  "--label cannot be used with --branch: branch runs are labelled with the branch name.",
@@ -297,6 +298,7 @@ def run(
297
298
  files, base_path = collect_tree(path, base_path)
298
299
  if label is None:
299
300
  label = _git_branch(path)
301
+ project_id = resolve_project_id(project_id, path / Path(base_path))
300
302
 
301
303
  try:
302
304
  run_record = post_eval_run_start(
@@ -6,7 +6,7 @@ identical — the backend image doesn't ship ``docs/``, so the CLI carries its o
6
6
  copy). ``pull`` writes it into the checkout as ``<base_path>/AGENTS.md`` and
7
7
  ``fmt`` keeps it canonical, so a repo-aware agent loads current Cassis modeling
8
8
  doctrine by convention. The file is managed: a banner marks it generated and the
9
- CLI overwrites local edits, exactly as ``fmt`` rewrites drifted ontology YAML.
9
+ CLI overwrites local edits, exactly as ``fmt`` rewrites drifted ontology files.
10
10
 
11
11
  Two writers manage the file — this CLI and the Cassis server's git export — and
12
12
  they may run different doctrine versions (the guide ships inside each). The
@@ -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 = 1
34
+ DOCTRINE_VERSION = 4
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
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import json
6
6
  from pathlib import Path
7
7
  from typing import List, Optional
8
- from uuid import UUID
9
8
 
10
9
  import typer
11
10
  from cassis_cli.api import (
@@ -29,7 +28,9 @@ from cassis_cli.common import (
29
28
  )
30
29
  from cassis_cli.common import collect_files as _collect_files
31
30
  from cassis_cli.common import collect_tree as _collect_tree
31
+ from cassis_cli.common import is_legacy_domain_file as _is_legacy_domain_file
32
32
  from cassis_cli.common import require_api_key as _require_api_key
33
+ from cassis_cli.common import resolve_project_id as _resolve_project_id
33
34
  from cassis_cli.guide import DOCTRINE_VERSION, GUIDE_FILENAME, guide_status, refresh_guide
34
35
 
35
36
  app = typer.Typer(no_args_is_help=True, help="Ontology commands.")
@@ -113,11 +114,11 @@ def pull(
113
114
  Path("."),
114
115
  help="Repository checkout root (the directory containing the ontology export path).",
115
116
  ),
116
- project_id: str = typer.Option(
117
- ...,
117
+ project_id: Optional[str] = typer.Option(
118
+ None,
118
119
  "--project",
119
120
  envvar="CASSIS_PROJECT_ID",
120
- help="Source Cassis project ID (UUID, shown in the project's URL).",
121
+ help="Source Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
121
122
  ),
122
123
  api_key: Optional[str] = typer.Option(
123
124
  None,
@@ -140,28 +141,24 @@ def pull(
140
141
  prune: bool = typer.Option(
141
142
  True,
142
143
  "--prune/--no-prune",
143
- help="Delete local YAML files that no longer exist in the project's ontology (default: prune).",
144
+ help="Delete local ontology files that no longer exist in the project's ontology (default: prune).",
144
145
  ),
145
146
  json_output: bool = typer.Option(False, "--json", help="Print a JSON summary of written/deleted files."),
146
147
  ) -> None:
147
148
  """Download the project's unpublished ontology into a repository checkout.
148
149
 
149
- Writes the ontology YAML tree under the export path (full sync: files are
150
- overwritten and, unless --no-prune, stale local YAML files are deleted, so
151
- the checkout ends up matching the project exactly). Review the changes with
150
+ Writes the ontology tree under the export path (full sync: files are
151
+ overwritten and, unless --no-prune, stale local ontology files are deleted,
152
+ so the checkout ends up matching the project exactly). Review the changes with
152
153
  git diff before committing. Exits 0 on success, 2 on usage errors, 3 on
153
154
  transport/API errors.
154
155
  """
155
156
  api_key = _require_api_key(api_key)
156
- try:
157
- UUID(project_id)
158
- except ValueError:
159
- typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
160
- raise typer.Exit(EXIT_USAGE)
161
157
  base_path = base_path.strip().strip("/")
162
158
  if not base_path:
163
159
  typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
164
160
  raise typer.Exit(EXIT_USAGE)
161
+ project_id = _resolve_project_id(project_id, path / Path(base_path))
165
162
 
166
163
  try:
167
164
  files = get_ontology_export(api_url=api_url, api_key=api_key, project_id=project_id)
@@ -221,6 +218,13 @@ def pull(
221
218
  if guide_written:
222
219
  summary += f"; wrote {base_path}/{GUIDE_FILENAME}"
223
220
  typer.secho(f"{summary}.", fg=typer.colors.GREEN)
221
+ migrated = sum(1 for rel in deleted if _is_legacy_domain_file(rel))
222
+ if migrated:
223
+ typer.secho(
224
+ f" Migrated {migrated} domain(s) to Markdown README.md files; "
225
+ "the old _domain.yml / _project.yml were removed. Review the diff before committing.",
226
+ fg=typer.colors.YELLOW,
227
+ )
224
228
  raise typer.Exit(EXIT_OK)
225
229
 
226
230
 
@@ -230,11 +234,11 @@ def upload(
230
234
  Path("."),
231
235
  help="Repository checkout root (the directory containing the ontology export path).",
232
236
  ),
233
- project_id: str = typer.Option(
234
- ...,
237
+ project_id: Optional[str] = typer.Option(
238
+ None,
235
239
  "--project",
236
240
  envvar="CASSIS_PROJECT_ID",
237
- help="Target Cassis project ID (UUID, shown in the project's URL).",
241
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
238
242
  ),
239
243
  api_key: Optional[str] = typer.Option(
240
244
  None,
@@ -270,12 +274,8 @@ def upload(
270
274
  usage errors, 3 on transport/API errors.
271
275
  """
272
276
  api_key = _require_api_key(api_key)
273
- try:
274
- UUID(project_id)
275
- except ValueError:
276
- typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
277
- raise typer.Exit(EXIT_USAGE)
278
277
  files, base_path = _collect_tree(path, base_path)
278
+ project_id = _resolve_project_id(project_id, path / Path(base_path))
279
279
 
280
280
  try:
281
281
  result = post_ontology_import(
@@ -377,7 +377,7 @@ def fmt(
377
377
 
378
378
  changed = result["changed_paths"]
379
379
  removed = result["removed_paths"]
380
- # The managed AGENTS.md guide is canonicalized alongside the YAML tree
380
+ # The managed AGENTS.md guide is canonicalized alongside the ontology tree
381
381
  # (it isn't in the tree, so the server round-trip above never sees it).
382
382
  # A guide stamped with a NEWER doctrine than this CLI carries is left
383
383
  # alone and does not fail --check: the repo is fine, the CLI is old.
@@ -422,6 +422,13 @@ def fmt(
422
422
  + ". Review the diff: fields Cassis does not recognize are dropped.",
423
423
  fg=typer.colors.YELLOW,
424
424
  )
425
+ migrated = sum(1 for p in removed if _is_legacy_domain_file(p))
426
+ if migrated:
427
+ typer.secho(
428
+ f"Migrated {migrated} domain(s) to Markdown README.md files; "
429
+ "the old _domain.yml / _project.yml were removed.",
430
+ fg=typer.colors.YELLOW,
431
+ )
425
432
  else:
426
433
  # Only the guide was refreshed — the "rewrote ..." line above already said so.
427
434
  typer.secho(f"✓ {len(files)} file(s) already canonical.", fg=typer.colors.GREEN)
@@ -440,11 +447,11 @@ def test(
440
447
  "-q",
441
448
  help="Natural-language question to probe (repeat for several).",
442
449
  ),
443
- project_id: str = typer.Option(
444
- ...,
450
+ project_id: Optional[str] = typer.Option(
451
+ None,
445
452
  "--project",
446
453
  envvar="CASSIS_PROJECT_ID",
447
- help="Target Cassis project ID (UUID, shown in the project's URL).",
454
+ help="Target Cassis project ID (UUID). Defaults to the id in <base-path>/project.yml.",
448
455
  ),
449
456
  api_key: Optional[str] = typer.Option(
450
457
  None,
@@ -478,12 +485,8 @@ def test(
478
485
  invalid or a probe failed, 2 on usage errors, 3 on transport errors.
479
486
  """
480
487
  api_key = _require_api_key(api_key)
481
- try:
482
- UUID(project_id)
483
- except ValueError:
484
- typer.secho(f"--project must be a project ID (UUID), got {project_id!r}.", fg=typer.colors.RED, err=True)
485
- raise typer.Exit(EXIT_USAGE)
486
488
  files, base_path = _collect_tree(path, base_path)
489
+ project_id = _resolve_project_id(project_id, path / Path(base_path))
487
490
 
488
491
  outcomes: "list[dict]" = []
489
492
  failed = False
@@ -13,13 +13,13 @@ Read this before proposing an ontology change.
13
13
  ## 1. What a Cassis ontology is
14
14
 
15
15
  The ontology is the curated business context the agent uses to translate a
16
- natural-language question into SQL. It has five kinds of object plus one
17
- project-level field, edited as YAML files under the repository's ontology
18
- directory (`cassis/` by default):
16
+ natural-language question into SQL. It has five kinds of object plus a project-level root context, edited as
17
+ files under the repository's ontology directory (`cassis/` by default) —
18
+ domains as Markdown, everything else as YAML:
19
19
 
20
20
  | Layer | What it is | Key fields |
21
21
  |---|---|---|
22
- | **Root context** (`_project.yml → context_md`) | One free-text block, always in the agent's prompt | markdown |
22
+ | **Root context** (`domains/README.md` body) | One free-text block, always in the agent's prompt | markdown |
23
23
  | **Domains** | A navigable tree grouping the business by subject area | `path`, `display_name`, `description`, `context_md` |
24
24
  | **Tables** | A physical warehouse table (introspected) or a virtual one (SQL-defined) placed in a domain | `name` (`schema.TABLE`), `description`, `synonyms`, `grain`, columns |
25
25
  | **Columns** | Enrichment on a table's columns | `description`, `unit`, `synonyms` |
@@ -31,11 +31,12 @@ On disk, the export layout is fixed — create each object in its canonical home
31
31
 
32
32
  ```
33
33
  cassis/ (the git-sync base path)
34
- _project.yml root context (context_md), project display name
35
- domains/<path>/_domain.yml one per domain, nested by path
36
- tables/<schema>/<table>.yml one per table, columns inline
37
- metrics/<name>.yml one per metric
38
- joins.yml ALL joins, one list in one file
34
+ project.yml project identity: project id + Cassis format version (written by publish / pull)
35
+ domains/README.md root domain (path ""): frontmatter (type/title/description) + context_md body
36
+ domains/<path>/README.md one per sub-domain, nested by path (same Markdown format)
37
+ tables/<schema>/<table>.yml one per table, columns inline (YAML)
38
+ metrics/<name>.yml one per metric (YAML)
39
+ joins.yml ALL joins, one list in one file (YAML)
39
40
  ```
40
41
 
41
42
  Joins and metrics are never embedded inside a table's file, and a table's file
@@ -45,7 +46,7 @@ time (run `cassis ontology fmt` to see what would be lost).
45
46
  The **published** ontology is an immutable numbered snapshot — what production
46
47
  answers from. The **unpublished** ontology is the editable state (the published
47
48
  base plus unpublished changes). In a git-synced project the repository *is* the
48
- edit surface: you edit the YAML, open a pull request, and merging syncs and
49
+ edit surface: you edit the files, open a pull request, and merging syncs and
49
50
  publishes it.
50
51
 
51
52
  Physical tables and their columns come from schema introspection — you never
@@ -151,6 +152,12 @@ its own — an empty navigational domain is just noise. Every table and every
151
152
  metric names a `domain_path` that must resolve to a domain you've declared (or the
152
153
  root, `""`).
153
154
 
155
+ Each domain is a Markdown file (`domains/<path>/README.md`, or `domains/README.md`
156
+ for the root). Its YAML frontmatter holds the structured fields — `type: Domain`,
157
+ `title:` (the display name), `description:` — and the Markdown body **is** the
158
+ domain's `context_md`. Write the display name as `title`, not `display_name`: an
159
+ unrecognized frontmatter key is dropped on sync.
160
+
154
161
  A useful convention inside `context_md`: a `## Terms` section for the domain's
155
162
  vocabulary and disambiguation, and a `## Notes` section for routing hints and
156
163
  scope caveats. Omit either if you have nothing for it.
@@ -167,7 +174,7 @@ scope caveats. Omit either if you have nothing for it.
167
174
  - Markdown **links** to related domains, using the resolvable domain path
168
175
  (`[berries](play/features/berries)`), not a bare filename.
169
176
 
170
- **Root context** — the `context_md` in `_project.yml`, injected into every
177
+ **Root context** — the body of the root domain (`domains/README.md`), injected into every
171
178
  conversation — **holds only what applies across almost every query:** corporate
172
179
  identity, the core entity hierarchy (how the central models relate — e.g. "users
173
180
  belong to companies via enrollments; primary enrollments are employees, partner
@@ -379,13 +386,37 @@ description, so a wrong example is worse than none.
379
386
 
380
387
  ---
381
388
 
382
- ## 12. Working in a git-synced repo
389
+ ## 12. Extension passes: structure before content
390
+
391
+ Adding a source or a whole subject area inverts the scoping bullet above. §11 is
392
+ written for maintenance edits, where a structural change is genuinely adjacent;
393
+ on an extension pass the tree *is* the change — every table and metric names a
394
+ `domain_path`, so content filled into a hierarchy you already doubt all has to
395
+ move. Same human opt-in, front-loaded:
396
+
397
+ 1. **Scope.** Agree what is being added, from which sources, in what order.
398
+ 2. **Structure.** Evaluate the existing hierarchy against what is arriving and
399
+ propose the tree changes — new domains, splits, moves — for approval *before*
400
+ any content. On approval, create the new domain files frontmatter-only (empty
401
+ body), so every `domain_path` resolves while you fill.
402
+ 3. **Fill bottom-up.** Column and table facts, then metrics and joins, then each
403
+ domain README body last, written from what is left over. That residue is by
404
+ construction the cross-table connective tissue domain level owns, so the order
405
+ enforces the one-home rule instead of leaving it to vigilance. Bottom-up is an
406
+ authoring order, not a holding rule: a fact learned out of order still lands at
407
+ its owning layer immediately (§11). If filling shows the approved tree is wrong
408
+ — a domain overloads, or wants to split — stop and return to 2 rather than keep
409
+ filling into it.
410
+
411
+ ---
412
+
413
+ ## 13. Working in a git-synced repo
383
414
 
384
- The repository is the source of truth. Edit the YAML, then verify before opening
415
+ The repository is the source of truth. Edit the files, then verify before opening
385
416
  a pull request — the CLI runs the same checks the platform does, from your
386
417
  checkout:
387
418
 
388
- - `cassis ontology check` — validate the files (YAML parse, round-trip, semantic
419
+ - `cassis ontology check` — validate the files (parse, round-trip, semantic
389
420
  checks); the same gate that runs on the pull request.
390
421
  - `cassis ontology fmt` — rewrite the files in canonical form, so hand or agent
391
422
  edits round-trip cleanly and any dropped/unknown fields become visible in the
@@ -1,11 +1,17 @@
1
1
  [project]
2
2
  name = "cassis-cli"
3
- version = "1.0.0"
3
+ version = "1.1.1"
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
 
@@ -1,95 +0,0 @@
1
- """Helpers shared by the `cassis` subcommands (tree collection, auth, exit codes)."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
- from typing import Optional
7
-
8
- import typer
9
-
10
- # Repository directory the ontology tree is exported under. Must match the
11
- # project's git-sync "Path" setting in Cassis (default "cassis").
12
- DEFAULT_BASE_PATH = "cassis"
13
-
14
- # Exit codes (documented in the README; stable contract for CI scripts).
15
- EXIT_OK = 0
16
- EXIT_VALIDATION_FAILED = 1
17
- EXIT_USAGE = 2
18
- EXIT_TRANSPORT = 3
19
-
20
- # Request ceilings of the /api/ci file-tree endpoints, mirrored so oversized
21
- # trees fail fast with a clear message before any upload. Source of truth:
22
- # backend/app/schemas/ci.py (the server's 422 remains the backstop).
23
- MAX_FILES = 2000
24
- MAX_TOTAL_BYTES = 5 * 1024 * 1024
25
-
26
-
27
- def collect_files(ontology_dir: Path) -> dict[str, str]:
28
- """Read every YAML file under the ontology dir, keyed by posix relpath.
29
-
30
- Exits 2 (usage) on an unreadable or non-UTF-8 file — a local checkout
31
- problem, reported before anything is sent to the API.
32
- """
33
- files: dict[str, str] = {}
34
- for pattern in ("**/*.yml", "**/*.yaml"):
35
- for file in sorted(ontology_dir.glob(pattern)):
36
- if file.is_file():
37
- try:
38
- files[file.relative_to(ontology_dir).as_posix()] = file.read_text(encoding="utf-8")
39
- except (UnicodeDecodeError, OSError) as exc:
40
- typer.secho(f"Cannot read {file}: {exc}", fg=typer.colors.RED, err=True)
41
- raise typer.Exit(EXIT_USAGE) from exc
42
- return files
43
-
44
-
45
- def require_api_key(api_key: Optional[str]) -> str:
46
- """Exit 2 (usage) when no API key was provided."""
47
- if not api_key:
48
- typer.secho(
49
- "No API key. Set CASSIS_API_KEY or pass --api-key "
50
- "(create one in Cassis under Organization settings -> API keys).",
51
- fg=typer.colors.RED,
52
- err=True,
53
- )
54
- raise typer.Exit(EXIT_USAGE)
55
- return api_key
56
-
57
-
58
- def collect_tree(path: Path, base_path: str) -> "tuple[dict[str, str], str]":
59
- """Resolve the ontology dir under the checkout and read its YAML tree.
60
-
61
- Returns ``(files, normalized_base_path)``. Exits 2 (usage) on an empty or
62
- missing dir, or a tree beyond the API request ceilings — all local checkout
63
- problems, reported before anything is sent to the API.
64
- """
65
- base_path = base_path.strip().strip("/")
66
- if not base_path:
67
- typer.secho("--base-path must not be empty.", fg=typer.colors.RED, err=True)
68
- raise typer.Exit(EXIT_USAGE)
69
-
70
- ontology_dir = path / Path(base_path)
71
- if not ontology_dir.is_dir():
72
- typer.secho(
73
- f"No {base_path}/ directory found under {path}. "
74
- "If the project exports to a custom path, pass it with --base-path (or CASSIS_BASE_PATH).",
75
- fg=typer.colors.RED,
76
- err=True,
77
- )
78
- raise typer.Exit(EXIT_USAGE)
79
-
80
- files = collect_files(ontology_dir)
81
- if not files:
82
- typer.secho(f"No YAML files found under {ontology_dir}.", fg=typer.colors.RED, err=True)
83
- raise typer.Exit(EXIT_USAGE)
84
-
85
- total_bytes = sum(len(content.encode()) for content in files.values())
86
- if len(files) > MAX_FILES or total_bytes > MAX_TOTAL_BYTES:
87
- typer.secho(
88
- f"Ontology tree too large: {len(files)} files / {total_bytes / (1024 * 1024):.1f} MB "
89
- f"(limits: {MAX_FILES} files / {MAX_TOTAL_BYTES // (1024 * 1024)} MB). "
90
- "Check that --base-path points at the ontology directory, not a larger tree.",
91
- fg=typer.colors.RED,
92
- err=True,
93
- )
94
- raise typer.Exit(EXIT_USAGE)
95
- return files, base_path