semantic-trust 0.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.
- semantic_trust-0.1.0/LICENSE +21 -0
- semantic_trust-0.1.0/PKG-INFO +17 -0
- semantic_trust-0.1.0/README.md +118 -0
- semantic_trust-0.1.0/pyproject.toml +28 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/PKG-INFO +17 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/SOURCES.txt +47 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/dependency_links.txt +1 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/entry_points.txt +3 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/requires.txt +12 -0
- semantic_trust-0.1.0/semantic_trust.egg-info/top_level.txt +1 -0
- semantic_trust-0.1.0/setup.cfg +4 -0
- semantic_trust-0.1.0/tests/test_attribution.py +13 -0
- semantic_trust-0.1.0/tests/test_cli.py +96 -0
- semantic_trust-0.1.0/tests/test_compile.py +18 -0
- semantic_trust-0.1.0/tests/test_config.py +44 -0
- semantic_trust-0.1.0/tests/test_env_doctor.py +14 -0
- semantic_trust-0.1.0/tests/test_joinability.py +59 -0
- semantic_trust-0.1.0/tests/test_judgment.py +128 -0
- semantic_trust-0.1.0/tests/test_makefile_targets.py +43 -0
- semantic_trust-0.1.0/tests/test_manifest_loader.py +73 -0
- semantic_trust-0.1.0/tests/test_manifest_scaffold.py +65 -0
- semantic_trust-0.1.0/tests/test_mcp_server.py +148 -0
- semantic_trust-0.1.0/tests/test_ownership.py +169 -0
- semantic_trust-0.1.0/tests/test_ownership_gate.py +18 -0
- semantic_trust-0.1.0/tests/test_packaging.py +114 -0
- semantic_trust-0.1.0/tests/test_report.py +82 -0
- semantic_trust-0.1.0/tests/test_scorer.py +76 -0
- semantic_trust-0.1.0/tests/test_secrets_scan.py +62 -0
- semantic_trust-0.1.0/tests/test_skills_gate.py +76 -0
- semantic_trust-0.1.0/tests/test_spec_detect.py +20 -0
- semantic_trust-0.1.0/tests/test_templates_grammar.py +115 -0
- semantic_trust-0.1.0/tests/test_uniqueness.py +56 -0
- semantic_trust-0.1.0/tests/test_vendor_check.py +14 -0
- semantic_trust-0.1.0/tests/test_vendored_refs.py +21 -0
- semantic_trust-0.1.0/trust/__init__.py +0 -0
- semantic_trust-0.1.0/trust/cli.py +119 -0
- semantic_trust-0.1.0/trust/compile.py +28 -0
- semantic_trust-0.1.0/trust/config.py +37 -0
- semantic_trust-0.1.0/trust/joinability.py +216 -0
- semantic_trust-0.1.0/trust/judgment.py +102 -0
- semantic_trust-0.1.0/trust/manifest_loader.py +164 -0
- semantic_trust-0.1.0/trust/manifest_scaffold.py +83 -0
- semantic_trust-0.1.0/trust/mcp_server.py +207 -0
- semantic_trust-0.1.0/trust/normalized.py +26 -0
- semantic_trust-0.1.0/trust/ownership.py +97 -0
- semantic_trust-0.1.0/trust/report.py +113 -0
- semantic_trust-0.1.0/trust/scorer.py +319 -0
- semantic_trust-0.1.0/trust/spec_detect.py +39 -0
- semantic_trust-0.1.0/trust/uniqueness.py +62 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ravish Jain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: semantic-trust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Score and certify dbt Semantic Layer models — trust bands, gates, and dbt-parse certification.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: mcp>=1.0
|
|
8
|
+
Provides-Extra: ci
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == "ci"
|
|
10
|
+
Requires-Dist: hypothesis>=6.0; extra == "ci"
|
|
11
|
+
Requires-Dist: pyyaml>=6.0; extra == "ci"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
16
|
+
Requires-Dist: dbt-metricflow<0.14,>=0.13; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# semantic-trust
|
|
2
|
+
|
|
3
|
+
Score and certify dbt Semantic Layer models. The trust/quality layer on top of authoring.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
dbt-labs' agent skills author semantic models — they scaffold YAML, wire measures, and generate metric definitions. `semantic-trust` picks up where authoring stops: it **scores** and **certifies** those models before they reach production.
|
|
10
|
+
|
|
11
|
+
Scoring runs four deterministic gates:
|
|
12
|
+
|
|
13
|
+
- **Structural** — required fields present, types correct, no dangling references.
|
|
14
|
+
- **Uniqueness** — no duplicate metric or dimension names across the project.
|
|
15
|
+
- **Joinability** — join paths resolve; foreign-key references match declared entities.
|
|
16
|
+
- **Ownership** — every model has an approved owner email (optionally domain-restricted).
|
|
17
|
+
|
|
18
|
+
On top of the gate results, an LLM-judgment pass evaluates description quality, naming clarity, and metric intent. The combined score maps to an **A–F trust band**. A model that clears all gates and passes LLM review earns an A; structural failures floor the score at F.
|
|
19
|
+
|
|
20
|
+
Certification adds a final compile check via `dbt parse`. A model that scores B or above and compiles cleanly is marked **certified** and safe to promote.
|
|
21
|
+
|
|
22
|
+
`semantic-trust` works on top of any dbt project — it reads the compiled `semantic_manifest.json` that `dbt parse` emits and requires no changes to your dbt project structure.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
### Claude Code plugin
|
|
29
|
+
|
|
30
|
+
The plugin bundles the skills (`document-semantics`, `build-dbt-model`, `validate-semantics`) and declares the MCP server. The public repository and marketplace install via the Claude Code plugin registry are **coming in a future release (Phase 5)**. Once the repo is published, you'll install locally with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/rj1224/semantic-trust
|
|
34
|
+
cd semantic-trust
|
|
35
|
+
# Claude Code picks up .claude-plugin/plugin.json and .mcp.json automatically
|
|
36
|
+
# when you open the project in a Claude Code session.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### MCP server (standalone)
|
|
40
|
+
|
|
41
|
+
The plugin's `.mcp.json` launches the MCP server via `uvx`. You can also start it directly:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uvx semantic-trust-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This starts the `semantic-trust-mcp` stdio server, exposing three tools: `scaffold_semantic_model`, `score_semantic_model`, and `validate_semantic_model`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
Open a Claude Code session in your dbt project directory, then:
|
|
54
|
+
|
|
55
|
+
**Natural language:**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
validate my semantic model
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Claude routes this to the `validate-semantics` skill, which compiles your project, runs all four gates, applies LLM judgment, and returns a trust report with gate results, score, trust band, and any blocking issues.
|
|
62
|
+
|
|
63
|
+
**Slash commands:**
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
/semantic-trust:validate # full trust report
|
|
67
|
+
/semantic-trust:document # generate or improve semantic model descriptions
|
|
68
|
+
/semantic-trust:build # scaffold a new semantic model from a dbt model
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The trust report shows gate-by-gate pass/fail, the A–F band, and a recommendation (promote / fix-and-retry / escalate).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## dbt version support
|
|
76
|
+
|
|
77
|
+
`semantic-trust` supports both the legacy and the current dbt Semantic Layer spec:
|
|
78
|
+
|
|
79
|
+
| dbt Core version | Spec form | Notes |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| 1.6 – 1.11 | **Legacy** — top-level `semantic_models:` block + standalone `measures:` in `schema.yml` | `mf validate-configs` available as an additional validation pass |
|
|
82
|
+
| **1.12+** / Fusion | **Latest** — model-annotation form: semantic metadata lives inside `models:` blocks | `mf validate-configs` not supported (skipped automatically) |
|
|
83
|
+
|
|
84
|
+
`dbt parse` is the **universal compile gate** for both versions — `semantic-trust` always runs it first. `mf validate-configs` is a legacy-only bonus pass and is skipped automatically on 1.12+ projects.
|
|
85
|
+
|
|
86
|
+
The version is detected from `dbt_project.yml` at runtime; no configuration is required.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Not affiliated with dbt Labs
|
|
91
|
+
|
|
92
|
+
`semantic-trust` references the dbt ecosystem and is designed to work alongside dbt projects. It is **not** an official dbt product and is not affiliated with, endorsed by, or supported by dbt Labs, Inc.
|
|
93
|
+
|
|
94
|
+
The vendored content under `vendor/dbt-agent-skills/` consists of dbt-labs' public Semantic Layer spec guides, included under their Apache-2.0 license. See `vendor/dbt-agent-skills/NOTICE` for attribution and license terms. That content remains Apache-2.0; the rest of this project is MIT.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
`semantic-trust` works with zero configuration. To enable the ownership gate's domain check, add a `.semantic-trust.json` file at your dbt project root:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"approved_email_domains": ["example.com"]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
With `approved_email_domains` set, the ownership gate fails any model whose `owner.email` does not match one of the listed domains. Without this config key, the ownership gate only checks that an email is present.
|
|
109
|
+
|
|
110
|
+
No other configuration keys are required or supported in 0.1.0.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
This project is licensed under the **MIT License** — see the `LICENSE` file.
|
|
117
|
+
|
|
118
|
+
Vendored content under `vendor/dbt-agent-skills/` is licensed under the **Apache-2.0 License** — see `vendor/dbt-agent-skills/NOTICE` and `vendor/dbt-agent-skills/LICENSE`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "semantic-trust"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Score and certify dbt Semantic Layer models — trust bands, gates, and dbt-parse certification."
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = ["mcp>=1.0"] # engine reads the compiled dbt manifest (JSON); MCP = the server SDK
|
|
7
|
+
|
|
8
|
+
# Entry points (AD-1). MCP server is the skill-facing interface (launched via uvx by the plugin);
|
|
9
|
+
# the `semantic-trust` console script is the local dev/testing CLI.
|
|
10
|
+
[project.scripts]
|
|
11
|
+
semantic-trust-mcp = "trust.mcp_server:main"
|
|
12
|
+
semantic-trust = "trust.cli:main_cli"
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
# ci: fast CI — pytest + hypothesis + pyyaml (e2e tests render YAML). No dbt tree.
|
|
16
|
+
ci = ["pytest>=8.0", "hypothesis>=6.0", "pyyaml>=6.0"]
|
|
17
|
+
# dev adds the dbt-metricflow bundle for `mf validate-configs` (legacy-only) + latest-spec checks.
|
|
18
|
+
dev = ["pytest>=8.0", "hypothesis>=6.0", "pyyaml>=6.0", "dbt-metricflow>=0.13,<0.14"]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=68"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["trust*"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: semantic-trust
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Score and certify dbt Semantic Layer models — trust bands, gates, and dbt-parse certification.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: mcp>=1.0
|
|
8
|
+
Provides-Extra: ci
|
|
9
|
+
Requires-Dist: pytest>=8.0; extra == "ci"
|
|
10
|
+
Requires-Dist: hypothesis>=6.0; extra == "ci"
|
|
11
|
+
Requires-Dist: pyyaml>=6.0; extra == "ci"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
16
|
+
Requires-Dist: dbt-metricflow<0.14,>=0.13; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
semantic_trust.egg-info/PKG-INFO
|
|
5
|
+
semantic_trust.egg-info/SOURCES.txt
|
|
6
|
+
semantic_trust.egg-info/dependency_links.txt
|
|
7
|
+
semantic_trust.egg-info/entry_points.txt
|
|
8
|
+
semantic_trust.egg-info/requires.txt
|
|
9
|
+
semantic_trust.egg-info/top_level.txt
|
|
10
|
+
tests/test_attribution.py
|
|
11
|
+
tests/test_cli.py
|
|
12
|
+
tests/test_compile.py
|
|
13
|
+
tests/test_config.py
|
|
14
|
+
tests/test_env_doctor.py
|
|
15
|
+
tests/test_joinability.py
|
|
16
|
+
tests/test_judgment.py
|
|
17
|
+
tests/test_makefile_targets.py
|
|
18
|
+
tests/test_manifest_loader.py
|
|
19
|
+
tests/test_manifest_scaffold.py
|
|
20
|
+
tests/test_mcp_server.py
|
|
21
|
+
tests/test_ownership.py
|
|
22
|
+
tests/test_ownership_gate.py
|
|
23
|
+
tests/test_packaging.py
|
|
24
|
+
tests/test_report.py
|
|
25
|
+
tests/test_scorer.py
|
|
26
|
+
tests/test_secrets_scan.py
|
|
27
|
+
tests/test_skills_gate.py
|
|
28
|
+
tests/test_spec_detect.py
|
|
29
|
+
tests/test_templates_grammar.py
|
|
30
|
+
tests/test_uniqueness.py
|
|
31
|
+
tests/test_vendor_check.py
|
|
32
|
+
tests/test_vendored_refs.py
|
|
33
|
+
trust/__init__.py
|
|
34
|
+
trust/cli.py
|
|
35
|
+
trust/compile.py
|
|
36
|
+
trust/config.py
|
|
37
|
+
trust/joinability.py
|
|
38
|
+
trust/judgment.py
|
|
39
|
+
trust/manifest_loader.py
|
|
40
|
+
trust/manifest_scaffold.py
|
|
41
|
+
trust/mcp_server.py
|
|
42
|
+
trust/normalized.py
|
|
43
|
+
trust/ownership.py
|
|
44
|
+
trust/report.py
|
|
45
|
+
trust/scorer.py
|
|
46
|
+
trust/spec_detect.py
|
|
47
|
+
trust/uniqueness.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trust
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from trust.manifest_loader import load_metrics
|
|
2
|
+
|
|
3
|
+
FIX = "tests/fixtures/manifests/two_model_derived"
|
|
4
|
+
|
|
5
|
+
def test_simple_metrics_owned_by_their_model():
|
|
6
|
+
by = {m.name: m for m in load_metrics(FIX)}
|
|
7
|
+
assert by["order_revenue"].owner_model == "fct_orders"
|
|
8
|
+
assert by["refund_total"].owner_model == "fct_refunds"
|
|
9
|
+
|
|
10
|
+
def test_derived_metric_is_cross_model_unowned():
|
|
11
|
+
by = {m.name: m for m in load_metrics(FIX)}
|
|
12
|
+
assert by["net_revenue"].owner_model is None
|
|
13
|
+
assert by["net_revenue"].type == "derived"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# tests/test_cli.py
|
|
2
|
+
# AD-4: direct import, not subprocess — faster and avoids PATH/venv issues.
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from trust.cli import build_report, _orphan_collision_warnings
|
|
5
|
+
from trust.normalized import NormalizedMetric
|
|
6
|
+
|
|
7
|
+
LEGACY_FIX = str(Path(__file__).parent / "fixtures" / "manifests" / "qcommerce_legacy")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_build_report_returns_band_and_gates():
|
|
11
|
+
rep = build_report(LEGACY_FIX, "orders")
|
|
12
|
+
assert rep["model"] == "orders"
|
|
13
|
+
assert rep["band"] in {"A", "B", "C", "D", "F"}
|
|
14
|
+
assert {"structural", "ownership", "completeness", "uniqueness", "joinability"} <= set(rep["gates"].keys())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_build_report_unknown_model_returns_error():
|
|
18
|
+
rep = build_report(LEGACY_FIX, "nonexistent_model")
|
|
19
|
+
assert "error" in rep
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_orphan_name_collision_surfaces_warning():
|
|
23
|
+
"""Pure unit test of _orphan_collision_warnings for the name-collision path.
|
|
24
|
+
|
|
25
|
+
Two orphan (owner_model=None) metrics that share a name must produce a
|
|
26
|
+
name-collision warning, not pass clean-and-silent.
|
|
27
|
+
|
|
28
|
+
Note: this scenario is unreachable via real dbt — dbt enforces unique metric names
|
|
29
|
+
at parse time, so a semantic_manifest.json with two metrics sharing a name can
|
|
30
|
+
never be genuine `dbt parse` output. No fixture is used here. The logic is
|
|
31
|
+
retained as defense-in-depth and is tested entirely in-memory.
|
|
32
|
+
"""
|
|
33
|
+
# Construct two orphan metrics with the same name but distinct object identities.
|
|
34
|
+
metric_a = NormalizedMetric(
|
|
35
|
+
name="blended_rate",
|
|
36
|
+
type="derived",
|
|
37
|
+
definition_norm="total_order_amount / 2",
|
|
38
|
+
description="Derived metric A.",
|
|
39
|
+
owner=None,
|
|
40
|
+
source_file="models/metrics.yml",
|
|
41
|
+
owner_model=None,
|
|
42
|
+
)
|
|
43
|
+
metric_b = NormalizedMetric(
|
|
44
|
+
name="blended_rate",
|
|
45
|
+
type="derived",
|
|
46
|
+
definition_norm="total_order_amount / 3",
|
|
47
|
+
description="Derived metric B — duplicate name, also orphaned.",
|
|
48
|
+
owner=None,
|
|
49
|
+
source_file="models/metrics.yml",
|
|
50
|
+
owner_model=None,
|
|
51
|
+
)
|
|
52
|
+
all_metrics = [metric_a, metric_b]
|
|
53
|
+
# Collision dict as find_collisions would produce for a name collision.
|
|
54
|
+
collisions = [
|
|
55
|
+
{
|
|
56
|
+
"kind": "name",
|
|
57
|
+
"a": "blended_rate",
|
|
58
|
+
"b": "blended_rate",
|
|
59
|
+
"files": ["models/metrics.yml", "models/metrics.yml"],
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
warnings = _orphan_collision_warnings(all_metrics, collisions)
|
|
63
|
+
assert any(
|
|
64
|
+
"blended_rate" in w and "name collision" in w for w in warnings
|
|
65
|
+
), f"expected a name-collision warning for blended_rate, got: {warnings}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_build_report_is_two_level_and_deterministic():
|
|
69
|
+
LATEST_FIX = str(Path(__file__).parent / "fixtures" / "manifests" / "qcommerce_latest")
|
|
70
|
+
a = build_report(LATEST_FIX, "orders")
|
|
71
|
+
b = build_report(LATEST_FIX, "orders")
|
|
72
|
+
assert a == b # reproducible
|
|
73
|
+
assert "documents" in a and "trust_score" in a and a["band"] in {"A", "B", "C", "D", "F"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_build_report_joinability_sees_all_models():
|
|
77
|
+
"""build_report must pass ALL models in the project to the joinability gate,
|
|
78
|
+
not just the target model (so cross-model join checks are meaningful).
|
|
79
|
+
The joinbreak fixture has a parity mismatch — gate must fail, not pass silently."""
|
|
80
|
+
JOINBREAK_FIX = str(Path(__file__).parent / "fixtures" / "manifests" / "qcommerce_joinbreak")
|
|
81
|
+
rep = build_report(JOINBREAK_FIX, "orders")
|
|
82
|
+
assert "joinability" in rep["gates"]
|
|
83
|
+
# Joinability MUST fail — orders has a parity mismatch with payments
|
|
84
|
+
assert rep["gates"]["joinability"] is False, (
|
|
85
|
+
"joinability gate should be False for joinbreak fixture, "
|
|
86
|
+
f"got gates={rep['gates']}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_build_report_two_level_mcp_shape():
|
|
91
|
+
"""MCP handler must return the same two-level dict as build_report."""
|
|
92
|
+
from trust.mcp_server import handle_score_semantic_model
|
|
93
|
+
LATEST_FIX = str(Path(__file__).parent / "fixtures" / "manifests" / "qcommerce_latest")
|
|
94
|
+
rep = handle_score_semantic_model(LATEST_FIX, "orders")
|
|
95
|
+
assert "documents" in rep and "trust_score" in rep
|
|
96
|
+
assert rep["band"] in {"A", "B", "C", "D", "F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from unittest import mock
|
|
3
|
+
from trust import compile as compile_mod
|
|
4
|
+
|
|
5
|
+
def test_compile_ok_when_parse_succeeds_and_artifact_exists(tmp_path):
|
|
6
|
+
(tmp_path / "target").mkdir()
|
|
7
|
+
(tmp_path / "target" / "semantic_manifest.json").write_text("{}")
|
|
8
|
+
fake = mock.Mock(returncode=0, stdout="ok", stderr="")
|
|
9
|
+
with mock.patch("trust.compile.subprocess.run", return_value=fake):
|
|
10
|
+
res = compile_mod.compile_manifest(str(tmp_path))
|
|
11
|
+
assert res["ok"] is True
|
|
12
|
+
assert res["manifest_path"].endswith("target/semantic_manifest.json")
|
|
13
|
+
|
|
14
|
+
def test_compile_not_ok_when_artifact_absent(tmp_path):
|
|
15
|
+
fake = mock.Mock(returncode=0, stdout="", stderr="")
|
|
16
|
+
with mock.patch("trust.compile.subprocess.run", return_value=fake):
|
|
17
|
+
res = compile_mod.compile_manifest(str(tmp_path))
|
|
18
|
+
assert res["ok"] is False
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for trust.config.load_config — safe defaults and JSON parsing.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from trust.config import load_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_missing_config_safe_default(tmp_path):
|
|
10
|
+
"""No config file → empty approved_email_domains (no domain check, not a failure)."""
|
|
11
|
+
cfg = load_config(str(tmp_path))
|
|
12
|
+
assert cfg["approved_email_domains"] == []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_config_with_domains_loaded(tmp_path):
|
|
16
|
+
"""Valid JSON with approved_email_domains → list returned correctly."""
|
|
17
|
+
config_file = tmp_path / ".semantic-trust.json"
|
|
18
|
+
config_file.write_text(json.dumps({"approved_email_domains": ["example.com", "corp.io"]}))
|
|
19
|
+
cfg = load_config(str(tmp_path))
|
|
20
|
+
assert cfg["approved_email_domains"] == ["example.com", "corp.io"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_config_missing_key_returns_empty_list(tmp_path):
|
|
24
|
+
"""JSON present but key absent → safe default empty list."""
|
|
25
|
+
config_file = tmp_path / ".semantic-trust.json"
|
|
26
|
+
config_file.write_text(json.dumps({"some_other_key": "value"}))
|
|
27
|
+
cfg = load_config(str(tmp_path))
|
|
28
|
+
assert cfg["approved_email_domains"] == []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_config_empty_file_returns_empty_list(tmp_path):
|
|
32
|
+
"""Empty/invalid JSON file → safe default."""
|
|
33
|
+
config_file = tmp_path / ".semantic-trust.json"
|
|
34
|
+
config_file.write_text("")
|
|
35
|
+
cfg = load_config(str(tmp_path))
|
|
36
|
+
assert cfg["approved_email_domains"] == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_no_hardcoded_domains(tmp_path):
|
|
40
|
+
"""load_config must never inject any domain when file is absent."""
|
|
41
|
+
cfg = load_config(str(tmp_path))
|
|
42
|
+
# Explicitly verify no domain leaks from implementation
|
|
43
|
+
assert cfg["approved_email_domains"] == []
|
|
44
|
+
assert len(cfg["approved_email_domains"]) == 0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from scripts.env_doctor import check, HARD
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_check_reports_hard_requirements():
|
|
5
|
+
rows = check()
|
|
6
|
+
names = {n for n, _, _ in rows}
|
|
7
|
+
assert HARD <= names # every hard req is checked
|
|
8
|
+
assert "python>=3.11" in names and "uv" in names and "pyyaml" in names
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_python_and_pyyaml_present_in_this_env():
|
|
12
|
+
rows = {n: ok for n, ok, _ in check()}
|
|
13
|
+
assert rows["python>=3.11"] is True # run under uv's 3.12
|
|
14
|
+
assert rows["pyyaml"] is True # installed via .[dev]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# tests/test_joinability.py
|
|
2
|
+
from trust.manifest_loader import load_models, load_metrics
|
|
3
|
+
from trust.joinability import check_joinability
|
|
4
|
+
from trust.uniqueness import find_collisions
|
|
5
|
+
from trust.scorer import score_model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_parity_mismatch_flagged():
|
|
9
|
+
models = load_models("tests/fixtures/manifests/joinbreak")
|
|
10
|
+
issues = check_joinability(models)
|
|
11
|
+
assert any(i.rule == "joinability_parity" for i in issues)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_clean_project_no_joinability_issues():
|
|
15
|
+
models = load_models("tests/fixtures/manifests/single_latest")
|
|
16
|
+
assert check_joinability(models) == []
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_orphan_or_parity_entity_flagged():
|
|
20
|
+
models = load_models("tests/fixtures/manifests/joinbreak")
|
|
21
|
+
issues = check_joinability(models)
|
|
22
|
+
assert any(i.rule == "joinability_orphan" for i in issues)
|
|
23
|
+
assert any(i.rule == "joinability_parity" for i in issues)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_joinability_issues_have_correct_fields():
|
|
27
|
+
models = load_models("tests/fixtures/manifests/joinbreak")
|
|
28
|
+
issues = check_joinability(models)
|
|
29
|
+
for issue in issues:
|
|
30
|
+
assert issue.severity == "warning"
|
|
31
|
+
assert issue.provenance == "deterministic"
|
|
32
|
+
assert issue.rule in {"joinability_parity", "joinability_orphan"}
|
|
33
|
+
assert issue.location
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_joinability_gate_wired_into_scorer():
|
|
37
|
+
models = load_models("tests/fixtures/manifests/joinbreak")
|
|
38
|
+
j_issues = check_joinability(models)
|
|
39
|
+
sm = next(m for m in models if m.name == "fct_orders")
|
|
40
|
+
model_j_issues = [i for i in j_issues if i.location == sm.source_file]
|
|
41
|
+
metrics = load_metrics("tests/fixtures/manifests/joinbreak")
|
|
42
|
+
model_metrics = [m for m in metrics if m.owner_model == sm.name]
|
|
43
|
+
collisions = find_collisions(metrics)
|
|
44
|
+
rep = score_model(sm, model_metrics, collisions, joinability_issues=model_j_issues)
|
|
45
|
+
assert "joinability" in rep.gates
|
|
46
|
+
assert rep.gates["joinability"] is False
|
|
47
|
+
assert rep.band == "F"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_clean_project_joinability_gate_passes():
|
|
51
|
+
models = load_models("tests/fixtures/manifests/single_latest")
|
|
52
|
+
j_issues = check_joinability(models)
|
|
53
|
+
assert j_issues == []
|
|
54
|
+
sm = next(m for m in models if m.name == "fct_orders")
|
|
55
|
+
metrics = load_metrics("tests/fixtures/manifests/single_latest")
|
|
56
|
+
model_metrics = [m for m in metrics if m.owner_model == sm.name]
|
|
57
|
+
collisions = find_collisions(metrics)
|
|
58
|
+
rep = score_model(sm, model_metrics, collisions, joinability_issues=j_issues)
|
|
59
|
+
assert rep.gates["joinability"] is True
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for trust/judgment.py — judgment combination + guardrail (Task 3.1d).
|
|
3
|
+
|
|
4
|
+
Guardrail contract:
|
|
5
|
+
apply_judgment() accepts the deterministic ModelReport (read-only) and an LLM
|
|
6
|
+
judgment payload (per judge.md) and returns a NEW ModelReport that:
|
|
7
|
+
- adds document_quality per DocumentReport (from judgment["documents"])
|
|
8
|
+
- appends provenance="llm_judge" advisory issues per DocumentReport
|
|
9
|
+
- NEVER changes trust_score, band, gates, context, quality, or any
|
|
10
|
+
provenance="deterministic" issue regardless of what the payload contains
|
|
11
|
+
"""
|
|
12
|
+
from trust.scorer import score_model
|
|
13
|
+
from trust.manifest_loader import load_models, load_metrics
|
|
14
|
+
from trust.uniqueness import find_collisions
|
|
15
|
+
from trust.judgment import apply_judgment
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _report():
|
|
19
|
+
FIX = "tests/fixtures/manifests/qcommerce_latest"
|
|
20
|
+
sm = next(m for m in load_models(FIX) if m.name == "orders")
|
|
21
|
+
ms = load_metrics(FIX)
|
|
22
|
+
return score_model(sm, [m for m in ms if m.owner_model == "orders"], find_collisions(ms))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_judgment_adds_quality_without_touching_trust():
|
|
26
|
+
base = _report()
|
|
27
|
+
before = (base.trust_score, base.band, dict(base.gates))
|
|
28
|
+
out = apply_judgment(base, {"documents": {"metrics": {"quality": 62, "issues": [
|
|
29
|
+
{"severity": "warning", "dimension": "data_context", "rule": "description_business_meaning",
|
|
30
|
+
"message": "restates the formula", "location": "metrics[0].description"}]}}})
|
|
31
|
+
assert (out.trust_score, out.band, dict(out.gates)) == before # deterministic untouched
|
|
32
|
+
assert out.documents["metrics"].to_dict().get("document_quality") == 62
|
|
33
|
+
assert any(i["provenance"] == "llm_judge" for i in out.documents["metrics"].to_dict()["issues"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_judgment_cannot_flip_a_gate():
|
|
37
|
+
base = _report()
|
|
38
|
+
out = apply_judgment(base, {"override_gates": {"uniqueness": False}, "documents": {}}) # malicious payload
|
|
39
|
+
assert out.gates == base.gates # ignored
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_judgment_does_not_mutate_base_report():
|
|
43
|
+
"""apply_judgment must return a new object; the original is unchanged."""
|
|
44
|
+
base = _report()
|
|
45
|
+
base_trust = base.trust_score
|
|
46
|
+
base_metrics_issues_count = len(base.documents["metrics"].issues)
|
|
47
|
+
apply_judgment(base, {"documents": {"metrics": {"quality": 50, "issues": [
|
|
48
|
+
{"severity": "info", "dimension": "data_context", "rule": "some_rule",
|
|
49
|
+
"message": "advisory note", "location": "metrics[0]"}]}}})
|
|
50
|
+
assert base.trust_score == base_trust
|
|
51
|
+
assert len(base.documents["metrics"].issues) == base_metrics_issues_count
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_judgment_no_documents_key_is_safe():
|
|
55
|
+
"""Payload with no 'documents' key must not raise — treat as empty."""
|
|
56
|
+
base = _report()
|
|
57
|
+
out = apply_judgment(base, {})
|
|
58
|
+
assert out.trust_score == base.trust_score
|
|
59
|
+
assert out.band == base.band
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_judgment_unknown_doc_key_ignored():
|
|
63
|
+
"""LLM referencing a non-existent document name must not raise or corrupt."""
|
|
64
|
+
base = _report()
|
|
65
|
+
out = apply_judgment(base, {"documents": {"nonexistent_doc": {"quality": 99, "issues": []}}})
|
|
66
|
+
assert out.trust_score == base.trust_score
|
|
67
|
+
# known documents unchanged
|
|
68
|
+
for key in base.documents:
|
|
69
|
+
assert out.documents[key].to_dict().get("document_quality") is None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_judgment_deterministic_issues_provenance_unchanged():
|
|
73
|
+
"""Deterministic issues on the base report keep provenance='deterministic' after apply_judgment."""
|
|
74
|
+
base = _report()
|
|
75
|
+
# inject a synthetic deterministic issue onto the base (simulate scorer output)
|
|
76
|
+
from trust.report import Issue
|
|
77
|
+
synth = Issue(severity="warning", dimension="completeness", rule="test_rule",
|
|
78
|
+
message="test", location="x.yml", provenance="deterministic")
|
|
79
|
+
base.issues.append(synth)
|
|
80
|
+
out = apply_judgment(base, {"documents": {}})
|
|
81
|
+
det_issues = [i for i in out.issues if i.provenance == "deterministic"]
|
|
82
|
+
assert any(i.rule == "test_rule" for i in det_issues)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_quality_clamped_above_100():
|
|
86
|
+
"""LLM emitting quality > 100 must be clamped to 100."""
|
|
87
|
+
base = _report()
|
|
88
|
+
out = apply_judgment(base, {"documents": {"metrics": {"quality": 150, "issues": []}}})
|
|
89
|
+
assert out.documents["metrics"].document_quality == 100
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_quality_clamped_below_0():
|
|
93
|
+
"""LLM emitting negative quality must be clamped to 0."""
|
|
94
|
+
base = _report()
|
|
95
|
+
out = apply_judgment(base, {"documents": {"metrics": {"quality": -50, "issues": []}}})
|
|
96
|
+
assert out.documents["metrics"].document_quality == 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_quality_none_leaves_document_quality_unset():
|
|
100
|
+
"""Missing quality key must not crash and must leave document_quality as None."""
|
|
101
|
+
base = _report()
|
|
102
|
+
out = apply_judgment(base, {"documents": {"metrics": {"issues": []}}})
|
|
103
|
+
assert out.documents["metrics"].document_quality is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_quality_non_int_leaves_document_quality_unset():
|
|
107
|
+
"""Non-int/unconvertible quality must not crash; document_quality stays None."""
|
|
108
|
+
base = _report()
|
|
109
|
+
out = apply_judgment(base, {"documents": {"metrics": {"quality": "bad_value", "issues": []}}})
|
|
110
|
+
assert out.documents["metrics"].document_quality is None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_deterministic_provenance_on_doc_issue_retained_after_llm_append():
|
|
114
|
+
"""A deterministic issue on a DocumentReport retains its provenance after apply_judgment
|
|
115
|
+
appends an llm_judge issue to the same document (deep-copy guarantee)."""
|
|
116
|
+
from trust.report import Issue
|
|
117
|
+
base = _report()
|
|
118
|
+
det_issue = Issue(severity="warning", dimension="completeness", rule="det_rule",
|
|
119
|
+
message="deterministic", location="metrics.yml", provenance="deterministic")
|
|
120
|
+
base.documents["metrics"].issues.append(det_issue)
|
|
121
|
+
out = apply_judgment(base, {"documents": {"metrics": {"quality": 70, "issues": [
|
|
122
|
+
{"severity": "info", "dimension": "data_context", "rule": "llm_rule",
|
|
123
|
+
"message": "advisory", "location": "metrics[0]"}]}}})
|
|
124
|
+
doc_issues = out.documents["metrics"].issues
|
|
125
|
+
det_issues = [i for i in doc_issues if i.provenance == "deterministic"]
|
|
126
|
+
llm_issues = [i for i in doc_issues if i.provenance == "llm_judge"]
|
|
127
|
+
assert any(i.rule == "det_rule" for i in det_issues), "deterministic issue lost"
|
|
128
|
+
assert any(i.rule == "llm_rule" for i in llm_issues), "llm_judge issue missing"
|