jevcheck 0.2.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. jevcheck-0.2.0/.github/workflows/ci.yml +59 -0
  2. jevcheck-0.2.0/LICENSE +21 -0
  3. jevcheck-0.2.0/MANIFEST.in +10 -0
  4. jevcheck-0.2.0/PKG-INFO +162 -0
  5. jevcheck-0.2.0/README.md +138 -0
  6. jevcheck-0.2.0/docs/adr-009-two-model-compare.md +64 -0
  7. jevcheck-0.2.0/docs/contract.md +125 -0
  8. jevcheck-0.2.0/docs/jev-api.md +119 -0
  9. jevcheck-0.2.0/docs/release-readiness-audit-v0.1-recheck.md +111 -0
  10. jevcheck-0.2.0/docs/release-readiness-audit-v0.1.md +117 -0
  11. jevcheck-0.2.0/fixtures/replay-baseline.json +54 -0
  12. jevcheck-0.2.0/fixtures/replay-breaking.json +53 -0
  13. jevcheck-0.2.0/fixtures/replay-unchanged.json +54 -0
  14. jevcheck-0.2.0/fixtures/support-triage.json +97 -0
  15. jevcheck-0.2.0/fixtures/support-triage.jsonl +2 -0
  16. jevcheck-0.2.0/pyproject.toml +46 -0
  17. jevcheck-0.2.0/setup.cfg +4 -0
  18. jevcheck-0.2.0/src/jevcheck/__init__.py +57 -0
  19. jevcheck-0.2.0/src/jevcheck/__main__.py +3 -0
  20. jevcheck-0.2.0/src/jevcheck/answers.py +228 -0
  21. jevcheck-0.2.0/src/jevcheck/cli.py +356 -0
  22. jevcheck-0.2.0/src/jevcheck/client.py +78 -0
  23. jevcheck-0.2.0/src/jevcheck/compare.py +199 -0
  24. jevcheck-0.2.0/src/jevcheck/contract.py +314 -0
  25. jevcheck-0.2.0/src/jevcheck/eval.py +450 -0
  26. jevcheck-0.2.0/src/jevcheck/gate.py +61 -0
  27. jevcheck-0.2.0/src/jevcheck/pinning.py +101 -0
  28. jevcheck-0.2.0/src/jevcheck/py.typed +0 -0
  29. jevcheck-0.2.0/src/jevcheck/questions.py +100 -0
  30. jevcheck-0.2.0/src/jevcheck.egg-info/PKG-INFO +162 -0
  31. jevcheck-0.2.0/src/jevcheck.egg-info/SOURCES.txt +47 -0
  32. jevcheck-0.2.0/src/jevcheck.egg-info/dependency_links.txt +1 -0
  33. jevcheck-0.2.0/src/jevcheck.egg-info/entry_points.txt +2 -0
  34. jevcheck-0.2.0/src/jevcheck.egg-info/requires.txt +5 -0
  35. jevcheck-0.2.0/src/jevcheck.egg-info/top_level.txt +1 -0
  36. jevcheck-0.2.0/tests/__init__.py +0 -0
  37. jevcheck-0.2.0/tests/conftest.py +11 -0
  38. jevcheck-0.2.0/tests/helpers.py +34 -0
  39. jevcheck-0.2.0/tests/test_answers.py +175 -0
  40. jevcheck-0.2.0/tests/test_cli.py +614 -0
  41. jevcheck-0.2.0/tests/test_client.py +102 -0
  42. jevcheck-0.2.0/tests/test_compare.py +487 -0
  43. jevcheck-0.2.0/tests/test_contract.py +267 -0
  44. jevcheck-0.2.0/tests/test_eval.py +134 -0
  45. jevcheck-0.2.0/tests/test_eval_boundaries.py +194 -0
  46. jevcheck-0.2.0/tests/test_gate.py +35 -0
  47. jevcheck-0.2.0/tests/test_live.py +36 -0
  48. jevcheck-0.2.0/tests/test_pinning.py +112 -0
  49. jevcheck-0.2.0/tests/test_sdk_transport.py +296 -0
