cassis-cli 2.0.0__tar.gz → 2.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-2.0.0 → cassis_cli-2.1.0}/PKG-INFO +13 -2
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/README.md +12 -1
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/api.py +20 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/eval.py +109 -3
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/guide.py +1 -1
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/ontology_design_guide.md +55 -11
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/pyproject.toml +1 -1
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/LICENSE +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/NOTICE +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/__init__.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/common.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/issues.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/main.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/ontology.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/schema.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/schema_plan.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/status.py +0 -0
- {cassis_cli-2.0.0 → cassis_cli-2.1.0}/cassis_cli/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -104,6 +104,17 @@ cassis eval run --project ... --case 019f0000-0000-7000-8000-0000000000ca
|
|
|
104
104
|
|
|
105
105
|
# Start the run and return immediately (poll in the webapp):
|
|
106
106
|
cassis eval run --project ... --no-wait
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Under each failed case `eval run` prints what is needed to diagnose it: the SQL the agent
|
|
110
|
+
generated, the gold SQL it was compared against, how many rows each side returned, any concepts
|
|
111
|
+
the agent found missing, and the judge's reasoning when a judge graded the case. The expected and
|
|
112
|
+
actual row *values* are deliberately not printed — they are in `--json` and in the Cassis app.
|
|
113
|
+
Note that the **generated SQL is printed as the agent wrote it**, and an agent that read your data
|
|
114
|
+
while planning can carry a value it saw into a literal in that SQL. Treat `eval run` output as
|
|
115
|
+
carrying the same sensitivity as the queries themselves when you decide who can read your CI logs.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
107
118
|
|
|
108
119
|
# Probe questions through the text-to-SQL agent using the local ontology files
|
|
109
120
|
# (one full agent run per question, expect ~30-90s each; repeat -q for several):
|
|
@@ -197,7 +208,7 @@ cassis ontology fmt --check
|
|
|
197
208
|
| ---- | ------------------------------------------------------------------------------ |
|
|
198
209
|
| 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) |
|
|
199
210
|
| 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)) |
|
|
200
|
-
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
211
|
+
| 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) |
|
|
201
212
|
| 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 |
|
|
202
213
|
|
|
203
214
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
@@ -82,6 +82,17 @@ cassis eval run --project ... --case 019f0000-0000-7000-8000-0000000000ca
|
|
|
82
82
|
|
|
83
83
|
# Start the run and return immediately (poll in the webapp):
|
|
84
84
|
cassis eval run --project ... --no-wait
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Under each failed case `eval run` prints what is needed to diagnose it: the SQL the agent
|
|
88
|
+
generated, the gold SQL it was compared against, how many rows each side returned, any concepts
|
|
89
|
+
the agent found missing, and the judge's reasoning when a judge graded the case. The expected and
|
|
90
|
+
actual row *values* are deliberately not printed — they are in `--json` and in the Cassis app.
|
|
91
|
+
Note that the **generated SQL is printed as the agent wrote it**, and an agent that read your data
|
|
92
|
+
while planning can carry a value it saw into a literal in that SQL. Treat `eval run` output as
|
|
93
|
+
carrying the same sensitivity as the queries themselves when you decide who can read your CI logs.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
85
96
|
|
|
86
97
|
# Probe questions through the text-to-SQL agent using the local ontology files
|
|
87
98
|
# (one full agent run per question, expect ~30-90s each; repeat -q for several):
|
|
@@ -175,7 +186,7 @@ cassis ontology fmt --check
|
|
|
175
186
|
| ---- | ------------------------------------------------------------------------------ |
|
|
176
187
|
| 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) |
|
|
177
188
|
| 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)) |
|
|
178
|
-
| 2 | Usage error (missing API key or project, no ontology directory, unreadable file, tree over the size limits) |
|
|
189
|
+
| 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) |
|
|
179
190
|
| 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 |
|
|
180
191
|
|
|
181
192
|
Commands that send the local tree (`check`, `fmt`, `upload`, `eval run`, `test`) accept up to
|
|
@@ -106,6 +106,21 @@ class EvalRunActiveError(ApiError):
|
|
|
106
106
|
"""Another eval run is already active for the project (409)."""
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class EvalBranchNotFoundError(ApiError):
|
|
110
|
+
"""`--branch` named an ontology branch the project does not have (404)."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _is_branch_not_found(detail: object) -> bool:
|
|
114
|
+
"""Whether a 404 detail is the start endpoint's missing-branch message.
|
|
115
|
+
|
|
116
|
+
Wire contract with `start_eval_run` in backend `endpoints/ci/eval.py`,
|
|
117
|
+
matched the way `delete_eval_case` matches its own 404 text. A server that
|
|
118
|
+
words it differently falls through to the generic project-scope error, so an
|
|
119
|
+
older server degrades rather than misreporting.
|
|
120
|
+
"""
|
|
121
|
+
return isinstance(detail, str) and detail.startswith("Branch ") and detail.endswith(" not found")
|
|
122
|
+
|
|
123
|
+
|
|
109
124
|
def _project_scope_error(response: httpx.Response) -> ApiError:
|
|
110
125
|
# Surface the server's own message when it names the missing resource
|
|
111
126
|
# (e.g. "Branch 'x' not found") — the generic hint covers the rest.
|
|
@@ -544,6 +559,11 @@ def post_eval_run_start(
|
|
|
544
559
|
"An eval run is already active for this project — wait for it to finish or cancel it "
|
|
545
560
|
"(in the webapp's Evals page, or with the run id printed when it was started)."
|
|
546
561
|
)
|
|
562
|
+
if response.status_code == 404 and _is_branch_not_found(_detail_or_text(response)):
|
|
563
|
+
# Distinct from a project-scope 404: the key and project are fine, the
|
|
564
|
+
# branch name is not, and the generic copy sends the user to check
|
|
565
|
+
# permissions instead.
|
|
566
|
+
raise EvalBranchNotFoundError(_detail_or_text(response))
|
|
547
567
|
if response.status_code in (403, 404):
|
|
548
568
|
raise _project_scope_error(response)
|
|
549
569
|
if response.status_code >= 400:
|
|
@@ -14,6 +14,7 @@ from cassis_cli.api import (
|
|
|
14
14
|
DEFAULT_API_URL,
|
|
15
15
|
ApiError,
|
|
16
16
|
AuthError,
|
|
17
|
+
EvalBranchNotFoundError,
|
|
17
18
|
EvalCaseExistsError,
|
|
18
19
|
EvalCaseGoldSqlError,
|
|
19
20
|
EvalCaseNotFoundError,
|
|
@@ -56,6 +57,15 @@ _STATUS_COLORS = {
|
|
|
56
57
|
"plan_unexecutable": typer.colors.YELLOW,
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
# Caps on the evidence printed under a failed case. Bounded so a suite of
|
|
61
|
+
# failures stays readable in a CI log, and always announced when they bite.
|
|
62
|
+
_MAX_ERROR_CHARS = 200
|
|
63
|
+
_MAX_REASONING_CHARS = 500
|
|
64
|
+
_MAX_SQL_LINES = 20
|
|
65
|
+
# A line cap alone bounds nothing: a gold SQL written as one long line in a YAML
|
|
66
|
+
# case is a single line of any size, printed twice per failing case.
|
|
67
|
+
_MAX_SQL_CHARS = 2000
|
|
68
|
+
|
|
59
69
|
|
|
60
70
|
def _run_page_url(app_url: str, project_id: str, run_id: str) -> str:
|
|
61
71
|
"""Webapp URL of the run's detail view (Evals page, runs tab)."""
|
|
@@ -118,6 +128,84 @@ def _print_validation_failure(detail: object, base_path: str) -> None:
|
|
|
118
128
|
typer.secho(str(detail), fg=typer.colors.RED, err=True)
|
|
119
129
|
|
|
120
130
|
|
|
131
|
+
def _shorten(text: str, limit: int) -> str:
|
|
132
|
+
"""Shorten to `limit` characters and say so. A silent cut can drop the cause."""
|
|
133
|
+
text = " ".join(text.split())
|
|
134
|
+
if len(text) <= limit:
|
|
135
|
+
return text
|
|
136
|
+
return f"{text[:limit]}... (truncated, {len(text)} chars)"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _print_sql(label: str, sql: str) -> None:
|
|
140
|
+
"""Print SQL under a failed case, bounded on both lines and characters."""
|
|
141
|
+
lines = sql.strip().splitlines()
|
|
142
|
+
typer.echo(f" {label}:")
|
|
143
|
+
budget = _MAX_SQL_CHARS
|
|
144
|
+
shown = 0
|
|
145
|
+
for line in lines[:_MAX_SQL_LINES]:
|
|
146
|
+
if budget <= 0:
|
|
147
|
+
break
|
|
148
|
+
if len(line) > budget:
|
|
149
|
+
typer.echo(f" {line[:budget]}... (truncated, {len(line)} chars)")
|
|
150
|
+
budget = 0
|
|
151
|
+
else:
|
|
152
|
+
typer.echo(f" {line}")
|
|
153
|
+
budget -= len(line)
|
|
154
|
+
shown += 1
|
|
155
|
+
if shown < len(lines):
|
|
156
|
+
typer.echo(f" ... ({len(lines) - shown} more lines)")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _print_failure_evidence(r: dict[str, Any]) -> None:
|
|
160
|
+
"""Print what a failed case needs to be diagnosed here, without row values.
|
|
161
|
+
|
|
162
|
+
Which field carries the reason depends on the status: a missing-concept
|
|
163
|
+
failure explains itself through `missing_concepts`, a judge rejection
|
|
164
|
+
through `judge_reasoning`, a data mismatch through `error` and the row
|
|
165
|
+
counts. Printing only `error` — as this did — showed nothing at all for the
|
|
166
|
+
first two.
|
|
167
|
+
|
|
168
|
+
Expected and actual row values are deliberately not printed. They ride in
|
|
169
|
+
`--json` and the webapp instead: `cassis verify` output is a CI job log, and
|
|
170
|
+
warehouse values in a job log travel further than whoever reads it intends.
|
|
171
|
+
|
|
172
|
+
Every field is read with `.get`, so an older server that returns none of
|
|
173
|
+
them degrades to the error line alone.
|
|
174
|
+
"""
|
|
175
|
+
error = r.get("error")
|
|
176
|
+
if error:
|
|
177
|
+
typer.echo(f" {_shorten(str(error), _MAX_ERROR_CHARS)}")
|
|
178
|
+
|
|
179
|
+
names = [str(c["name"]) for c in (r.get("missing_concepts") or []) if isinstance(c, dict) and c.get("name")]
|
|
180
|
+
if names:
|
|
181
|
+
typer.echo(f" Missing concepts: {', '.join(names)}")
|
|
182
|
+
|
|
183
|
+
if r.get("judge_verdict"):
|
|
184
|
+
typer.echo(f" Judge verdict: {r['judge_verdict']}")
|
|
185
|
+
if r.get("judge_reasoning"):
|
|
186
|
+
typer.echo(f" Judge reasoning: {_shorten(str(r['judge_reasoning']), _MAX_REASONING_CHARS)}")
|
|
187
|
+
|
|
188
|
+
expected, actual = r.get("expected_row_count"), r.get("actual_row_count")
|
|
189
|
+
if expected is not None or actual is not None:
|
|
190
|
+
shown_actual = "-" if actual is None else str(actual)
|
|
191
|
+
shown_expected = "-" if expected is None else str(expected)
|
|
192
|
+
typer.echo(f" Rows: {shown_actual} actual vs {shown_expected} expected")
|
|
193
|
+
|
|
194
|
+
# The agent's own words, for the statuses that carry no other explanation
|
|
195
|
+
# (a conversational reply instead of a plan, for one).
|
|
196
|
+
content = r.get("content")
|
|
197
|
+
if content and not error and not names and not r.get("judge_reasoning"):
|
|
198
|
+
typer.echo(f" {_shorten(str(content), _MAX_ERROR_CHARS)}")
|
|
199
|
+
|
|
200
|
+
if r.get("generated_sql"):
|
|
201
|
+
_print_sql("Generated SQL", str(r["generated_sql"]))
|
|
202
|
+
if r.get("gold_sql"):
|
|
203
|
+
label = "Gold SQL"
|
|
204
|
+
if r.get("gold_sql_from_live_case"):
|
|
205
|
+
label = "Gold SQL (the case as it stands now; this result predates snapshotting)"
|
|
206
|
+
_print_sql(label, str(r["gold_sql"]))
|
|
207
|
+
|
|
208
|
+
|
|
121
209
|
def _print_results_table(results: list[dict[str, Any]]) -> None:
|
|
122
210
|
for r in sorted(results, key=lambda r: (r.get("status") == _PASSED, r.get("question") or "")):
|
|
123
211
|
status = r.get("status", "?")
|
|
@@ -129,8 +217,9 @@ def _print_results_table(results: list[dict[str, Any]]) -> None:
|
|
|
129
217
|
question = question[:67] + "..."
|
|
130
218
|
line = f" {icon} {status:<17} {duration:>5} {question}"
|
|
131
219
|
typer.secho(line, fg=color)
|
|
132
|
-
if
|
|
133
|
-
|
|
220
|
+
if status == _PASSED:
|
|
221
|
+
continue
|
|
222
|
+
_print_failure_evidence(r)
|
|
134
223
|
|
|
135
224
|
|
|
136
225
|
def _print_summary(run: dict[str, Any]) -> None:
|
|
@@ -448,7 +537,13 @@ def run(
|
|
|
448
537
|
named case(s) run — e.g. proving one fresh `add-case` in seconds instead of
|
|
449
538
|
rerunning the whole suite. Exits 0 when the run completes with every case
|
|
450
539
|
passed, 1 on any failed case / failed run / invalid tree, 2 on usage
|
|
451
|
-
errors, 3 on transport
|
|
540
|
+
errors (including a --branch the project does not have), 3 on transport
|
|
541
|
+
errors or --timeout.
|
|
542
|
+
|
|
543
|
+
Failed cases print the generated SQL as the agent wrote it. Row values are
|
|
544
|
+
never printed (they are in --json), but an agent that read your data while
|
|
545
|
+
planning can carry a value it saw into a SQL literal — so treat this output
|
|
546
|
+
as sensitive as the queries themselves.
|
|
452
547
|
"""
|
|
453
548
|
api_key = require_api_key(api_key)
|
|
454
549
|
for case_id in case or []:
|
|
@@ -485,6 +580,17 @@ def run(
|
|
|
485
580
|
except EvalStartValidationError as exc:
|
|
486
581
|
_print_validation_failure(exc.detail, base_path)
|
|
487
582
|
raise typer.Exit(EXIT_VALIDATION_FAILED) from exc
|
|
583
|
+
except EvalBranchNotFoundError as exc:
|
|
584
|
+
# Bad input, not a transport or permissions problem — so exit 2, and say
|
|
585
|
+
# what --branch actually takes instead of pointing at the API key.
|
|
586
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
587
|
+
typer.echo(
|
|
588
|
+
"--branch runs against an ontology branch that already exists in Cassis "
|
|
589
|
+
"(create it in the webapp, or push it with `cassis ontology push`). "
|
|
590
|
+
"To label a run after your local checkout's branch, use --label instead.",
|
|
591
|
+
err=True,
|
|
592
|
+
)
|
|
593
|
+
raise typer.Exit(EXIT_USAGE) from exc
|
|
488
594
|
except ApiError as exc: # covers AuthError and EvalRunActiveError too
|
|
489
595
|
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
490
596
|
raise typer.Exit(EXIT_TRANSPORT) from exc
|
|
@@ -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 = 7
|
|
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
|
|
@@ -107,7 +107,7 @@ Put each fact at the lowest layer that fully owns it:
|
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
column synonym / column description single-column facts: meaning, encoding, unit, date format, nulls
|
|
110
|
-
→ table description single-table facts:
|
|
110
|
+
→ table description single-table facts: scope, mandatory filters, "use this not that"
|
|
111
111
|
→ domain context_md cross-table routing, disambiguation, vocabulary spanning tables
|
|
112
112
|
→ root context truly global rules only: corporate identity, entity hierarchy, global conventions
|
|
113
113
|
```
|
|
@@ -134,13 +134,18 @@ otherwise push it down.
|
|
|
134
134
|
Per-table facts go on that table's `description`; keep only genuinely
|
|
135
135
|
cross-table routing in `context_md`.
|
|
136
136
|
- Metrics are surfaced with their full definition → don't re-paste a formula in
|
|
137
|
-
prose
|
|
137
|
+
prose, and that includes the metric's **own** `description` and `notes` (§8).
|
|
138
|
+
Point to a metric by name **only** when it lives in a *different* domain
|
|
138
139
|
than the prose discussing it; a domain's own metrics are surfaced alongside its
|
|
139
140
|
`context_md` already.
|
|
140
141
|
- Joins are surfaced with their condition and cardinality → don't enumerate join
|
|
141
142
|
keys in prose. The only join content that belongs in prose is a join+filter
|
|
142
143
|
*recipe* no single structured join can express.
|
|
143
144
|
|
|
145
|
+
**Short or empty beats redundant.** When the structured fields already say
|
|
146
|
+
everything worth saying, leave the prose field short or empty. An empty
|
|
147
|
+
description costs nothing; a redundant one goes stale.
|
|
148
|
+
|
|
144
149
|
---
|
|
145
150
|
|
|
146
151
|
## 4. Domains and `context_md`
|
|
@@ -188,9 +193,9 @@ domain. Everything domain-specific moves down.
|
|
|
188
193
|
|
|
189
194
|
A table's `description` answers *"what is this, and what must the SQL agent know
|
|
190
195
|
to use it correctly?"* — business meaning **plus** the correctness facts (scope,
|
|
191
|
-
mandatory filters, gotchas).
|
|
192
|
-
|
|
193
|
-
|
|
196
|
+
mandatory filters, gotchas). The grain goes in the `grain` field — the identifying
|
|
197
|
+
column(s) — not in the description: the agent reads it structurally, and "one row
|
|
198
|
+
per order" in prose is a second copy. Don't enumerate the columns either — the
|
|
194
199
|
column list is surfaced automatically.
|
|
195
200
|
|
|
196
201
|
Use `synonyms` for alternate names a user might say ("purchases", "transactions"),
|
|
@@ -219,10 +224,10 @@ in the `sql` per §7.
|
|
|
219
224
|
## 6. Columns
|
|
220
225
|
|
|
221
226
|
Column enrichment is `description`, `unit`, and `synonyms` — you cannot add or
|
|
222
|
-
rename columns (they come from introspection). The
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
rename columns (they come from introspection). The agent sees a column's name,
|
|
228
|
+
type, `unit` and `synonyms` structurally; the `description` carries what those
|
|
229
|
+
don't: meaning, possible values, encoding, date format, null behavior, any
|
|
230
|
+
mandatory filter.
|
|
226
231
|
|
|
227
232
|
- **Prioritize the trap columns.** Disambiguate similarly-named columns
|
|
228
233
|
(`created_at` vs `updated_at`, `amount` vs `net_amount`), numeric-encoded
|
|
@@ -232,6 +237,14 @@ behavior, any mandatory filter.
|
|
|
232
237
|
- **Set `unit`** whenever a number is a currency, percentage, or duration —
|
|
233
238
|
aggregation correctness depends on it.
|
|
234
239
|
- **Use `synonyms`** for 1–3 short, plain alternate terms a user might use.
|
|
240
|
+
- **Facts, not SQL.** Say what the column holds and how it is encoded ("TRUE =
|
|
241
|
+
the account can sign in"), never a fragment to paste ("filter with
|
|
242
|
+
`is_active = TRUE`", "count people with `COUNT(DISTINCT id)`"). Writing the SQL
|
|
243
|
+
is the agent's job.
|
|
244
|
+
- **Relationships live in joins.** A foreign-key column says what it identifies
|
|
245
|
+
("the owning organization"), not which table it references or on what
|
|
246
|
+
condition; the join carries that, and a description repeating it drifts when
|
|
247
|
+
the join changes.
|
|
235
248
|
- **Keep the join key queryable.** A foreign-key column stays a real, described
|
|
236
249
|
column even when a join covers the relationship — hiding it leaves the agent
|
|
237
250
|
unable to filter or group on it without traversing the join. Test: imagine a
|
|
@@ -312,12 +325,33 @@ unit: euro cents
|
|
|
312
325
|
synonyms:
|
|
313
326
|
- net sales
|
|
314
327
|
- revenue net of refunds
|
|
315
|
-
description:
|
|
328
|
+
description: Revenue kept after refunds; the top-line sales figure.
|
|
316
329
|
```
|
|
317
330
|
|
|
318
331
|
A metric definition beats a prose "revenue = net revenue excluding tax" note —
|
|
319
332
|
the metric is surfaced structurally and can be reused; the prose drifts.
|
|
320
333
|
|
|
334
|
+
**Definition in the fields, meaning in the prose.** `expression`, `filters`,
|
|
335
|
+
`table_schema`/`table_name` and `unit` *are* the definition. Every prose field —
|
|
336
|
+
including the metric's own `description` and `notes` — says what the number means
|
|
337
|
+
and when to use it, never how it is computed: no columns, thresholds, time windows,
|
|
338
|
+
or numerator/denominator. A rule copied into prose is a second definition, and the
|
|
339
|
+
two drift apart (one metric shipped with a 90-day window in `filters` and "30 days"
|
|
340
|
+
in its own description). Business intent is fine ("draft bookings are not
|
|
341
|
+
commitments"); its encoding (`status <> 'DRAFT'`) is not. This holds even when the
|
|
342
|
+
metric's name *is* the term being defined ("active partners"): the reader sees
|
|
343
|
+
`filters` right next to the description. Disambiguate from sibling metrics by the
|
|
344
|
+
question each answers, never by restating their windows or filters. If the fields
|
|
345
|
+
say it all, keep the description to a phrase or leave it empty.
|
|
346
|
+
|
|
347
|
+
```yaml
|
|
348
|
+
# Bad — restates the definition
|
|
349
|
+
description: Active partners = COUNT(DISTINCT partner_id) with a non-cancelled
|
|
350
|
+
booking in the last 90 days.
|
|
351
|
+
# Good — what the number is for
|
|
352
|
+
description: Partners still trading; the activity count used for outreach targets.
|
|
353
|
+
```
|
|
354
|
+
|
|
321
355
|
---
|
|
322
356
|
|
|
323
357
|
## 9. Identifiers and case — where SQL silently breaks
|
|
@@ -351,7 +385,9 @@ description, so a wrong example is worse than none.
|
|
|
351
385
|
|
|
352
386
|
- **Use real stored enum values verbatim.** Never hand-wave a list with "etc." —
|
|
353
387
|
the agent reads `etc.` as license to invent values. If you don't have the exact
|
|
354
|
-
values, omit the list rather than approximate it.
|
|
388
|
+
values, omit the list rather than approximate it. A `DISTINCT` over the column
|
|
389
|
+
*is* the list of stored values: write it plainly ("Stored values: 'EDITOR'"),
|
|
390
|
+
with no "observed so far" hedge and no counts. When two columns supposedly
|
|
355
391
|
share an enum, verify they're identical before encoding both.
|
|
356
392
|
- **Be wary of analogies to external products** — they mislead the moment the
|
|
357
393
|
real mechanic diverges.
|
|
@@ -390,6 +426,14 @@ description, so a wrong example is worse than none.
|
|
|
390
426
|
- **Ground writes in the real schema.** Before referencing a physical table or
|
|
391
427
|
column, confirm it exists. Don't invent tables or columns; the physical schema
|
|
392
428
|
is fixed.
|
|
429
|
+
- **Never persist a measurement.** Prose holds stable encodings and business
|
|
430
|
+
definitions, not what the data shows today. Row counts, distinct counts, shares,
|
|
431
|
+
distributions, funnel rates and "as of" / "currently" / "observed" statements go
|
|
432
|
+
stale silently, and the answering agent repeats them as fact. Query the data to
|
|
433
|
+
*verify* a stable fact (stored enum values, a date format, null behaviour) and
|
|
434
|
+
write only the fact: "Stored values: 'EDITOR', 'ADMIN'", not "only 'EDITOR'
|
|
435
|
+
observed (6 of 6, 100%)". Complete value lists and constants the business fixes
|
|
436
|
+
(a 14-day trial) are facts, not measurements.
|
|
393
437
|
- **Ask for the business meaning; don't guess.** A term like "active user" or
|
|
394
438
|
"last month" has a project-specific definition (time window, criteria, filters).
|
|
395
439
|
Encode the business's actual definition, not a textbook default.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|