cassis-cli 2.3.0__tar.gz → 2.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cassis-cli
3
- Version: 2.3.0
3
+ Version: 2.4.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
@@ -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 a dbt model or migration 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.
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.
@@ -151,7 +153,8 @@ cassis projects list
151
153
  cassis issues analyze
152
154
 
153
155
  # Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
154
- cassis issues list --status open
156
+ cassis issues list # open issues by default
157
+ cassis issues list --status all # include resolved and dismissed issues
155
158
  cassis issues show 019f0000-0000-7000-8000-0000000000e1
156
159
 
157
160
  # Work one ontology domain at a time (nested domains included):
@@ -162,8 +165,8 @@ cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8
162
165
 
163
166
  # Close the loop once the fix is published (or reopen). When the fix ships in a pull
164
167
  # 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
168
+ cassis issues resolve 019f0000-0000-7000-8000-0000000000e1 --published
169
+ cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1 --reason irrelevant --detail "Outside our scope"
167
170
  cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
168
171
 
169
172
  # Published version vs local checkout (add --watch to poll until your merge is published):
@@ -212,7 +215,7 @@ cassis ontology fmt --check
212
215
  | Code | Meaning |
213
216
  | ---- | ------------------------------------------------------------------------------ |
214
217
  | 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)) |
218
+ | 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)) |
216
219
  | 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) |
217
220
  | 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
221
 
@@ -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 a dbt model or migration 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.
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.
@@ -129,7 +131,8 @@ cassis projects list
129
131
  cassis issues analyze
130
132
 
131
133
  # Triage the issues Cassis raised (filter by --status/--impact/--cause; --json for raw output):
132
- cassis issues list --status open
134
+ cassis issues list # open issues by default
135
+ cassis issues list --status all # include resolved and dismissed issues
133
136
  cassis issues show 019f0000-0000-7000-8000-0000000000e1
134
137
 
135
138
  # Work one ontology domain at a time (nested domains included):
@@ -140,8 +143,8 @@ cassis issues evidence 019f0000-0000-7000-8000-0000000000e1 019f0000-0000-7000-8
140
143
 
141
144
  # Close the loop once the fix is published (or reopen). When the fix ships in a pull
142
145
  # 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
146
+ cassis issues resolve 019f0000-0000-7000-8000-0000000000e1 --published
147
+ cassis issues dismiss 019f0000-0000-7000-8000-0000000000e1 --reason irrelevant --detail "Outside our scope"
145
148
  cassis issues reopen 019f0000-0000-7000-8000-0000000000e1
146
149
 
147
150
  # Published version vs local checkout (add --watch to poll until your merge is published):
@@ -190,7 +193,7 @@ cassis ontology fmt --check
190
193
  | Code | Meaning |
191
194
  | ---- | ------------------------------------------------------------------------------ |
192
195
  | 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)) |
196
+ | 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)) |
194
197
  | 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) |
195
198
  | 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
199
 
@@ -915,13 +915,20 @@ def post_issue_status(
915
915
  project_id: str,
916
916
  issue_id: str,
917
917
  status: str,
918
+ reason: Optional[str] = None,
919
+ detail: Optional[str] = None,
920
+ confirm_published: bool = False,
918
921
  transport: Optional[httpx.BaseTransport] = None,
919
922
  ) -> dict[str, Any]:
920
923
  """POST /api/ci/projects/{project_id}/issues/{issue_id}/status and return the updated issue."""
921
924
  url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/issues/{issue_id}/status"
922
925
  try:
923
926
  with _client(transport=transport) as client:
924
- response = client.post(url, json={"status": status}, headers={"Authorization": f"Bearer {api_key}"})
927
+ response = client.post(
928
+ url,
929
+ json={"status": status, "reason": reason, "detail": detail, "confirm_published": confirm_published},
930
+ headers={"Authorization": f"Bearer {api_key}"},
931
+ )
925
932
  except httpx.HTTPError as exc:
926
933
  raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
927
934
 
@@ -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 = 7
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(None, "--status", help=f"Filter by status ({', '.join(STATUSES)})."),
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,
@@ -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 came from a Cassis issue, merging is still not the last step.
488
- Confirm the published version contains it (`cassis status`, or
489
- `get_project_status` over MCP), then propose resolving that issue —
490
- `cassis issues resolve <id>`, or `update_issue_status`. Propose it to whoever
491
- owns the project: resolving needs the editor or admin role, and it records an
492
- outcome without changing any ontology. Never resolve before publication, and
493
- never silently — an issue nobody resolves stays in the triage queue and reads
494
- as a gap that was never fixed.
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
 
@@ -49,7 +49,7 @@ from cassis_cli.common import (
49
49
  resolve_project_id,
50
50
  sync_ontology_tree,
51
51
  )
