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.
Files changed (49) hide show
  1. semantic_trust-0.1.0/LICENSE +21 -0
  2. semantic_trust-0.1.0/PKG-INFO +17 -0
  3. semantic_trust-0.1.0/README.md +118 -0
  4. semantic_trust-0.1.0/pyproject.toml +28 -0
  5. semantic_trust-0.1.0/semantic_trust.egg-info/PKG-INFO +17 -0
  6. semantic_trust-0.1.0/semantic_trust.egg-info/SOURCES.txt +47 -0
  7. semantic_trust-0.1.0/semantic_trust.egg-info/dependency_links.txt +1 -0
  8. semantic_trust-0.1.0/semantic_trust.egg-info/entry_points.txt +3 -0
  9. semantic_trust-0.1.0/semantic_trust.egg-info/requires.txt +12 -0
  10. semantic_trust-0.1.0/semantic_trust.egg-info/top_level.txt +1 -0
  11. semantic_trust-0.1.0/setup.cfg +4 -0
  12. semantic_trust-0.1.0/tests/test_attribution.py +13 -0
  13. semantic_trust-0.1.0/tests/test_cli.py +96 -0
  14. semantic_trust-0.1.0/tests/test_compile.py +18 -0
  15. semantic_trust-0.1.0/tests/test_config.py +44 -0
  16. semantic_trust-0.1.0/tests/test_env_doctor.py +14 -0
  17. semantic_trust-0.1.0/tests/test_joinability.py +59 -0
  18. semantic_trust-0.1.0/tests/test_judgment.py +128 -0
  19. semantic_trust-0.1.0/tests/test_makefile_targets.py +43 -0
  20. semantic_trust-0.1.0/tests/test_manifest_loader.py +73 -0
  21. semantic_trust-0.1.0/tests/test_manifest_scaffold.py +65 -0
  22. semantic_trust-0.1.0/tests/test_mcp_server.py +148 -0
  23. semantic_trust-0.1.0/tests/test_ownership.py +169 -0
  24. semantic_trust-0.1.0/tests/test_ownership_gate.py +18 -0
  25. semantic_trust-0.1.0/tests/test_packaging.py +114 -0
  26. semantic_trust-0.1.0/tests/test_report.py +82 -0
  27. semantic_trust-0.1.0/tests/test_scorer.py +76 -0
  28. semantic_trust-0.1.0/tests/test_secrets_scan.py +62 -0
  29. semantic_trust-0.1.0/tests/test_skills_gate.py +76 -0
  30. semantic_trust-0.1.0/tests/test_spec_detect.py +20 -0
  31. semantic_trust-0.1.0/tests/test_templates_grammar.py +115 -0
  32. semantic_trust-0.1.0/tests/test_uniqueness.py +56 -0
  33. semantic_trust-0.1.0/tests/test_vendor_check.py +14 -0
  34. semantic_trust-0.1.0/tests/test_vendored_refs.py +21 -0
  35. semantic_trust-0.1.0/trust/__init__.py +0 -0
  36. semantic_trust-0.1.0/trust/cli.py +119 -0
  37. semantic_trust-0.1.0/trust/compile.py +28 -0
  38. semantic_trust-0.1.0/trust/config.py +37 -0
  39. semantic_trust-0.1.0/trust/joinability.py +216 -0
  40. semantic_trust-0.1.0/trust/judgment.py +102 -0
  41. semantic_trust-0.1.0/trust/manifest_loader.py +164 -0
  42. semantic_trust-0.1.0/trust/manifest_scaffold.py +83 -0
  43. semantic_trust-0.1.0/trust/mcp_server.py +207 -0
  44. semantic_trust-0.1.0/trust/normalized.py +26 -0
  45. semantic_trust-0.1.0/trust/ownership.py +97 -0
  46. semantic_trust-0.1.0/trust/report.py +113 -0
  47. semantic_trust-0.1.0/trust/scorer.py +319 -0
  48. semantic_trust-0.1.0/trust/spec_detect.py +39 -0
  49. 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,3 @@
1
+ [console_scripts]
2
+ semantic-trust = trust.cli:main_cli
3
+ semantic-trust-mcp = trust.mcp_server:main
@@ -0,0 +1,12 @@
1
+ mcp>=1.0
2
+
3
+ [ci]
4
+ pytest>=8.0
5
+ hypothesis>=6.0
6
+ pyyaml>=6.0
7
+
8
+ [dev]
9
+ pytest>=8.0
10
+ hypothesis>=6.0
11
+ pyyaml>=6.0
12
+ dbt-metricflow<0.14,>=0.13
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"