Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
{
"name": "sl-toolkit",
"description": "Semantic layer toolkit for Keboola — inspect, validate, and build models via the metastore API. CRUD operations handled conversationally via the semantic-layer skill. (Renamed from sl-builder in v3.0.0.)",
"version": "3.0.0",
"version": "3.0.1",
"source": "./plugins/sl-toolkit",
"category": "development"
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/sl-toolkit/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sl-toolkit",
"version": "3.0.0",
"version": "3.0.1",
"description": "Semantic layer tools for Keboola — inspect (/sl-show), validate (/sl-validate), and build (/sl-build) models via the metastore API. Add/edit/remove operations handled conversationally via the semantic-layer reference skill.",
"author": {
"name": "Keboola s.r.o.",
Expand Down
2 changes: 1 addition & 1 deletion plugins/sl-toolkit/commands/sl-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ if UPDATE_ID:
else:
uuid = api_post('/api/v1/repository/semantic-model', {
'name': model['name'],
'data': {'name': model['name'], 'description': model['description'], 'sqlDialect': 'Snowflake'},
'data': {'name': model['name'], 'description': model['description'], 'sql_dialect': 'Snowflake'},
'branch': 'main', 'schemaVersion': '1.0.0', 'scope': 'project'
})['data']['id']
print(f'✓ model created {uuid}')
Expand Down
13 changes: 11 additions & 2 deletions plugins/sl-toolkit/skills/semantic-layer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ on the returned list — the `?modelId` query param is unreliable.
```json
{
"name": "<model name>",
"data": { "name": "<model name>", "description": "...", "sqlDialect": "Snowflake" },
"data": { "name": "<model name>", "description": "...", "sql_dialect": "Snowflake" },
"branch": "main",
"schemaVersion": "1.0.0",
"scope": "project"
Expand Down Expand Up @@ -248,10 +248,19 @@ and a bare `KEBOOLA` reference will fail at Snowflake query time.
"name": "net_margin_critical",
"constraintType": "range",
"metrics": ["Net Profit Margin"],
"ruleExpression": { "bounds": { "min": -2.0, "max": 0.05 } },
"rule": "-2.0 <= Net Profit Margin <= 0.05",
"ruleExpression": { "operator": "between", "left": "Net Profit Margin", "bounds": { "min": -2.0, "max": 0.05 } },
"severity": "error"
}
```
- **`rule` is REQUIRED** by the metastore — a plain-string logical expression using metric names
(e.g. `"profit <= revenue"`, `"-2.0 <= Net Profit Margin <= 0.05"`, `maxLength` 1000). Omitting it
returns **422 missing property 'rule'**.
- **`ruleExpression` is OPTIONAL** and is a structured object `{operator, left, right, bounds}`
(`operator` ∈ `< <= = >= > != between in sum_equals ratio_between`). Send it **in addition to**
`rule` — downstream pipelines read `ruleExpression.bounds`; the API enforces `rule`.
- The metastore validates `semantic-constraint` with **`additionalProperties: false`** — unlike
`semantic-model`, a stray/misspelled key is hard-rejected, so keep the payload clean.
- `severity` API accepts only: `error` / `warning` / `info`
- Encode 4-level health bands in the **name suffix**: `_critical` / `_warning` / `_healthy` / `_review`
- Downstream pipelines parse the suffix; `severity` is secondary
Expand Down
4 changes: 2 additions & 2 deletions plugins/sl-toolkit/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ python -m pytest plugins/sl-toolkit/tests/ -v

| File | Covers |
|---|---|
| `test_smoke.py` | jsonschema round-trip for all 6 entity types (envelope + data shape), `sqlDialect` camelCase invariant, constraint severity suffix, FQN uses `KEBOOLA_<projectId>` |
| `test_smoke.py` | jsonschema round-trip for all 6 entity types (envelope + data shape), `sql_dialect` snake_case invariant, constraint requires string `rule`, constraint severity suffix, FQN uses `KEBOOLA_<projectId>` |
| `test_fqn.py` | `db_name()` token-verify success / cache hit / HTTP error fallback / malformed response. `fqn()` three-part construction with dotted schemas |
| `test_skill_consistency.py` | Greps `SKILL.md` and command markdowns to assert: no hardcoded `KEBOOLA` fqn, `sqlDialect` not `sql_dialect`, no `allowed-tools` in reference-skill frontmatter, multi-cloud regex (`gcp\|aws\|azure`), no literal placeholder strings, push loop includes `semantic-constraint`, VERSION rule instructs probing |
| `test_skill_consistency.py` | Greps `SKILL.md` and command markdowns to assert: no hardcoded `KEBOOLA` fqn, `sql_dialect` not `sqlDialect`, no `allowed-tools` in reference-skill frontmatter, multi-cloud regex (`gcp\|aws\|azure`), no literal placeholder strings, push loop includes `semantic-constraint`, VERSION rule instructs probing |

## Fixtures and schemas

Expand Down
3 changes: 2 additions & 1 deletion plugins/sl-toolkit/tests/fixtures/semantic-constraint.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"name": "net_margin_critical",
"constraintType": "range",
"metrics": ["Net Profit Margin"],
"ruleExpression": {"bounds": {"min": -2.0, "max": 0.05}},
"rule": "-2.0 <= Net Profit Margin <= 0.05",
"ruleExpression": {"operator": "between", "left": "Net Profit Margin", "bounds": {"min": -2.0, "max": 0.05}},
"severity": "error",
"modelUUID": "00000000-0000-0000-0000-000000000001"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/sl-toolkit/tests/fixtures/semantic-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"data": {
"name": "Revenue Analytics",
"description": "Greenfield model for revenue tracking",
"sqlDialect": "Snowflake"
"sql_dialect": "Snowflake"
},
"branch": "main",
"schemaVersion": "1.0.0",
Expand Down
6 changes: 5 additions & 1 deletion plugins/sl-toolkit/tests/schemas/semantic-constraint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "semantic-constraint.data",
"type": "object",
"required": ["name", "constraintType", "metrics", "ruleExpression", "severity", "modelUUID"],
"required": ["name", "constraintType", "metrics", "rule", "ruleExpression", "severity", "modelUUID"],
"additionalProperties": true,
"properties": {
"name": {"type": "string", "minLength": 1, "pattern": ".*_(critical|warning|healthy|review)$"},
"constraintType": {"type": "string", "enum": ["range", "threshold", "expression"]},
"metrics": {"type": "array", "minItems": 1, "items": {"type": "string"}},
"rule": {"type": "string", "minLength": 1, "maxLength": 1000},
"ruleExpression": {
"type": "object",
"properties": {
"operator": {"type": "string"},
"left": {"type": "string"},
"right": {"type": "string"},
"bounds": {
"type": "object",
"properties": {
Expand Down
6 changes: 3 additions & 3 deletions plugins/sl-toolkit/tests/schemas/semantic-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "semantic-model.data",
"type": "object",
"required": ["name", "description", "sqlDialect"],
"required": ["name", "description", "sql_dialect"],
"additionalProperties": true,
"properties": {
"name": {"type": "string", "minLength": 1},
"description": {"type": "string"},
"sqlDialect": {"type": "string", "enum": ["Snowflake", "BigQuery", "Redshift", "Postgres"]}
"sql_dialect": {"type": "string", "enum": ["Snowflake", "BigQuery", "Redshift", "Postgres"]}
},
"not": {
"anyOf": [
{"required": ["sql_dialect"]},
{"required": ["sqlDialect"]},
{"required": ["sqldialect"]},
{"required": ["SqlDialect"]}
]
Expand Down
29 changes: 23 additions & 6 deletions plugins/sl-toolkit/tests/test_skill_consistency.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Verify SKILL.md and command markdowns don't drift from canonical invariants.

These tests grep the markdown so changes to SKILL.md that re-introduce the bugs
Jordan flagged (hardcoded KEBOOLA, sql_dialect, allowed-tools, etc.) fail in CI.
These tests grep the markdown so changes to SKILL.md that re-introduce known bugs
(hardcoded KEBOOLA, camelCase sqlDialect, allowed-tools, etc.) fail in CI.
"""
import re
from pathlib import Path
Expand Down Expand Up @@ -43,11 +43,28 @@ def test_no_hardcoded_keboola_in_fqn_construction():
)


def test_sqldialect_is_camelcase():
"""Regression: PR #72 sql_dialect bug. SKILL.md must use sqlDialect, never snake_case."""
def test_sqldialect_is_snakecase():
"""The metastore semantic-model schema requires snake_case `sql_dialect`
(see go-monorepo services/metastore/migrations/schema/semantic-model_schema_1.0.0.json:
required=["name","sql_dialect"]). camelCase `sqlDialect` is silently ignored, so the
required key is missing and the very first POST fails with 422. PR #72 pinned the wrong
spelling; this asserts the correct one."""
text = read(SKILL_MD)
assert "sqlDialect" in text, "SKILL.md must document sqlDialect"
assert "sql_dialect" not in text, "snake_case sql_dialect is the PR #72 regression"
assert "sql_dialect" in text, "SKILL.md must document snake_case sql_dialect (metastore contract)"
assert "sqlDialect" not in text, "camelCase sqlDialect is rejected by the metastore API"


def test_constraint_rule_is_documented():
"""The metastore semantic-constraint schema requires a string `rule`; documenting only
`ruleExpression` causes 422 missing property 'rule'. SKILL.md must show `rule` in the
semantic-constraint payload."""
text = read(SKILL_MD)
section = text[text.find("### semantic-constraint"):]
section = section[: section.find("\n---")]
assert '"rule"' in section, (
"SKILL.md semantic-constraint payload must include the required string `rule` — "
"ruleExpression alone is rejected by the metastore with 422 missing property 'rule'."
)


def test_no_allowed_tools_wildcard_in_skill_frontmatter():
Expand Down
21 changes: 16 additions & 5 deletions plugins/sl-toolkit/tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Round-trip jsonschema validation for every metastore entity type.

These tests would have caught the sql_dialect/sqlDialect divergence from PR #72:
the model schema explicitly forbids snake_case spellings via `not.anyOf`.
These tests guard against key-name drift from the metastore contract: the model schema
requires snake_case `sql_dialect` and forbids the camelCase spelling via `not.anyOf`, and
the constraint fixture must carry the required string `rule`.
"""
import json
from pathlib import Path
Expand Down Expand Up @@ -42,10 +43,20 @@ def test_data_shape(entity):


def test_no_dialect_drift():
"""Regression test for PR #72: sqlDialect must be camelCase, never snake_case."""
"""The metastore requires snake_case `sql_dialect`; camelCase `sqlDialect` is ignored and
the required key ends up missing (422 on the first POST). Assert the correct spelling."""
fixture = load(FIXTURES / "semantic-model.json")
assert "sqlDialect" in fixture["data"], "semantic-model.data must use camelCase sqlDialect"
assert "sql_dialect" not in fixture["data"], "snake_case sql_dialect is the bug from #72"
assert "sql_dialect" in fixture["data"], "semantic-model.data must use snake_case sql_dialect"
assert "sqlDialect" not in fixture["data"], "camelCase sqlDialect is rejected by the metastore API"


def test_constraint_requires_rule():
"""The metastore semantic-constraint schema requires a string `rule` (required=[...,'rule',...]).
`ruleExpression` alone triggers 422 missing property 'rule'. Send both: `rule` for the API,
`ruleExpression` for downstream pipelines that read the bounds."""
fixture = load(FIXTURES / "semantic-constraint.json")["data"]
assert isinstance(fixture.get("rule"), str) and fixture["rule"], "constraint must include a string `rule`"
assert "ruleExpression" in fixture, "constraint should retain ruleExpression for downstream pipelines"


def test_constraint_severity_suffix():
Expand Down
Loading