@@ -0,0 +1,59 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: pip install -e ".[dev]"
21
+ - name: Test
22
+ run: pytest
23
+
24
+ packaging:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+ - name: Build wheel and sdist
32
+ run: |
33
+ pip install build
34
+ python -m build
35
+ - name: Installed-wheel CLI smoke
36
+ run: |
37
+ python -m venv "$RUNNER_TEMP/wheel"
38
+ "$RUNNER_TEMP/wheel/bin/pip" install dist/*.whl
39
+ "$RUNNER_TEMP/wheel/bin/jevcheck" eval fixtures/support-triage.json \
40
+ --candidate-model jev-1.14 \
41
+ --answers fixtures/replay-unchanged.json
42
+ "$RUNNER_TEMP/wheel/bin/jevcheck" compare fixtures/support-triage.json \
43
+ --from fixtures/replay-baseline.json \
44
+ --to jev-1.14 \
45
+ --answers fixtures/replay-unchanged.json
46
+ "$RUNNER_TEMP/wheel/bin/python" -c "import jevcheck,inspect; print(jevcheck.__version__)"
47
+ - name: sdist includes tests/fixtures and pytest collects
48
+ run: |
49
+ python -m venv "$RUNNER_TEMP/sdist"
50
+ "$RUNNER_TEMP/sdist/bin/pip" install dist/*.tar.gz pytest
51
+ mkdir -p "$RUNNER_TEMP/srcdist"
52
+ tar -xzf dist/*.tar.gz -C "$RUNNER_TEMP/srcdist"
53
+ srcdir=$(echo "$RUNNER_TEMP/srcdist"/jevcheck-*)
54
+ test -f "$srcdir/tests/helpers.py"
55
+ test -f "$srcdir/tests/conftest.py"
56
+ test -f "$srcdir/fixtures/support-triage.json"
57
+ test -f "$srcdir/docs/contract.md"
58
+ cd "$srcdir"
59
+ "$RUNNER_TEMP/sdist/bin/pytest"
jevcheck-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nithilan Kumaran
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,10 @@
1
+ graft tests
2
+ graft fixtures
3
+ graft docs
4
+ include LICENSE
5
+ include README.md
6
+ include pyproject.toml
7
+ recursive-include .github *.yml
8
+ global-exclude *.py[cod]
9
+ global-exclude __pycache__
10
+ global-exclude .env
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: jevcheck
3
+ Version: 0.2.0
4
+ Summary: pytest for Jev — pin production behavioral contracts and eval candidate model upgrades
5
+ Author: Nithilan Kumaran
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sathariels/jevcheck
8
+ Project-URL: Documentation, https://github.com/sathariels/jevcheck
9
+ Keywords: jev,typesafe,eval,contract,model-upgrade
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pydantic>=2
20
+ Requires-Dist: typesafe-sdk>=0.7.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # jevcheck
26
+
27
+ **pytest for Jev.** Pin what production is allowed to do, eval a candidate model, and fail the upgrade when answers flip or confidence drops.
28
+
29
+ Probabilities and model versions move. A raw `0.94` is not a release decision. jevcheck records a **production contract** (baseline model + fixtures + expected answers) and evals a candidate against that fixture.
30
+
31
+ **v0.1** `eval` is fixture-versus-candidate. **v0.2** adds two-model execution: `record` a baseline model's answers, then `compare` a candidate against that snapshot (or fetch both models live). See [`docs/adr-009-two-model-compare.md`](docs/adr-009-two-model-compare.md).
32
+
33
+ The repo’s `jev-1.13` / `jev-1.14` strings are **unverified example pin labels** used by fixtures. A documented TypeSafe version pin (2026-09-19 model list) is `jev-1.13.0`. Floating aliases `jev-latest` and `jev-preview` are rejected unless you pass `--allow-unpinned`. With that opt-in, a response whose `model` is the concrete resolved ID (for example `jev-1.13.0`) is accepted; the eval report prints that resolved model. Concrete pins still require exact identity.
34
+
35
+ ```
36
+ pin contract → ship on the pinned model → jevcheck eval → compatible or breaking
37
+ → record → compare → compatible or breaking (v0.2)
38
+ ```
39
+
40
+ Reports: **unchanged** / **confidence regressions** / **answer flips**, with exact diffs (`billing→general`, `0.94→0.71`) and a nonzero exit on failure.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install -e ".[dev]"
46
+ export TYPESAFE_API_KEY=... # live Jev only; never commit this
47
+ ```
48
+
49
+ Auth is `TYPESAFE_API_KEY` only. Unit tests mock the network.
50
+
51
+ ## Pin → eval → upgrade
52
+
53
+ 1. Write a contract (see [`docs/contract.md`](docs/contract.md)) against the model you ship.
54
+ 2. Call Jev with that **explicit** model. `jev-latest` and `jev-preview` are rejected unless you opt in. Concrete pins require `response.model` to match exactly. An opted-in alias may resolve to a nonempty concrete (non-alias) response model, which is reported.
55
+ 3. Before upgrading, eval the candidate (example fixture label — not a verified live ID):
56
+
57
+ ```bash
58
+ # Live call. Pin a catalog version such as jev-1.13.0 in production.
59
+ # The example below matches this repo's replay fixtures only.
60
+ jevcheck eval fixtures/support-triage.json --candidate-model jev-1.14
61
+ ```
62
+
63
+ Replay recorded answers (CI / no key):
64
+
65
+ ```bash
66
+ jevcheck eval fixtures/support-triage.json \
67
+ --candidate-model jev-1.14 \
68
+ --answers fixtures/replay-breaking.json
69
+ ```
70
+
71
+ 4. Compatible (exit 0) → change the pin. Breaking (exit 1) → read the diffs; do not upgrade.
72
+
73
+ ## Upgrade flow (v0.2): record baseline → compare candidate
74
+
75
+ Two-model execution is a **v0.2** capability. `eval` above stays the v0.1 fixture path and is unchanged.
76
+
77
+ 1. Record answers from the model you ship. The file is the same replay JSON `eval --answers` already accepts:
78
+
79
+ ```bash
80
+ # Live (needs TYPESAFE_API_KEY). Pin a catalog version such as jev-1.13.0 in production.
81
+ jevcheck record fixtures/support-triage.json --out baseline-answers.json
82
+
83
+ # CI / no key: copy a previously recorded snapshot through the same command.
84
+ jevcheck record fixtures/support-triage.json \
85
+ --answers fixtures/replay-baseline.json \
86
+ --out baseline-answers.json
87
+ ```
88
+
89
+ `record` writes only after every answer kind matches the contract question (choice / noul / score). A mismatch is exit 2. Opted-in aliases print the resolved response model in the summary (`jev-preview → jev-1.13.0`).
90
+
91
+ 2. Compare the candidate against that snapshot:
92
+
93
+ ```bash
94
+ # Live candidate against recorded baseline
95
+ jevcheck compare fixtures/support-triage.json \
96
+ --from baseline-answers.json \
97
+ --to jev-1.14
98
+
99
+ # CI / no key: recorded baseline + candidate replay
100
+ jevcheck compare fixtures/support-triage.json \
101
+ --from fixtures/replay-baseline.json \
102
+ --to jev-1.14 \
103
+ --answers fixtures/replay-breaking.json
104
+ ```
105
+
106
+ 3. Or fetch both models in one step (live; needs a key):
107
+
108
+ ```bash
109
+ jevcheck compare fixtures/support-triage.json \
110
+ --from-model jev-1.13 \
111
+ --to jev-1.14
112
+ ```
113
+
114
+ `--from` and `--from-model` are mutually exclusive. Identity rules are the v0.1 pins: concrete names must match `response.model` exactly; `jev-latest` / `jev-preview` still need `--allow-unpinned`. Exit codes stay 0 compatible / 1 breaking / 2 usage-or-identity / 3 ops.
115
+
116
+ Verified System One fields: [`docs/jev-api.md`](docs/jev-api.md).
117
+
118
+ ## Example
119
+
120
+ ```python
121
+ from jevcheck import JevClient, evaluate, load_contract
122
+
123
+ contract = load_contract("fixtures/support-triage.json")
124
+ client = JevClient(model="jev-1.14") # example fixture label; pin a catalog version in production
125
+
126
+ report = evaluate(
127
+ contract,
128
+ lambda case: client.system_one(
129
+ state=case.state,
130
+ questions=case.questions,
131
+ model="jev-1.14",
132
+ ),
133
+ candidate_model="jev-1.14",
134
+ )
135
+ print(report.summary())
136
+ if report.breaking:
137
+ raise SystemExit(1)
138
+ ```
139
+
140
+ A `Gate` helper exists for auto / ask-human / reject thresholds. It is optional and is not the product.
141
+
142
+ ## Develop
143
+
144
+ ```bash
145
+ pip install -e ".[dev]"
146
+ pytest
147
+ python -m jevcheck eval fixtures/support-triage.json \
148
+ --candidate-model jev-1.14 \
149
+ --answers fixtures/replay-unchanged.json
150
+ python -m jevcheck compare fixtures/support-triage.json \
151
+ --from fixtures/replay-baseline.json \
152
+ --to jev-1.14 \
153
+ --answers fixtures/replay-unchanged.json
154
+ ```
155
+
156
+ ## For agents / audits
157
+
158
+ v0.2 two-model lock: [`docs/adr-009-two-model-compare.md`](docs/adr-009-two-model-compare.md).
159
+
160
+ See [`docs/release-readiness-audit-v0.1.md`](docs/release-readiness-audit-v0.1.md). Recheck: [`docs/release-readiness-audit-v0.1-recheck.md`](docs/release-readiness-audit-v0.1-recheck.md).
161
+
162
+ MIT.
@@ -0,0 +1,138 @@
1
+ # jevcheck
2
+
3
+ **pytest for Jev.** Pin what production is allowed to do, eval a candidate model, and fail the upgrade when answers flip or confidence drops.
4
+
5
+ Probabilities and model versions move. A raw `0.94` is not a release decision. jevcheck records a **production contract** (baseline model + fixtures + expected answers) and evals a candidate against that fixture.
6
+
7
+ **v0.1** `eval` is fixture-versus-candidate. **v0.2** adds two-model execution: `record` a baseline model's answers, then `compare` a candidate against that snapshot (or fetch both models live). See [`docs/adr-009-two-model-compare.md`](docs/adr-009-two-model-compare.md).
8
+
9
+ The repo’s `jev-1.13` / `jev-1.14` strings are **unverified example pin labels** used by fixtures. A documented TypeSafe version pin (2026-09-19 model list) is `jev-1.13.0`. Floating aliases `jev-latest` and `jev-preview` are rejected unless you pass `--allow-unpinned`. With that opt-in, a response whose `model` is the concrete resolved ID (for example `jev-1.13.0`) is accepted; the eval report prints that resolved model. Concrete pins still require exact identity.
10
+
11
+ ```
12
+ pin contract → ship on the pinned model → jevcheck eval → compatible or breaking
13
+ → record → compare → compatible or breaking (v0.2)
14
+ ```
15
+
16
+ Reports: **unchanged** / **confidence regressions** / **answer flips**, with exact diffs (`billing→general`, `0.94→0.71`) and a nonzero exit on failure.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install -e ".[dev]"
22
+ export TYPESAFE_API_KEY=... # live Jev only; never commit this
23
+ ```
24
+
25
+ Auth is `TYPESAFE_API_KEY` only. Unit tests mock the network.
26
+
27
+ ## Pin → eval → upgrade
28
+
29
+ 1. Write a contract (see [`docs/contract.md`](docs/contract.md)) against the model you ship.
30
+ 2. Call Jev with that **explicit** model. `jev-latest` and `jev-preview` are rejected unless you opt in. Concrete pins require `response.model` to match exactly. An opted-in alias may resolve to a nonempty concrete (non-alias) response model, which is reported.
31
+ 3. Before upgrading, eval the candidate (example fixture label — not a verified live ID):
32
+
33
+ ```bash
34
+ # Live call. Pin a catalog version such as jev-1.13.0 in production.
35
+ # The example below matches this repo's replay fixtures only.
36
+ jevcheck eval fixtures/support-triage.json --candidate-model jev-1.14
37
+ ```
38
+
39
+ Replay recorded answers (CI / no key):
40
+
41
+ ```bash
42
+ jevcheck eval fixtures/support-triage.json \
43
+ --candidate-model jev-1.14 \
44
+ --answers fixtures/replay-breaking.json
45
+ ```
46
+
47
+ 4. Compatible (exit 0) → change the pin. Breaking (exit 1) → read the diffs; do not upgrade.
48
+
49
+ ## Upgrade flow (v0.2): record baseline → compare candidate
50
+
51
+ Two-model execution is a **v0.2** capability. `eval` above stays the v0.1 fixture path and is unchanged.
52
+
53
+ 1. Record answers from the model you ship. The file is the same replay JSON `eval --answers` already accepts:
54
+
55
+ ```bash
56
+ # Live (needs TYPESAFE_API_KEY). Pin a catalog version such as jev-1.13.0 in production.
57
+ jevcheck record fixtures/support-triage.json --out baseline-answers.json
58
+
59
+ # CI / no key: copy a previously recorded snapshot through the same command.
60
+ jevcheck record fixtures/support-triage.json \
61
+ --answers fixtures/replay-baseline.json \
62
+ --out baseline-answers.json
63
+ ```
64
+
65
+ `record` writes only after every answer kind matches the contract question (choice / noul / score). A mismatch is exit 2. Opted-in aliases print the resolved response model in the summary (`jev-preview → jev-1.13.0`).
66
+
67
+ 2. Compare the candidate against that snapshot:
68
+
69
+ ```bash
70
+ # Live candidate against recorded baseline
71
+ jevcheck compare fixtures/support-triage.json \
72
+ --from baseline-answers.json \
73
+ --to jev-1.14
74
+
75
+ # CI / no key: recorded baseline + candidate replay
76
+ jevcheck compare fixtures/support-triage.json \
77
+ --from fixtures/replay-baseline.json \
78
+ --to jev-1.14 \
79
+ --answers fixtures/replay-breaking.json
80
+ ```
81
+
82
+ 3. Or fetch both models in one step (live; needs a key):
83
+
84
+ ```bash
85
+ jevcheck compare fixtures/support-triage.json \
86
+ --from-model jev-1.13 \
87
+ --to jev-1.14
88
+ ```
89
+
90
+ `--from` and `--from-model` are mutually exclusive. Identity rules are the v0.1 pins: concrete names must match `response.model` exactly; `jev-latest` / `jev-preview` still need `--allow-unpinned`. Exit codes stay 0 compatible / 1 breaking / 2 usage-or-identity / 3 ops.
91
+
92
+ Verified System One fields: [`docs/jev-api.md`](docs/jev-api.md).
93
+
94
+ ## Example
95
+
96
+ ```python
97
+ from jevcheck import JevClient, evaluate, load_contract
98
+
99
+ contract = load_contract("fixtures/support-triage.json")
100
+ client = JevClient(model="jev-1.14") # example fixture label; pin a catalog version in production
101
+
102
+ report = evaluate(
103
+ contract,
104
+ lambda case: client.system_one(
105
+ state=case.state,
106
+ questions=case.questions,
107
+ model="jev-1.14",
108
+ ),
109
+ candidate_model="jev-1.14",
110
+ )
111
+ print(report.summary())
112
+ if report.breaking:
113
+ raise SystemExit(1)
114
+ ```
115
+
116
+ A `Gate` helper exists for auto / ask-human / reject thresholds. It is optional and is not the product.
117
+
118
+ ## Develop
119
+
120
+ ```bash
121
+ pip install -e ".[dev]"
122
+ pytest
123
+ python -m jevcheck eval fixtures/support-triage.json \
124
+ --candidate-model jev-1.14 \
125
+ --answers fixtures/replay-unchanged.json
126
+ python -m jevcheck compare fixtures/support-triage.json \
127
+ --from fixtures/replay-baseline.json \
128
+ --to jev-1.14 \
129
+ --answers fixtures/replay-unchanged.json
130
+ ```
131
+
132
+ ## For agents / audits
133
+
134
+ v0.2 two-model lock: [`docs/adr-009-two-model-compare.md`](docs/adr-009-two-model-compare.md).
135
+
136
+ See [`docs/release-readiness-audit-v0.1.md`](docs/release-readiness-audit-v0.1.md). Recheck: [`docs/release-readiness-audit-v0.1-recheck.md`](docs/release-readiness-audit-v0.1-recheck.md).
137
+
138
+ MIT.
@@ -0,0 +1,64 @@
1
+ # ADR 009 — Two-model / baseline comparison (v0.2 slice)
2
+
3
+ Status: accepted for the v0.2 slice. Packaging version is `0.2.0`. The owner tags and publishes to PyPI after merge; this PR does not create a GitHub release. This document is the lock for `record` / `compare`.
4
+
5
+ v0.1 `eval` remains fixture-versus-candidate. This slice adds a second, explicit path so upgrade safety can mean **baseline answers versus candidate answers**, not only authored `expect` versus candidate.
6
+
7
+ ## Commands
8
+
9
+ | Command | Meaning |
10
+ | --- | --- |
11
+ | `record` | Given a v0.1 contract and a baseline model, fetch System One answers for every case and write a replay JSON. The file is the same shape as `eval --answers` (`dump_replay` / `load_replay`). |
12
+ | `compare` | Evaluate a candidate against those recorded baseline answers, **or** fetch both models live. |
13
+ | Baseline recording | The act of calling `record` (or an equivalent in-process `record_answers`). It does **not** rewrite the contract schema. Fixture `expect` stays the v0.1 authored contract. |
14
+
15
+ Chosen CLI (one UX, documented here):
16
+
17
+ ```
18
+ jevcheck record CONTRACT --out FILE [--baseline-model MODEL] [--answers REPLAY]
19
+ jevcheck compare CONTRACT --from FILE --to MODEL [--answers CANDIDATE_REPLAY]
20
+ jevcheck compare CONTRACT --from-model MODEL --to MODEL [--answers CANDIDATE_REPLAY]
21
+ ```
22
+
23
+ - `--from FILE` is a recorded baseline replay. `--from-model MODEL` is a live baseline fetch. Exactly one is required.
24
+ - `--to` is the candidate pin (same role as `eval --candidate-model`).
25
+ - `--answers` on `record` / `compare` is a mocked replay so CI and tests need no key. On `compare` it is the **candidate** replay.
26
+ - `--baseline-model` still overrides contract / JSONL metadata. With `--from-model`, that live pin is the baseline identity.
27
+ - `eval CONTRACT --candidate-model … [--answers]` is unchanged.
28
+
29
+ ## How compare decides
30
+
31
+ Compare does not invent a second scoring engine. It builds a derived contract: same cases, questions, defaults, and floors/tolerances; each `expect` field is filled from the baseline answer (`choice` / `noul`+`noul_true` / `score`, plus `baseline_confidence` from the [mapped scalar](jev-api.md#confidence-mapping-used-by-jevcheck)). Then it runs existing `evaluate()` on the candidate.
32
+
33
+ `record` and `compare` reject a wrong-kind answer (choice↔ChoiceAnswer, noul↔NoulAnswer, score↔ScoreAnswer) as a usage error (exit 2) rather than writing a snapshot `compare` cannot load. Missing baseline cases or missing expected fields are usage errors, not silent flips. Candidate missing answers stay v0.1 **answer flips**.
34
+
35
+ Compare does **not** invent a default `confidence_tolerance`. Drop detection uses the contract’s resolved defaults, same as v0.1.
36
+
37
+ ## Identity and pinning
38
+
39
+ Reuse v0.1 `require_pinned` / `require_response_identity` for every requested model and every response `model`:
40
+
41
+ - Concrete pins: exact string equality. `jev-1.13` is not `jev-1.13.0`.
42
+ - Floating aliases (`jev-latest`, `jev-preview`, or any name containing `latest` / `preview`) stay rejected unless `--allow-unpinned`.
43
+ - With that opt-in, a nonempty concrete (non-alias) response model is accepted and reported. Record and compare summaries print `alias → resolved` (for example `jev-preview → jev-1.13.0`). jevcheck never rewrites a name.
44
+
45
+ `record` identity-checks the baseline. `compare --from` identity-checks each recorded baseline response against the contract / `--baseline-model` pin. `compare --from-model` identity-checks the live baseline. The candidate is identity-checked as in `eval`.
46
+
47
+ ## Exit codes (unchanged)
48
+
49
+ | Code | Meaning |
50
+ | --- | --- |
51
+ | 0 | Compatible (`compare`) or successful write (`record`) |
52
+ | 1 | Behavioral contract failure (flips / confidence regressions) |
53
+ | 2 | Usage / input / identity / missing answers / unpinned without opt-in |
54
+ | 3 | Operational (SDK HTTP / invalid API body) |
55
+
56
+ Secret redaction on error text is unchanged.
57
+
58
+ ## Non-goals
59
+
60
+ - Not a pytest plugin.
61
+ - `Gate` is unchanged and is still not the product.
62
+ - Contract JSON schema stays v0.1 (`version: "0.1"`). Record writes a replay file; it does not emit a new contract format.
63
+ - No TypeSafe / System One fields beyond [docs/jev-api.md](jev-api.md).
64
+ - No `0.2.0` git tag in this PR.
@@ -0,0 +1,125 @@
1
+ # jevcheck contract format (v0.1)
2
+
3
+ A **contract** is the production baseline: pinned model, fixtures, and expected answers. `jevcheck eval` compares a candidate model against that contract and reports unchanged cases, confidence regressions, and answer flips.
4
+
5
+ This format is owner-locked for v0.1. Ask before changing it.
6
+
7
+ ## Files
8
+
9
+ - **JSON** (recommended): one object with metadata + `cases`.
10
+ - **JSONL**: one case object per line. Blank lines ignored. Optional first-line metadata object with `"_meta": true`.
11
+
12
+ ## Contract object
13
+
14
+ ```json
15
+ {
16
+ "version": "0.1",
17
+ "name": "support-triage",
18
+ "baseline_model": "jev-1.13",
19
+ "allow_unpinned": false,
20
+ "defaults": {
21
+ "min_confidence": 0.8,
22
+ "confidence_tolerance": 0.1,
23
+ "noul_true_threshold": 0.5,
24
+ "noul_tolerance": 0.1,
25
+ "score_tolerance": 0.5
26
+ },
27
+ "cases": []
28
+ }
29
+ ```
30
+
31
+ | Field | Required | Meaning |
32
+ | --- | --- | --- |
33
+ | `version` | yes | Must be `"0.1"` |
34
+ | `name` | no | Shown in the CLI summary |
35
+ | `baseline_model` | yes for JSON | Pinned production model this contract was recorded against. Empty `""` is rejected. Floating aliases (`jev-latest`, `jev-preview`, or any name containing `latest`/`preview`) are rejected unless `allow_unpinned` / `--allow-unpinned`. jevcheck does not rewrite aliases: `jev-1.13` is not `jev-1.13.0`. |
36
+ | `allow_unpinned` | no | If `true`, floating `jev-latest` / `jev-preview` names are allowed (default `false`) |
37
+ | `defaults` | no | Applied when a case field omits the same key |
38
+ | `cases` | yes for JSON | Fixture list |
39
+
40
+ JSONL without a `_meta` line still needs `--baseline-model` on the CLI (or a per-eval override).
41
+
42
+ ## Case object
43
+
44
+ ```json
45
+ {
46
+ "id": "ticket-001",
47
+ "state": "I was charged twice. Please fix this ASAP.",
48
+ "questions": {
49
+ "intent": {
50
+ "type": "choice",
51
+ "instructions": "What is this ticket about?",
52
+ "criteria": {"billing": null, "general": null}
53
+ }
54
+ },
55
+ "expect": {
56
+ "intent": {
57
+ "choice": "billing",
58
+ "min_confidence": 0.85,
59
+ "baseline_confidence": 0.94
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ `state` and `questions` match the verified System One request: see `docs/jev-api.md`. Question objects use `type` of `noul`, `choice`, or `score` and only those official fields.
66
+
67
+ Every key in `expect` must exist in `questions`. Extra unknown keys are rejected. An empty field expect (`{}`) is rejected. Kind-specific keys on the wrong question type (for example `score` on a choice question) are rejected. Shared confidence keys may appear on any field. This is an applicability check; the fixture key set is unchanged.
68
+
69
+ ## Field expectations
70
+
71
+ All keys optional except that a field must state at least one *effective* constraint (a kind-specific answer key, `min_noul`, `min_confidence`, or `baseline_confidence` **together with** a resolved `confidence_tolerance` or `min_confidence` floor). A baseline-confidence-only expect with no applicable floor or tolerance after defaults is rejected (exit 2). Do not assume a default `confidence_tolerance`. Tolerance-only objects are rejected.
72
+
73
+ | Key | Applies to | Meaning |
74
+ | --- | --- | --- |
75
+ | `choice` | choice | Expected winning label. Different label → **answer flip** |
76
+ | `noul_true` | noul | Expected yes (`true`) or no (`false`) vs `noul_true_threshold`. Opposite polarity → **answer flip** |
77
+ | `noul` | noul | Expected `noul` float. Used with `noul_tolerance` / `min_noul` |
78
+ | `min_noul` | noul | Floor on the `noul` float (yes-probability) |
79
+ | `noul_tolerance` | noul | Allowed absolute drift from `noul` (inclusive; exact 0.1 drop against 0.1 passes, with a 1e-9 epsilon so binary float subtraction of decimal tenths does not false-fail) |
80
+ | `score` | score | Expected expected-score. Nearest integer level change → **answer flip** |
81
+ | `score_tolerance` | score | **Level-flip suppression**, not an absolute max distance. If `\|actual - expected\|` is within this inclusive bound (same 1e-9 epsilon as confidence/noul; so 1.4 vs 1.6 at 0.2 is within), a nearest-level change is not a flip. When both values already share a nearest level (Python `round`, ties toward even: `1.5→2`, `2.5→2`), a larger float gap still counts as unchanged. Example: expected 1.8, actual 2.2, tolerance 0.1 → unchanged (both level 2). |
82
+ | `min_confidence` | all | Floor on the [mapped scalar](jev-api.md#confidence-mapping-used-by-jevcheck) |
83
+ | `baseline_confidence` | all | Previously recorded scalar (e.g. `0.94`) |
84
+ | `confidence_tolerance` | all | Allowed drop from `baseline_confidence` (inclusive, same 1e-9 epsilon as noul drift) |
85
+
86
+ Noul has no API `confidence`. The mapped scalar is `noul` itself.
87
+
88
+ ## Outcomes (per field, worst wins the case)
89
+
90
+ 1. **answer flip** — categorical change: choice label, noul yes/no, or score nearest level (unless `score_tolerance` suppresses that flip); or the answer is missing / wrong type.
91
+ 2. **confidence regression** — same answer, but the mapped scalar is below `min_confidence` / `min_noul`, or dropped more than `confidence_tolerance` from `baseline_confidence`, or noul drifted more than `noul_tolerance` from expected `noul`. Inclusive boundaries: a drop of exactly 0.1 against tolerance 0.1 is not a regression.
92
+ 3. **unchanged** — answer matches and scalars stay within the pinned floors/tolerances.
93
+
94
+ Choice and score answers must include a nonempty probability map of finite values `>= 0` whose sum is `1 ± 1e-6`. Missing, null, empty, negative, NaN, or badly normalized maps are rejected (exit 2). The adapter does not invent an empty map. Score answers also require a nonempty `legend` (SDK-shaped responses always include it). Score `legend` / `probabilities` coming from typesafe-sdk 0.7.0 may use integer keys; jevcheck stringifies those keys.
95
+
96
+ When `--candidate-model` is a **concrete pin**, every response `model` must equal that candidate exactly. A null or missing model is an error (never the string `"None"`). Opted-in floating aliases (`jev-latest`, `jev-preview`, or names containing those tokens) under `--allow-unpinned` accept a nonempty concrete response `model` that is not itself a floating alias; the eval report prints the resolved response model. Without `--allow-unpinned`, floating aliases stay rejected.
97
+
98
+ Repo examples such as `jev-1.13` / `jev-1.14` are **unverified example pin labels** for fixtures, not a claim that those IDs exist on the live TypeSafe catalog. A documented version pin on the 2026-09-19 model list is `jev-1.13.0`.
99
+
100
+ A higher candidate confidence is not a regression.
101
+
102
+ ## Replay file (`--answers`)
103
+
104
+ For mocked evals and tests, a JSON object keyed by case `id`:
105
+
106
+ ```json
107
+ {
108
+ "ticket-001": {
109
+ "model": "jev-1.14",
110
+ "usage": {"input_tokens": 10, "output_tokens": 4},
111
+ "answers": {
112
+ "intent": {
113
+ "type": "choice",
114
+ "choice": "general",
115
+ "confidence": 0.71,
116
+ "probabilities": {"billing": 0.29, "general": 0.71}
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ Answer objects use only verified System One fields.
124
+
125
+ `jevcheck record` (v0.2) writes this same file. `jevcheck compare --from` reads it as the baseline snapshot. See [`adr-009-two-model-compare.md`](adr-009-two-model-compare.md). `eval` remains fixture-versus-candidate.