cassis-cli 2.3.0__tar.gz → 3.0.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.
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/PKG-INFO +15 -10
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/README.md +14 -9
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/api.py +16 -4
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/common.py +128 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/issues.py +44 -3
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/ontology.py +7 -2
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/ontology_design_guide.md +15 -8
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/schema.py +32 -15
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/schema_plan.py +49 -1
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/status.py +7 -1
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/pyproject.toml +1 -1
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/LICENSE +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/NOTICE +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/__init__.py +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/eval.py +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/main.py +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-2.3.0 → cassis_cli-3.0.0}/cassis_cli/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Validate, test and evaluate your Cassis ontology from your terminal, then publish it
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,7 +27,7 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
27
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. It then prints advisory **ontology quality warnings** for a tree that parsed — tables not assigned to any domain, joins/metrics pointing at unknown tables or columns, missing table/column descriptions (the same findings `ontology test` reports, without the agent run). 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** too — advisory only (the object may simply not be built or synced yet). Warnings never fail the check.
|
|
28
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.
|
|
29
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.
|
|
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.
|
|
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. It runs from a git checkout whose ontology files are committed, and the published version records that commit, so `cassis status` can tell whether a checkout matches what is live.
|
|
31
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). Pruning only deletes files that are tracked and unmodified in git (i.e. restorable with `git checkout`); untracked or locally modified files are kept and listed, and every deleted path is printed.
|
|
32
32
|
- `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.
|
|
33
33
|
- 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).
|
|
@@ -35,8 +35,10 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
35
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.
|
|
36
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
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).
|
|
38
|
-
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology to the app (`--yes` in CI). The file speaks only for the schemas it contains — pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so
|
|
38
|
+
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology to the app (`--yes` in CI). The file speaks only for the schemas it contains — pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so the schema snapshot for a change still in a PR can be planned against safely; `--write-checkout` writes the ontology files it would produce into the checkout, to commit alongside the schema change.
|
|
39
39
|
- `cassis projects list` lists the projects your API key can reach — id (what `--project` and `CASSIS_PROJECT_ID` take), name, published ontology version, and data-source dialect — so a pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
40
|
+
- DDL imports describe one schema snapshot/export file, including ordinary and materialized views; they do not replay incremental migrations. Extraction diagnostics include object names and statement locations. If extraction is incomplete, `schema plan` and `--dry-run` show the extracted inventory and exit 1; `apply`, `push`, and `--write-checkout` cannot save that result. Unknown column types are warnings when all output names are known. Plans also show object-kind, view-definition, comment, and constraint changes. `--json` preserves structured diagnostics and the server-capped inventory.
|
|
41
|
+
|
|
40
42
|
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, a schema plan waiting to be applied, and how your local git HEAD relates to the published commit (in sync / N commits ahead / diverged). `cassis status --watch` polls until the published commit matches your local HEAD — e.g. right after merging a PR whose CI publishes the ontology — instead of watching the GitHub Actions tab.
|
|
41
43
|
- `cassis issues` triages the issues Cassis raised on the project — what it found wrong while answering questions (an ontology gap, missing data) — without leaving the checkout: `issues list` (filterable by status, impact, cause and ontology domain, and showing each issue's domain so you can work through one domain at a time), `issues show <id>` for the diagnosis, suggested action and the occurrences behind it, `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it. When the fix ships through a pull request, write the `PR mention:` line `issues show` prints (`Resolves <id>`) in the PR description instead: Cassis resolves the issue when the PR merges, and `issues show` then reports how it was closed and through which PR.
|
|
42
44
|
- `cassis verify` runs the full local gate in one verb — `ontology fmt --check`, `ontology check`, `eval run` — stopping at the first failure. One command in a checkout ("is this change safe to merge?"), one job in CI. `--no-eval` skips the eval suite.
|
|
@@ -77,7 +79,8 @@ cassis ontology check /path/to/checkout
|
|
|
77
79
|
# --no-prune keeps even the tracked stale files it would otherwise delete):
|
|
78
80
|
cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
|
|
79
81
|
|
|
80
|
-
# Upload the ontology to a project and publish it immediately
|
|
82
|
+
# Upload the ontology to a project and publish it immediately (commit the
|
|
83
|
+
# changes under cassis/ first: uploads refuse uncommitted ontology files):
|
|
81
84
|
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
82
85
|
|
|
83
86
|
# Upload without publishing (the tree becomes the project's unpublished ontology, to review in Cassis):
|
|
@@ -138,6 +141,7 @@ cassis schema pull
|
|
|
138
141
|
# Preview, apply locally and push a schema update from a DDL file (DDL-only projects):
|
|
139
142
|
cassis schema plan schema.sql --complete
|
|
140
143
|
cassis schema apply schema.sql --complete # writes cassis/ locally
|
|
144
|
+
git add cassis && git commit -m "Apply schema update" # push needs the tree committed
|
|
141
145
|
cassis schema push schema.sql --complete --yes # schema + ontology to the app
|
|
142
146
|
cassis schema plan --warehouse # warehouse-connected projects: introspect instead
|
|
143
147
|
cassis schema plan future.sql --dry-run --write-checkout # plan a not-yet-deployed DDL, keep nothing server-side
|
|
@@ -151,7 +155,8 @@ cassis projects list
|
|
|
151
155
|
cassis issues analyze
|
|
152
156
|
|
|
153
157
|
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
154
|
-
cassis issues list
|
|
158
|
+
cassis issues list # open issues by default
|
|
159
|
+
cassis issues list --status all # include resolved and dismissed issues
|
|
155
160
|
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
156
161
|
|
|
157
162
|
# Work one ontology domain at a time (nested domains included):
|
|
@@ -162,8 +167,8 @@ cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8
|
|
|
162
167
|
|
|
163
168
|
# Close the loop once the fix is published (or reopen). When the fix ships in a pull
|
|
164
169
|
# request, put `Resolves <id>` in its description instead and the merge closes the issue:
|
|
165
|
-
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1
|
|
166
|
-
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1
|
|
170
|
+
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1 --published
|
|
171
|
+
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1 --reason irrelevant --detail "Outside our scope"
|
|
167
172
|
cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
168
173
|
|
|
169
174
|
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
@@ -212,8 +217,8 @@ cassis ontology fmt --check
|
|
|
212
217
|
| Code | Meaning |
|
|
213
218
|
| ---- | ------------------------------------------------------------------------------ |
|
|
214
219
|
| 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) |
|
|
215
|
-
| 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; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
216
|
-
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits, `eval run --branch` naming an ontology branch the project does not have) |
|
|
220
|
+
| 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; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
221
|
+
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits, `eval run --branch` naming an ontology branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted ontology files) |
|
|
217
222
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, or `--timeout` reached |
|
|
218
223
|
|
|
219
224
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
@@ -296,7 +301,7 @@ ontology-eval:
|
|
|
296
301
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
297
302
|
|
|
298
303
|
ontology-publish:
|
|
299
|
-
image: python:3.12-slim
|
|
304
|
+
image: python:3.12 # not -slim: the upload needs git to record the commit
|
|
300
305
|
rules:
|
|
301
306
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
302
307
|
script:
|
|
@@ -5,7 +5,7 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
5
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. It then prints advisory **ontology quality warnings** for a tree that parsed — tables not assigned to any domain, joins/metrics pointing at unknown tables or columns, missing table/column descriptions (the same findings `ontology test` reports, without the agent run). 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** too — advisory only (the object may simply not be built or synced yet). Warnings never fail the check.
|
|
6
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.
|
|
7
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.
|
|
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.
|
|
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. It runs from a git checkout whose ontology files are committed, and the published version records that commit, so `cassis status` can tell whether a checkout matches what is live.
|
|
9
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). Pruning only deletes files that are tracked and unmodified in git (i.e. restorable with `git checkout`); untracked or locally modified files are kept and listed, and every deleted path is printed.
|
|
10
10
|
- `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.
|
|
11
11
|
- 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).
|
|
@@ -13,8 +13,10 @@ Validate, test and evaluate your ontology from your terminal, then publish it. T
|
|
|
13
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.
|
|
14
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
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).
|
|
16
|
-
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology to the app (`--yes` in CI). The file speaks only for the schemas it contains — pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so
|
|
16
|
+
- `cassis schema plan <ddl>` (or `--warehouse` on a project connected to a warehouse) previews what a schema update would change before anything is applied: the source schema diff, the ontology changes Cassis will make (every change on a table placed in the ontology, with everything a drop takes with it) and warnings, terraform-style. `cassis schema apply <ddl>` (or `--plan <id>`) writes the resulting ontology files into the local checkout, app untouched, for review with `git diff`. `cassis schema push <ddl> [--publish]` pushes the new schema and the local ontology to the app (`--yes` in CI). The file speaks only for the schemas it contains — pass `--complete` when it is the project's complete source schema so schemas absent from it are treated as dropped. With `--warehouse` the server introspects the connected warehouse instead of parsing a file; the plan is always whole-source. `cassis schema plan <ddl> --dry-run` is the prepare-ahead variant: the plan is computed synchronously and nothing is kept in Cassis (no plan to apply or resume, the current plan untouched), so the schema snapshot for a change still in a PR can be planned against safely; `--write-checkout` writes the ontology files it would produce into the checkout, to commit alongside the schema change.
|
|
17
17
|
- `cassis projects list` lists the projects your API key can reach — id (what `--project` and `CASSIS_PROJECT_ID` take), name, published ontology version, and data-source dialect — so a pipeline or agent can discover the project id from the terminal instead of fishing it out of a webapp URL.
|
|
18
|
+
- DDL imports describe one schema snapshot/export file, including ordinary and materialized views; they do not replay incremental migrations. Extraction diagnostics include object names and statement locations. If extraction is incomplete, `schema plan` and `--dry-run` show the extracted inventory and exit 1; `apply`, `push`, and `--write-checkout` cannot save that result. Unknown column types are warnings when all output names are known. Plans also show object-kind, view-definition, comment, and constraint changes. `--json` preserves structured diagnostics and the server-capped inventory.
|
|
19
|
+
|
|
18
20
|
- `cassis status` shows the project's published version (number, label, git commit), whether unpublished changes await publication, the git-sync binding, a schema plan waiting to be applied, and how your local git HEAD relates to the published commit (in sync / N commits ahead / diverged). `cassis status --watch` polls until the published commit matches your local HEAD — e.g. right after merging a PR whose CI publishes the ontology — instead of watching the GitHub Actions tab.
|
|
19
21
|
- `cassis issues` triages the issues Cassis raised on the project — what it found wrong while answering questions (an ontology gap, missing data) — without leaving the checkout: `issues list` (filterable by status, impact, cause and ontology domain, and showing each issue's domain so you can work through one domain at a time), `issues show <id>` for the diagnosis, suggested action and the occurrences behind it, `issues evidence <id> <occurrence-id>` for what the agent actually saw, and `issues resolve` / `dismiss` / `reopen` once you've acted on it. When the fix ships through a pull request, write the `PR mention:` line `issues show` prints (`Resolves <id>`) in the PR description instead: Cassis resolves the issue when the PR merges, and `issues show` then reports how it was closed and through which PR.
|
|
20
22
|
- `cassis verify` runs the full local gate in one verb — `ontology fmt --check`, `ontology check`, `eval run` — stopping at the first failure. One command in a checkout ("is this change safe to merge?"), one job in CI. `--no-eval` skips the eval suite.
|
|
@@ -55,7 +57,8 @@ cassis ontology check /path/to/checkout
|
|
|
55
57
|
# --no-prune keeps even the tracked stale files it would otherwise delete):
|
|
56
58
|
cassis ontology pull --project 019f0000-0000-7000-8000-000000000000
|
|
57
59
|
|
|
58
|
-
# Upload the ontology to a project and publish it immediately
|
|
60
|
+
# Upload the ontology to a project and publish it immediately (commit the
|
|
61
|
+
# changes under cassis/ first: uploads refuse uncommitted ontology files):
|
|
59
62
|
cassis ontology upload --project 019f0000-0000-7000-8000-000000000000
|
|
60
63
|
|
|
61
64
|
# Upload without publishing (the tree becomes the project's unpublished ontology, to review in Cassis):
|
|
@@ -116,6 +119,7 @@ cassis schema pull
|
|
|
116
119
|
# Preview, apply locally and push a schema update from a DDL file (DDL-only projects):
|
|
117
120
|
cassis schema plan schema.sql --complete
|
|
118
121
|
cassis schema apply schema.sql --complete # writes cassis/ locally
|
|
122
|
+
git add cassis && git commit -m "Apply schema update" # push needs the tree committed
|
|
119
123
|
cassis schema push schema.sql --complete --yes # schema + ontology to the app
|
|
120
124
|
cassis schema plan --warehouse # warehouse-connected projects: introspect instead
|
|
121
125
|
cassis schema plan future.sql --dry-run --write-checkout # plan a not-yet-deployed DDL, keep nothing server-side
|
|
@@ -129,7 +133,8 @@ cassis projects list
|
|
|
129
133
|
cassis issues analyze
|
|
130
134
|
|
|
131
135
|
# Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
|
|
132
|
-
cassis issues list
|
|
136
|
+
cassis issues list # open issues by default
|
|
137
|
+
cassis issues list --status all # include resolved and dismissed issues
|
|
133
138
|
cassis issues show 019f0000-0000-7000-8000-0000000000e1
|
|
134
139
|
|
|
135
140
|
# Work one ontology domain at a time (nested domains included):
|
|
@@ -140,8 +145,8 @@ cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8
|
|
|
140
145
|
|
|
141
146
|
# Close the loop once the fix is published (or reopen). When the fix ships in a pull
|
|
142
147
|
# request, put `Resolves <id>` in its description instead and the merge closes the issue:
|
|
143
|
-
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1
|
|
144
|
-
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1
|
|
148
|
+
cassis issues resolve 019f0000-0000-7000-8000-0000000000e1 --published
|
|
149
|
+
cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1 --reason irrelevant --detail "Outside our scope"
|
|
145
150
|
cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
|
|
146
151
|
|
|
147
152
|
# Published version vs local checkout (add --watch to poll until your merge is published):
|
|
@@ -190,8 +195,8 @@ cassis ontology fmt --check
|
|
|
190
195
|
| Code | Meaning |
|
|
191
196
|
| ---- | ------------------------------------------------------------------------------ |
|
|
192
197
|
| 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) |
|
|
193
|
-
| 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; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
194
|
-
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits, `eval run --branch` naming an ontology branch the project does not have) |
|
|
198
|
+
| 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; issues: no such issue or occurrence in the project; issues analyze: failed or cancelled analysis run; schema plan/apply: extraction is incomplete, the plan failed (unparseable or truncated DDL), is stale or expired, the apply failed, or the project won't accept it (a plan is being applied, a DDL was given for a warehouse-connected project, or --warehouse for a DDL-only one)) |
|
|
199
|
+
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits, `eval run --branch` naming an ontology branch the project does not have, `upload` or `schema push` outside a git checkout or with uncommitted ontology files) |
|
|
195
200
|
| 3 | Transport/API error (unreachable API, invalid key, inaccessible project, unexpected response), another eval run or issue analysis already active, out of credits, or `--timeout` reached |
|
|
196
201
|
|
|
197
202
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
@@ -274,7 +279,7 @@ ontology-eval:
|
|
|
274
279
|
CASSIS_PROJECT_ID: $CASSIS_PROJECT_ID
|
|
275
280
|
|
|
276
281
|
ontology-publish:
|
|
277
|
-
image: python:3.12-slim
|
|
282
|
+
image: python:3.12 # not -slim: the upload needs git to record the commit
|
|
278
283
|
rules:
|
|
279
284
|
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
|
280
285
|
script:
|
|
@@ -204,12 +204,17 @@ def post_ontology_import(
|
|
|
204
204
|
project_id: str,
|
|
205
205
|
files: dict[str, str],
|
|
206
206
|
publish: bool,
|
|
207
|
+
git_commit_sha: str,
|
|
207
208
|
label: Optional[str] = None,
|
|
208
209
|
transport: Optional[httpx.BaseTransport] = None,
|
|
209
210
|
) -> dict[str, Any]:
|
|
210
|
-
"""POST the ontology tree to /api/ci/projects/{project_id}/ontology/import and return the response body.
|
|
211
|
+
"""POST the ontology tree to /api/ci/projects/{project_id}/ontology/import and return the response body.
|
|
212
|
+
|
|
213
|
+
``git_commit_sha`` is the commit the files were read from, recorded on the
|
|
214
|
+
version the import publishes.
|
|
215
|
+
"""
|
|
211
216
|
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/ontology/import"
|
|
212
|
-
body: dict[str, Any] = {"files": files, "publish": publish}
|
|
217
|
+
body: dict[str, Any] = {"files": files, "publish": publish, "git_commit_sha": git_commit_sha}
|
|
213
218
|
if label is not None:
|
|
214
219
|
body["label"] = label
|
|
215
220
|
try:
|
|
@@ -915,13 +920,20 @@ def post_issue_status(
|
|
|
915
920
|
project_id: str,
|
|
916
921
|
issue_id: str,
|
|
917
922
|
status: str,
|
|
923
|
+
reason: Optional[str] = None,
|
|
924
|
+
detail: Optional[str] = None,
|
|
925
|
+
confirm_published: bool = False,
|
|
918
926
|
transport: Optional[httpx.BaseTransport] = None,
|
|
919
927
|
) -> dict[str, Any]:
|
|
920
928
|
"""POST /api/ci/projects/{project_id}/issues/{issue_id}/status and return the updated issue."""
|
|
921
929
|
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues/{issue_id}/status"
|
|
922
930
|
try:
|
|
923
931
|
with _client(transport=transport) as client:
|
|
924
|
-
response = client.post(
|
|
932
|
+
response = client.post(
|
|
933
|
+
url,
|
|
934
|
+
json={"status": status, "reason": reason, "detail": detail, "confirm_published": confirm_published},
|
|
935
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
936
|
+
)
|
|
925
937
|
except httpx.HTTPError as exc:
|
|
926
938
|
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
927
939
|
|
|
@@ -1021,7 +1033,7 @@ def post_ontology_test(
|
|
|
1021
1033
|
if response.status_code == 400:
|
|
1022
1034
|
raise OntologyTestValidationError(_detail_or_text(response))
|
|
1023
1035
|
if response.status_code == 402:
|
|
1024
|
-
raise ApiError("Your organization has run out of credits. Contact your administrator to top up.")
|
|
1036
|
+
raise ApiError("Your organization has run out of credits. Contact your Cassis administrator to top up.")
|
|
1025
1037
|
if response.status_code in (403, 404):
|
|
1026
1038
|
raise _project_scope_error(response)
|
|
1027
1039
|
if response.status_code >= 400:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import subprocess
|
|
@@ -106,6 +107,133 @@ def git_file_states(directory: Path) -> Optional[tuple[set[str], set[str]]]:
|
|
|
106
107
|
return tracked, dirty
|
|
107
108
|
|
|
108
109
|
|
|
110
|
+
def require_committed_tree(path: Path, base_path: str, files: "dict[str, str]") -> str:
|
|
111
|
+
"""Return the ``HEAD`` commit that holds exactly the ontology ``files`` about to be uploaded.
|
|
112
|
+
|
|
113
|
+
An upload records this commit on the version it publishes, so the uploaded
|
|
114
|
+
files must be the ontology files of ``base_path`` at ``HEAD``: the same paths
|
|
115
|
+
with the same text, up to line endings (``collect_files`` reads CRLF as LF,
|
|
116
|
+
and line endings carry no ontology meaning). ``files`` is what ``collect_tree`` read, keyed by path
|
|
117
|
+
relative to the base path. Exits 2 (usage) when ``path`` is not a git checkout
|
|
118
|
+
with a commit, or when the two differ: a file modified, staged, renamed,
|
|
119
|
+
deleted, untracked, gitignored, missing from a sparse checkout or inside a
|
|
120
|
+
submodule. Files the upload does not send never block.
|
|
121
|
+
"""
|
|
122
|
+
ontology_dir = path / Path(base_path)
|
|
123
|
+
env = {**os.environ, "GIT_OPTIONAL_LOCKS": "0"}
|
|
124
|
+
|
|
125
|
+
def fail(reason: str) -> "typer.Exit":
|
|
126
|
+
typer.secho(
|
|
127
|
+
f"{reason} Upload the ontology from a git checkout of the repository that holds it: "
|
|
128
|
+
"Cassis records the commit each published version comes from.",
|
|
129
|
+
fg=typer.colors.RED,
|
|
130
|
+
err=True,
|
|
131
|
+
)
|
|
132
|
+
return typer.Exit(EXIT_USAGE)
|
|
133
|
+
|
|
134
|
+
def git(*args: str) -> str:
|
|
135
|
+
try:
|
|
136
|
+
proc = subprocess.run(
|
|
137
|
+
["git", *args],
|
|
138
|
+
cwd=ontology_dir,
|
|
139
|
+
env=env,
|
|
140
|
+
capture_output=True,
|
|
141
|
+
text=True,
|
|
142
|
+
errors="replace",
|
|
143
|
+
)
|
|
144
|
+
except OSError as exc:
|
|
145
|
+
raise fail(f"Could not run git ({exc}).") from exc
|
|
146
|
+
if proc.returncode != 0:
|
|
147
|
+
detail = proc.stderr.strip().splitlines()
|
|
148
|
+
raise fail(f"{path} is not a git checkout with a commit" + (f" (git: {detail[0]})." if detail else "."))
|
|
149
|
+
return proc.stdout
|
|
150
|
+
|
|
151
|
+
head = git("rev-parse", "--verify", "HEAD").strip()
|
|
152
|
+
# Run from ontology_dir, ls-tree lists paths relative to it. A submodule is a
|
|
153
|
+
# "commit" entry, so the files inside it count as not committed.
|
|
154
|
+
committed: "dict[str, str]" = {}
|
|
155
|
+
for entry in git("ls-tree", "-r", "-z", "HEAD", ".").split("\0"):
|
|
156
|
+
if not entry:
|
|
157
|
+
continue
|
|
158
|
+
meta, rel = entry.split("\t", 1)
|
|
159
|
+
_mode, kind, blob = meta.split()
|
|
160
|
+
if kind == "blob" and is_ontology_file(rel):
|
|
161
|
+
committed[rel] = blob
|
|
162
|
+
# Compare the content about to be uploaded, not the files on disk: a clean
|
|
163
|
+
# filter can make a working-tree file hash to its committed blob while the
|
|
164
|
+
# text read from it differs. Its blob id is computed here; a blob that does
|
|
165
|
+
# not match is accepted when it holds the same text up to line endings,
|
|
166
|
+
# which `collect_files` already reads as LF (a CRLF commit uploads as LF).
|
|
167
|
+
differing_blobs = {
|
|
168
|
+
p: committed[p] for p in files if p in committed and _blob_id(files[p], committed[p]) != committed[p]
|
|
169
|
+
}
|
|
170
|
+
committed_text = _read_blobs(ontology_dir, env, set(differing_blobs.values()), fail)
|
|
171
|
+
differing = sorted(
|
|
172
|
+
{p for p in files if p not in committed}
|
|
173
|
+
| {p for p, blob in differing_blobs.items() if _lf(committed_text.get(blob)) != _lf(files[p])}
|
|
174
|
+
| (committed.keys() - files.keys())
|
|
175
|
+
)
|
|
176
|
+
if differing:
|
|
177
|
+
shown = "\n".join(f" {base_path}/{p}" for p in differing[:20])
|
|
178
|
+
more = f"\n … and {len(differing) - 20} more" if len(differing) > 20 else ""
|
|
179
|
+
typer.secho(
|
|
180
|
+
f"Uncommitted changes under {base_path}/ (these files differ from HEAD):\n{shown}{more}\n"
|
|
181
|
+
f"Commit the changes under {base_path}/, then upload again.",
|
|
182
|
+
fg=typer.colors.RED,
|
|
183
|
+
err=True,
|
|
184
|
+
)
|
|
185
|
+
raise typer.Exit(EXIT_USAGE)
|
|
186
|
+
return head
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _blob_id(content: str, like: str) -> str:
|
|
190
|
+
"""The git blob id of ``content``, in the hash ``like`` (a blob id of the repository) uses."""
|
|
191
|
+
data = content.encode("utf-8")
|
|
192
|
+
digest = hashlib.sha256() if len(like) == 64 else hashlib.sha1()
|
|
193
|
+
digest.update(b"blob %d\0" % len(data) + data)
|
|
194
|
+
return digest.hexdigest()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _read_blobs(
|
|
198
|
+
cwd: Path, env: "dict[str, str]", blobs: "set[str]", fail: "Callable[[str], typer.Exit]"
|
|
199
|
+
) -> "dict[str, str]":
|
|
200
|
+
"""Read committed blobs in one ``git cat-file --batch``, keyed by blob id; non-UTF-8 blobs are left out."""
|
|
201
|
+
if not blobs:
|
|
202
|
+
return {}
|
|
203
|
+
try:
|
|
204
|
+
proc = subprocess.run(
|
|
205
|
+
["git", "cat-file", "--batch"],
|
|
206
|
+
cwd=cwd,
|
|
207
|
+
env=env,
|
|
208
|
+
input="".join(f"{blob}\n" for blob in sorted(blobs)).encode(),
|
|
209
|
+
capture_output=True,
|
|
210
|
+
)
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
raise fail(f"Could not run git ({exc}).") from exc
|
|
213
|
+
if proc.returncode != 0:
|
|
214
|
+
raise fail(f"Could not read committed files (git: {proc.stderr.decode(errors='replace').strip()}).")
|
|
215
|
+
texts: "dict[str, str]" = {}
|
|
216
|
+
out, pos = proc.stdout, 0
|
|
217
|
+
while pos < len(out):
|
|
218
|
+
header_end = out.index(b"\n", pos)
|
|
219
|
+
fields = out[pos:header_end].split()
|
|
220
|
+
pos = header_end + 1
|
|
221
|
+
if len(fields) != 3: # "<id> missing"
|
|
222
|
+
continue
|
|
223
|
+
size = int(fields[2])
|
|
224
|
+
try:
|
|
225
|
+
texts[fields[0].decode()] = out[pos : pos + size].decode("utf-8")
|
|
226
|
+
except UnicodeDecodeError:
|
|
227
|
+
pass
|
|
228
|
+
pos += size + 1 # the content is followed by a newline
|
|
229
|
+
return texts
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _lf(text: Optional[str]) -> Optional[str]:
|
|
233
|
+
"""``text`` with CRLF and CR line endings read as LF, as ``Path.read_text`` reads them."""
|
|
234
|
+
return None if text is None else text.replace("\r\n", "\n").replace("\r", "\n")
|
|
235
|
+
|
|
236
|
+
|
|
109
237
|
def sync_ontology_tree(
|
|
110
238
|
ontology_dir: Path,
|
|
111
239
|
files: "dict[str, str]",
|
|
@@ -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 =
|
|
34
|
+
DOCTRINE_VERSION = 8
|
|
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
|
|
@@ -10,6 +10,7 @@ conversations that arrived since the last pass, without waiting for the nightly
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import sys
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any, Optional
|
|
15
16
|
|
|
@@ -45,7 +46,8 @@ from cassis_cli.common import (
|
|
|
45
46
|
|
|
46
47
|
app = typer.Typer(no_args_is_help=True, help="Triage the project's issues.")
|
|
47
48
|
|
|
48
|
-
STATUSES = ("open", "resolved", "dismissed")
|
|
49
|
+
STATUSES = ("open", "resolved", "dismissed", "all")
|
|
50
|
+
DISMISS_REASONS = ("invalid", "irrelevant", "duplicate", "declined")
|
|
49
51
|
IMPACTS = ("wrong_answer", "unreliable_answer", "no_answer", "inefficient")
|
|
50
52
|
CAUSES = ("ontology_gap", "missing_data")
|
|
51
53
|
|
|
@@ -114,7 +116,7 @@ def _field(label: str, value: Any, *, blank_line: bool = False) -> None:
|
|
|
114
116
|
|
|
115
117
|
@app.command(name="list")
|
|
116
118
|
def list_issues(
|
|
117
|
-
status: Optional[str] = typer.Option(
|
|
119
|
+
status: Optional[str] = typer.Option("open", "--status", help=f"Filter by status ({', '.join(STATUSES)})."),
|
|
118
120
|
impact: Optional[str] = typer.Option(None, "--impact", help=f"Filter by impact ({', '.join(IMPACTS)})."),
|
|
119
121
|
cause: Optional[str] = typer.Option(None, "--cause", help=f"Filter by cause ({', '.join(CAUSES)})."),
|
|
120
122
|
domain: Optional[str] = typer.Option(
|
|
@@ -212,9 +214,23 @@ def show(
|
|
|
212
214
|
f"{issue.get('occurrence_count_cache', len(occurrences))} occurrence(s)"
|
|
213
215
|
)
|
|
214
216
|
_field("Domains", ", ".join(issue.get("domains") or []) or None)
|
|
215
|
-
if issue.get("resolved_via"):
|
|
217
|
+
if issue.get("resolved_via") and issue.get("status") != "open":
|
|
216
218
|
ref = issue.get("resolved_ref")
|
|
217
219
|
_field("Resolved via", f"{issue['resolved_via']}{f' ({ref})' if ref else ''}")
|
|
220
|
+
_field("Dismiss reason", issue.get("dismiss_reason"))
|
|
221
|
+
_field("Dismiss detail", issue.get("dismiss_detail"))
|
|
222
|
+
if issue.get("fix_applied_at"):
|
|
223
|
+
_field("Fix", "Applied; awaiting publication")
|
|
224
|
+
for event in issue.get("status_history") or []:
|
|
225
|
+
_field(
|
|
226
|
+
"History",
|
|
227
|
+
f"{event.get('created_at')} {event.get('from_status')} -> {event.get('to_status')} "
|
|
228
|
+
f"{event.get('via')} {event.get('actor_kind')}"
|
|
229
|
+
+ (f" {event['reason']}" if event.get("reason") else "")
|
|
230
|
+
+ (f" {event['detail']}" if event.get("detail") else "")
|
|
231
|
+
+ (f" {event['ref']}" if event.get("ref") else ""),
|
|
232
|
+
)
|
|
233
|
+
|
|
218
234
|
_field("Description", issue.get("description"), blank_line=True)
|
|
219
235
|
_field("Suggested action", issue.get("suggested_action"), blank_line=True)
|
|
220
236
|
typer.echo("")
|
|
@@ -283,6 +299,9 @@ def _set_status(
|
|
|
283
299
|
api_key: Optional[str],
|
|
284
300
|
api_url: str,
|
|
285
301
|
base_path: str,
|
|
302
|
+
reason: Optional[str] = None,
|
|
303
|
+
detail: Optional[str] = None,
|
|
304
|
+
confirm_published: bool = False,
|
|
286
305
|
) -> None:
|
|
287
306
|
"""Post a new status for the issue and confirm it on one line."""
|
|
288
307
|
api_key = require_api_key(api_key)
|
|
@@ -295,6 +314,9 @@ def _set_status(
|
|
|
295
314
|
project_id=project_id,
|
|
296
315
|
issue_id=issue_id,
|
|
297
316
|
status=status,
|
|
317
|
+
reason=reason,
|
|
318
|
+
detail=detail,
|
|
319
|
+
confirm_published=confirm_published,
|
|
298
320
|
)
|
|
299
321
|
except IssueNotFoundError as exc:
|
|
300
322
|
raise _not_found_failure(exc) from exc
|
|
@@ -307,6 +329,11 @@ def _set_status(
|
|
|
307
329
|
|
|
308
330
|
@app.command()
|
|
309
331
|
def resolve(
|
|
332
|
+
published: bool = typer.Option(
|
|
333
|
+
False,
|
|
334
|
+
"--published",
|
|
335
|
+
help="Confirm the fix is in the published ontology; required without an interactive terminal.",
|
|
336
|
+
),
|
|
310
337
|
issue_id: str = typer.Argument(..., help="Id of the issue to resolve (from `cassis issues list`)."),
|
|
311
338
|
path: Path = _PATH_OPTION,
|
|
312
339
|
project_id: Optional[str] = _PROJECT_OPTION,
|
|
@@ -322,9 +349,18 @@ def resolve(
|
|
|
322
349
|
go through a PR. Exits 0 on success, 1 when the issue does not exist in
|
|
323
350
|
the project, 2 on usage errors, 3 on transport/API errors.
|
|
324
351
|
"""
|
|
352
|
+
if not published:
|
|
353
|
+
if not sys.stdin.isatty():
|
|
354
|
+
typer.secho(
|
|
355
|
+
"Use --published to confirm the fix is in the published ontology.", fg=typer.colors.RED, err=True
|
|
356
|
+
)
|
|
357
|
+
raise typer.Exit(EXIT_USAGE)
|
|
358
|
+
if not typer.confirm("Is the fix already in the published ontology?"):
|
|
359
|
+
raise typer.Exit(EXIT_USAGE)
|
|
325
360
|
_set_status(
|
|
326
361
|
issue_id=issue_id,
|
|
327
362
|
status="resolved",
|
|
363
|
+
confirm_published=True,
|
|
328
364
|
path=path,
|
|
329
365
|
project_id=project_id,
|
|
330
366
|
api_key=api_key,
|
|
@@ -335,6 +371,8 @@ def resolve(
|
|
|
335
371
|
|
|
336
372
|
@app.command()
|
|
337
373
|
def dismiss(
|
|
374
|
+
reason: str = typer.Option(..., "--reason", help=f"Why this issue is dismissed: {', '.join(DISMISS_REASONS)}."),
|
|
375
|
+
detail: Optional[str] = typer.Option(None, "--detail", help="Additional context for the dismissal."),
|
|
338
376
|
issue_id: str = typer.Argument(..., help="Id of the issue to dismiss (from `cassis issues list`)."),
|
|
339
377
|
path: Path = _PATH_OPTION,
|
|
340
378
|
project_id: Optional[str] = _PROJECT_OPTION,
|
|
@@ -347,9 +385,12 @@ def dismiss(
|
|
|
347
385
|
Exits 0 on success, 1 when the issue does not exist in the project, 2 on
|
|
348
386
|
usage errors, 3 on transport/API errors.
|
|
349
387
|
"""
|
|
388
|
+
_validate_choice(reason, DISMISS_REASONS, "--reason")
|
|
350
389
|
_set_status(
|
|
351
390
|
issue_id=issue_id,
|
|
352
391
|
status="dismissed",
|
|
392
|
+
reason=reason,
|
|
393
|
+
detail=detail,
|
|
353
394
|
path=path,
|
|
354
395
|
project_id=project_id,
|
|
355
396
|
api_key=api_key,
|
|
@@ -29,6 +29,7 @@ from cassis_cli.common import (
|
|
|
29
29
|
from cassis_cli.common import collect_tree as _collect_tree
|
|
30
30
|
from cassis_cli.common import is_legacy_domain_file as _is_legacy_domain_file
|
|
31
31
|
from cassis_cli.common import require_api_key as _require_api_key
|
|
32
|
+
from cassis_cli.common import require_committed_tree as _require_committed_tree
|
|
32
33
|
from cassis_cli.common import resolve_project_id as _resolve_project_id
|
|
33
34
|
from cassis_cli.common import sync_ontology_tree as _sync_ontology_tree
|
|
34
35
|
from cassis_cli.guide import DOCTRINE_VERSION, GUIDE_FILENAME, guide_status, refresh_guide
|
|
@@ -332,11 +333,14 @@ def upload(
|
|
|
332
333
|
|
|
333
334
|
Replaces the project's unpublished ontology with the local tree (full
|
|
334
335
|
replace) and, unless --no-publish is passed, publishes it immediately as a
|
|
335
|
-
new version.
|
|
336
|
-
|
|
336
|
+
new version. The checkout must be a git repository whose ontology files
|
|
337
|
+
match HEAD: the published version records that commit. Exits 0 on success,
|
|
338
|
+
1 when the tree fails validation, 2 on usage errors (including uncommitted
|
|
339
|
+
changes under the base path), 3 on transport/API errors.
|
|
337
340
|
"""
|
|
338
341
|
api_key = _require_api_key(api_key)
|
|
339
342
|
files, base_path = _collect_tree(path, base_path)
|
|
343
|
+
head = _require_committed_tree(path, base_path, files)
|
|
340
344
|
project_id = _resolve_project_id(project_id, path / Path(base_path))
|
|
341
345
|
|
|
342
346
|
try:
|
|
@@ -346,6 +350,7 @@ def upload(
|
|
|
346
350
|
project_id=project_id,
|
|
347
351
|
files=files,
|
|
348
352
|
publish=publish,
|
|
353
|
+
git_commit_sha=head,
|
|
349
354
|
label=label,
|
|
350
355
|
)
|
|
351
356
|
except AuthError as exc:
|
|
@@ -484,14 +484,21 @@ checkout:
|
|
|
484
484
|
Merging the pull request syncs and publishes the ontology; nothing reaches
|
|
485
485
|
production answers until then.
|
|
486
486
|
|
|
487
|
-
When the change
|
|
488
|
-
|
|
489
|
-
`
|
|
490
|
-
`cassis issues
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
487
|
+
When the change fixes Cassis issues, say so in the pull request instead of
|
|
488
|
+
closing them by hand. Write one mention per issue in the PR description:
|
|
489
|
+
`Resolves <id>` (the full id, or its first 13 characters or more; `Closes` and
|
|
490
|
+
`Fixes` are accepted too). `cassis issues show <id>` prints the exact line to
|
|
491
|
+
copy, as `PR mention:`. Cassis resolves each mentioned issue when the pull
|
|
492
|
+
request merges and the ontology is published, and records the pull request on
|
|
493
|
+
the issue, so the resolution carries the change that earned it.
|
|
494
|
+
|
|
495
|
+
Resolving by hand is for outcomes that never go through a pull request:
|
|
496
|
+
`cassis issues resolve <id>`, or `update_issue_status` over MCP. It needs the
|
|
497
|
+
editor or admin role, changes no ontology, and belongs to whoever owns the
|
|
498
|
+
project. Confirm first that the published version really contains the fix
|
|
499
|
+
(`cassis status`, or `get_project_status`). Never resolve before publication,
|
|
500
|
+
and never silently: an issue nobody resolves stays in the triage queue and
|
|
501
|
+
reads as a gap that was never fixed.
|
|
495
502
|
|
|
496
503
|
---
|
|
497
504
|
|
|
@@ -46,10 +46,11 @@ from cassis_cli.common import (
|
|
|
46
46
|
collect_tree,
|
|
47
47
|
poll_until,
|
|
48
48
|
require_api_key,
|
|
49
|
+
require_committed_tree,
|
|
49
50
|
resolve_project_id,
|
|
50
51
|
sync_ontology_tree,
|
|
51
52
|
)
|
|
52
|
-
from cassis_cli.schema_plan import plan_counts, plan_is_empty, render_plan
|
|
53
|
+
from cassis_cli.schema_plan import plan_counts, plan_extraction_complete, plan_is_empty, render_plan
|
|
53
54
|
|
|
54
55
|
app = typer.Typer(help="Pull the data source's schema; plan, apply (locally) and push a schema update from DDL.")
|
|
55
56
|
|
|
@@ -219,7 +220,7 @@ def _require_one_source(
|
|
|
219
220
|
@app.command()
|
|
220
221
|
def plan(
|
|
221
222
|
ddl_file: Optional[Path] = typer.Argument(
|
|
222
|
-
None, help="Path to the DDL file (.sql, .ddl, .txt)
|
|
223
|
+
None, help="Path to the DDL file (.sql, .ddl, .txt) describing tables and views. Or --warehouse."
|
|
223
224
|
),
|
|
224
225
|
warehouse: bool = _WAREHOUSE_OPTION,
|
|
225
226
|
path: Path = _PATH_OPTION,
|
|
@@ -258,7 +259,7 @@ def plan(
|
|
|
258
259
|
|
|
259
260
|
--dry-run is the prepare-ahead gesture: the plan is computed synchronously
|
|
260
261
|
and nothing is kept server-side, so it works for a schema change that is
|
|
261
|
-
still a PR (
|
|
262
|
+
still a PR (the desired schema snapshot) and leaves the project's current plan
|
|
262
263
|
alone. --write-checkout then writes the resulting ontology files into the
|
|
263
264
|
checkout, to commit next to the schema change; nothing is pushed.
|
|
264
265
|
"""
|
|
@@ -282,6 +283,10 @@ def plan(
|
|
|
282
283
|
json_output=json_output,
|
|
283
284
|
out=out,
|
|
284
285
|
)
|
|
286
|
+
if not plan_extraction_complete(preview):
|
|
287
|
+
if json_output:
|
|
288
|
+
typer.echo(json.dumps(preview, indent=2))
|
|
289
|
+
raise typer.Exit(EXIT_VALIDATION_FAILED)
|
|
285
290
|
if write_checkout:
|
|
286
291
|
ontology_dir = path / base_path.strip().strip("/")
|
|
287
292
|
written, deleted, _kept = _write_checkout(ontology_dir, preview["files"], json_output=json_output)
|
|
@@ -309,7 +314,7 @@ def plan(
|
|
|
309
314
|
)
|
|
310
315
|
if json_output:
|
|
311
316
|
typer.echo(json.dumps(record, indent=2))
|
|
312
|
-
if record.get("status") != "ready":
|
|
317
|
+
if record.get("status") != "ready" or not plan_extraction_complete(record):
|
|
313
318
|
raise typer.Exit(EXIT_VALIDATION_FAILED)
|
|
314
319
|
raise typer.Exit(EXIT_OK)
|
|
315
320
|
|
|
@@ -366,7 +371,7 @@ def apply(
|
|
|
366
371
|
timeout=timeout,
|
|
367
372
|
json_output=json_output,
|
|
368
373
|
)
|
|
369
|
-
if record.get("status") != "ready":
|
|
374
|
+
if record.get("status") != "ready" or not plan_extraction_complete(record):
|
|
370
375
|
raise typer.Exit(EXIT_VALIDATION_FAILED)
|
|
371
376
|
try:
|
|
372
377
|
checkout = get_schema_plan_checkout(
|
|
@@ -430,15 +435,17 @@ def push(
|
|
|
430
435
|
Two steps, in order: the new schema (a DDL file, or the connected
|
|
431
436
|
warehouse with --warehouse) is planned and applied server-side (new
|
|
432
437
|
schema version, tracked schema updated, ontology edits the plan lists),
|
|
433
|
-
then the local ontology tree replaces the project's unpublished ontology
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
+
then the local ontology tree replaces the project's unpublished ontology.
|
|
439
|
+
Commit the tree `cassis schema apply` wrote, and any hand edits, before
|
|
440
|
+
pushing: the push refuses uncommitted changes under the base path, and a
|
|
441
|
+
published version records the commit. Pass --publish to publish it as a
|
|
442
|
+
new version. Exits 0 when pushed, 1 when the plan failed / is stale or the
|
|
443
|
+
upload was rejected, 2 on usage errors, 3 on transport errors.
|
|
438
444
|
"""
|
|
439
445
|
api_key = require_api_key(api_key)
|
|
440
446
|
_require_one_source(ddl_file, warehouse)
|
|
441
447
|
files, base_path = collect_tree(path, base_path)
|
|
448
|
+
head = require_committed_tree(path, base_path, files)
|
|
442
449
|
resolved_project = resolve_project_id(project_id, path / Path(base_path), quiet=json_output)
|
|
443
450
|
assert resolved_project is not None
|
|
444
451
|
|
|
@@ -454,7 +461,7 @@ def push(
|
|
|
454
461
|
timeout=timeout,
|
|
455
462
|
json_output=json_output,
|
|
456
463
|
)
|
|
457
|
-
if record.get("status") != "ready":
|
|
464
|
+
if record.get("status") != "ready" or not plan_extraction_complete(record):
|
|
458
465
|
raise typer.Exit(EXIT_VALIDATION_FAILED)
|
|
459
466
|
_require_marker_matches(path / Path(base_path), record)
|
|
460
467
|
if not yes:
|
|
@@ -502,7 +509,13 @@ def push(
|
|
|
502
509
|
typer.echo(f"Uploading {len(files)} ontology file(s)…", err=True)
|
|
503
510
|
try:
|
|
504
511
|
upload = post_ontology_import(
|
|
505
|
-
api_url=api_url,
|
|
512
|
+
api_url=api_url,
|
|
513
|
+
api_key=api_key,
|
|
514
|
+
project_id=resolved_project,
|
|
515
|
+
files=files,
|
|
516
|
+
publish=publish,
|
|
517
|
+
git_commit_sha=head,
|
|
518
|
+
label=label,
|
|
506
519
|
)
|
|
507
520
|
except UploadValidationError as exc:
|
|
508
521
|
typer.secho("Ontology upload rejected:", fg=typer.colors.RED, bold=True, err=True)
|
|
@@ -568,8 +581,8 @@ def _require_checkout_in_sync(
|
|
|
568
581
|
if len(differing) > 10:
|
|
569
582
|
typer.secho(f" … {len(differing) - 10} more", fg=typer.colors.RED, err=True)
|
|
570
583
|
typer.secho(
|
|
571
|
-
"Bring the checkout up to date first (`cassis ontology pull`), or
|
|
572
|
-
"(`cassis ontology upload --no-publish`), then apply again. `--force` overwrites the local files.",
|
|
584
|
+
"Bring the checkout up to date first (`cassis ontology pull`), or commit your local edits and push "
|
|
585
|
+
"them (`cassis ontology upload --no-publish`), then apply again. `--force` overwrites the local files.",
|
|
573
586
|
fg=typer.colors.RED,
|
|
574
587
|
err=True,
|
|
575
588
|
)
|
|
@@ -764,7 +777,9 @@ def _plan(
|
|
|
764
777
|
raise typer.Exit(EXIT_USAGE) from exc
|
|
765
778
|
if record.get("status") == "ready":
|
|
766
779
|
render_plan(record, err=json_output)
|
|
767
|
-
if
|
|
780
|
+
if not plan_extraction_complete(record):
|
|
781
|
+
pass # The renderer already explains the blocking diagnostics.
|
|
782
|
+
elif plan_is_empty(record):
|
|
768
783
|
typer.secho("✓ Schema is up to date.", fg=typer.colors.GREEN, err=json_output)
|
|
769
784
|
else:
|
|
770
785
|
typer.secho(f"✓ Plan ready: {plan_id}", fg=typer.colors.GREEN, err=json_output)
|
|
@@ -814,6 +829,8 @@ def _preview(
|
|
|
814
829
|
render_plan(record, err=json_output)
|
|
815
830
|
for warning in preview.get("warnings") or []:
|
|
816
831
|
typer.secho(f" warning: {warning}", fg=typer.colors.YELLOW, err=True)
|
|
832
|
+
if not plan_extraction_complete(record):
|
|
833
|
+
return preview
|
|
817
834
|
if plan_is_empty(record):
|
|
818
835
|
typer.secho("✓ Schema is up to date (dry run, nothing kept).", fg=typer.colors.GREEN, err=json_output)
|
|
819
836
|
else:
|
|
@@ -7,6 +7,7 @@ keeps stdout to the JSON record alone.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import json
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import typer
|
|
@@ -30,6 +31,39 @@ def _line(text: str, *, err: bool, mark: str | None = None, bold: bool = False)
|
|
|
30
31
|
def render_plan(plan: dict[str, Any], *, err: bool) -> None:
|
|
31
32
|
"""Print the plan: schema diff, ontology changes with their cascade, warnings, footer."""
|
|
32
33
|
document = plan.get("document") or {}
|
|
34
|
+
diagnostics = document.get("diagnostics") or []
|
|
35
|
+
if diagnostics:
|
|
36
|
+
_line(f"Extraction diagnostics ({len(diagnostics)})", err=err, bold=True)
|
|
37
|
+
for diagnostic in diagnostics:
|
|
38
|
+
location = diagnostic.get("object_name") or ""
|
|
39
|
+
if diagnostic.get("line") is not None:
|
|
40
|
+
location += f" line {diagnostic['line']}"
|
|
41
|
+
if diagnostic.get("column") is not None:
|
|
42
|
+
location += f":{diagnostic['column']}"
|
|
43
|
+
severity = diagnostic.get("severity", "info")
|
|
44
|
+
_line(
|
|
45
|
+
f" {severity}: {diagnostic.get('code')} {location.strip()}: {diagnostic.get('message')}",
|
|
46
|
+
err=err,
|
|
47
|
+
mark="-" if severity == "error" else "~",
|
|
48
|
+
)
|
|
49
|
+
if not plan_extraction_complete(plan):
|
|
50
|
+
objects = document.get("extracted_objects") or []
|
|
51
|
+
_line(f"Extracted objects ({document.get('extracted_object_count', len(objects))})", err=err, bold=True)
|
|
52
|
+
for obj in objects[:50]:
|
|
53
|
+
_line(
|
|
54
|
+
f" {obj.get('schema_name')}.{obj.get('name')} {obj.get('table_type')}"
|
|
55
|
+
f" ({obj.get('column_count')} columns)",
|
|
56
|
+
err=err,
|
|
57
|
+
)
|
|
58
|
+
if len(objects) > 50 or document.get("extracted_objects_truncated"):
|
|
59
|
+
_line(" … list capped; use --json or --out for the full returned inventory", err=err)
|
|
60
|
+
_line(
|
|
61
|
+
"Schema extraction is incomplete. Resolve the errors and plan again; nothing can be applied.",
|
|
62
|
+
err=err,
|
|
63
|
+
mark="-",
|
|
64
|
+
bold=True,
|
|
65
|
+
)
|
|
66
|
+
return
|
|
33
67
|
diff = document.get("schema_diff") or {}
|
|
34
68
|
tables = diff.get("tables") or []
|
|
35
69
|
_line(f"Schema diff ({len(tables)} table{'s' if len(tables) != 1 else ''})", err=err, bold=True)
|
|
@@ -44,6 +78,11 @@ def render_plan(plan: dict[str, Any], *, err: bool) -> None:
|
|
|
44
78
|
if t.get("in_ontology"):
|
|
45
79
|
suffix += " (in ontology)"
|
|
46
80
|
_line(f" {mark} {name}{suffix}", err=err, mark=mark)
|
|
81
|
+
for metadata in t.get("metadata_changes") or []:
|
|
82
|
+
_line(f" ~ {metadata.get('field')}", err=err, mark="~")
|
|
83
|
+
for label, value in (("from", metadata.get("old_value")), ("to", metadata.get("new_value"))):
|
|
84
|
+
formatted = value if isinstance(value, str) else json.dumps(value, indent=2, ensure_ascii=False)
|
|
85
|
+
_line(f" {label}: " + formatted.replace("\n", "\n "), err=err)
|
|
47
86
|
for c in t.get("columns") or []:
|
|
48
87
|
cmark = _MARK.get(c.get("kind", ""), "~")
|
|
49
88
|
if c.get("kind") == "renamed":
|
|
@@ -127,4 +166,13 @@ def plan_counts(plan: dict[str, Any]) -> tuple[int, int, int, int]:
|
|
|
127
166
|
def plan_is_empty(plan: dict[str, Any]) -> bool:
|
|
128
167
|
document = plan.get("document") or {}
|
|
129
168
|
diff = document.get("schema_diff") or {}
|
|
130
|
-
return
|
|
169
|
+
return plan_extraction_complete(plan) and not (
|
|
170
|
+
diff.get("tables") or document.get("ontology_changes") or document.get("warnings")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def plan_extraction_complete(plan: dict[str, Any]) -> bool:
|
|
175
|
+
document = plan.get("document") or {}
|
|
176
|
+
return document.get("extraction_complete") is not False and not any(
|
|
177
|
+
d.get("severity") == "error" for d in document.get("diagnostics") or []
|
|
178
|
+
)
|
|
@@ -88,7 +88,13 @@ def _render(status_record: "dict[str, Any]", comparison_text: str) -> None:
|
|
|
88
88
|
else:
|
|
89
89
|
typer.echo("Git sync: not configured")
|
|
90
90
|
schema_plan = status_record.get("schema_plan")
|
|
91
|
-
if
|
|
91
|
+
if (
|
|
92
|
+
isinstance(schema_plan, dict)
|
|
93
|
+
and schema_plan.get("status") == "ready"
|
|
94
|
+
and schema_plan.get("extraction_complete", True) is False
|
|
95
|
+
):
|
|
96
|
+
typer.echo("Schema plan: blocked by incomplete extraction (create a new plan after resolving the errors)")
|
|
97
|
+
elif isinstance(schema_plan, dict) and schema_plan.get("status") == "ready":
|
|
92
98
|
changes = schema_plan.get("ontology_changes")
|
|
93
99
|
changes_text = f", {changes} ontology change(s)" if changes is not None else ""
|
|
94
100
|
typer.echo(f"Schema plan: ready{changes_text} (cassis schema apply --plan {schema_plan.get('id')})")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|