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