52
- from cassis_cli.schema_plan import plan_counts, plan_is_empty, render_plan
52
+ from cassis_cli.schema_plan import plan_counts, plan_extraction_complete, plan_is_empty, render_plan
53
53
 
54
54
  app = typer.Typer(help="Pull the data source's schema; plan, apply (locally) and push a schema update from DDL.")
55
55
 
@@ -219,7 +219,7 @@ def _require_one_source(
219
219
  @app.command()
220
220
  def plan(
221
221
  ddl_file: Optional[Path] = typer.Argument(
222
- None, help="Path to the DDL file (.sql, .ddl, .txt) containing CREATE TABLE statements. Or --warehouse."
222
+ None, help="Path to the DDL file (.sql, .ddl, .txt) describing tables and views. Or --warehouse."
223
223
  ),
224
224
  warehouse: bool = _WAREHOUSE_OPTION,
225
225
  path: Path = _PATH_OPTION,
@@ -258,7 +258,7 @@ def plan(
258
258
 
259
259
  --dry-run is the prepare-ahead gesture: the plan is computed synchronously
260
260
  and nothing is kept server-side, so it works for a schema change that is
261
- still a PR (a dbt model, a migration) and leaves the project's current plan
261
+ still a PR (the desired schema snapshot) and leaves the project's current plan
262
262
  alone. --write-checkout then writes the resulting ontology files into the
263
263
  checkout, to commit next to the schema change; nothing is pushed.
264
264
  """
@@ -282,6 +282,10 @@ def plan(
282
282
  json_output=json_output,
283
283
  out=out,
284
284
  )
285
+ if not plan_extraction_complete(preview):
286
+ if json_output:
287
+ typer.echo(json.dumps(preview, indent=2))
288
+ raise typer.Exit(EXIT_VALIDATION_FAILED)
285
289
  if write_checkout:
286
290
  ontology_dir = path / base_path.strip().strip("/")
287
291
  written, deleted, _kept = _write_checkout(ontology_dir, preview["files"], json_output=json_output)
@@ -309,7 +313,7 @@ def plan(
309
313
  )
310
314
  if json_output:
311
315
  typer.echo(json.dumps(record, indent=2))
312
- if record.get("status") != "ready":
316
+ if record.get("status") != "ready" or not plan_extraction_complete(record):
313
317
  raise typer.Exit(EXIT_VALIDATION_FAILED)
314
318
  raise typer.Exit(EXIT_OK)
315
319
 
@@ -366,7 +370,7 @@ def apply(
366
370
  timeout=timeout,
367
371
  json_output=json_output,
368
372
  )
369
- if record.get("status") != "ready":
373
+ if record.get("status") != "ready" or not plan_extraction_complete(record):
370
374
  raise typer.Exit(EXIT_VALIDATION_FAILED)
371
375
  try:
372
376
  checkout = get_schema_plan_checkout(
@@ -454,7 +458,7 @@ def push(
454
458
  timeout=timeout,
455
459
  json_output=json_output,
456
460
  )
457
- if record.get("status") != "ready":
461
+ if record.get("status") != "ready" or not plan_extraction_complete(record):
458
462
  raise typer.Exit(EXIT_VALIDATION_FAILED)
459
463
  _require_marker_matches(path / Path(base_path), record)
460
464
  if not yes:
@@ -764,7 +768,9 @@ def _plan(
764
768
  raise typer.Exit(EXIT_USAGE) from exc
765
769
  if record.get("status") == "ready":
766
770
  render_plan(record, err=json_output)
767
- if plan_is_empty(record):
771
+ if not plan_extraction_complete(record):
772
+ pass # The renderer already explains the blocking diagnostics.
773
+ elif plan_is_empty(record):
768
774
  typer.secho("✓ Schema is up to date.", fg=typer.colors.GREEN, err=json_output)
769
775
  else:
770
776
  typer.secho(f"✓ Plan ready: {plan_id}", fg=typer.colors.GREEN, err=json_output)
@@ -814,6 +820,8 @@ def _preview(
814
820
  render_plan(record, err=json_output)
815
821
  for warning in preview.get("warnings") or []:
816
822
  typer.secho(f" warning: {warning}", fg=typer.colors.YELLOW, err=True)
823
+ if not plan_extraction_complete(record):
824
+ return preview
817
825
  if plan_is_empty(record):
818
826
  typer.secho("✓ Schema is up to date (dry run, nothing kept).", fg=typer.colors.GREEN, err=json_output)
819
827
  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 not (diff.get("tables") or document.get("ontology_changes") or document.get("warnings"))
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 isinstance(schema_plan, dict) and schema_plan.get("status") == "ready":
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')})")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cassis-cli"
3
- version = "2.3.0"
3
+ version = "2.4.0"
4
4
  description = "Validate, test and evaluate your Cassis ontology from your terminal, then publish it"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
File without changes
File without changes