jevlikedecisions 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 (81) hide show
  1. jevlikedecisions-0.1.0/.github/workflows/publish-root-python-package.yml +184 -0
  2. jevlikedecisions-0.1.0/.github/workflows/test-root-python-package.yml +288 -0
  3. jevlikedecisions-0.1.0/.gitignore +16 -0
  4. jevlikedecisions-0.1.0/DESIGN_PORTS_AND_ADAPTERS.md +9 -0
  5. jevlikedecisions-0.1.0/LICENSE +10 -0
  6. jevlikedecisions-0.1.0/MANIFEST.in +4 -0
  7. jevlikedecisions-0.1.0/PKG-INFO +136 -0
  8. jevlikedecisions-0.1.0/README.md +122 -0
  9. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_backend_aliases_and_selection_docs.md +135 -0
  10. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_caller_input_errors_must_not_report_as_internal_and_live_proof_needs_an_approval_stop.md +108 -0
  11. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_create.md +190 -0
  12. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_decision_audit_store_dispatch2_worktree_baseline_robust.md +122 -0
  13. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_decision_audit_store_records_request_reply_outcome_for_successes_and_failures.md +117 -0
  14. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_false_constant_name_and_unknown_backend_misdiagnosis.md +113 -0
  15. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_fix_provenance_and_falsifiable_route_guard.md +205 -0
  16. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_fix_t1_single_use_approval_gate.md +92 -0
  17. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_http_and_cli_surfaces_do_not_honour_the_settled_error_contract.md +121 -0
  18. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_live_openai_and_jev_provider_proof.md +119 -0
  19. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_schema_verify_and_zero_spend_estimate.md +85 -0
  20. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_sole_dependency_is_llm_spendguard.md +156 -0
  21. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_surface_completeness_audit.md +135 -0
  22. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_surfaces_cli_api_mcp_and_estimate_fix.md +122 -0
  23. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_typed_refusals_config_validation_and_live_proof_in_repo.md +222 -0
  24. jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_unknown_output_tokens_must_not_become_a_ledger_zero.md +160 -0
  25. jevlikedecisions-0.1.0/examples/paid_decision_live_proof.example.json +15 -0
  26. jevlikedecisions-0.1.0/pyproject.toml +36 -0
  27. jevlikedecisions-0.1.0/scripts/judge_capability_documentation.py +474 -0
  28. jevlikedecisions-0.1.0/scripts/migrate_capability_documentation_judging_instrument.py +52 -0
  29. jevlikedecisions-0.1.0/scripts/run_paid_decision_live_proof.py +89 -0
  30. jevlikedecisions-0.1.0/setup.cfg +4 -0
  31. jevlikedecisions-0.1.0/src/jevlikedecisions/__init__.py +25 -0
  32. jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/__init__.py +4 -0
  33. jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/mock_jevlike.py +22 -0
  34. jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/openai_decisions.py +73 -0
  35. jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/typesafe_jev.py +54 -0
  36. jevlikedecisions-0.1.0/src/jevlikedecisions/capability_contracts.py +187 -0
  37. jevlikedecisions-0.1.0/src/jevlikedecisions/cli.py +100 -0
  38. jevlikedecisions-0.1.0/src/jevlikedecisions/config/capability_documentation_verdicts.json +550 -0
  39. jevlikedecisions-0.1.0/src/jevlikedecisions/config/jevlikedecisions.json +91 -0
  40. jevlikedecisions-0.1.0/src/jevlikedecisions/configuration.py +243 -0
  41. jevlikedecisions-0.1.0/src/jevlikedecisions/core.py +218 -0
  42. jevlikedecisions-0.1.0/src/jevlikedecisions/decision_audit_store.py +95 -0
  43. jevlikedecisions-0.1.0/src/jevlikedecisions/documentation_verdicts.py +86 -0
  44. jevlikedecisions-0.1.0/src/jevlikedecisions/errors.py +25 -0
  45. jevlikedecisions-0.1.0/src/jevlikedecisions/estimate_approvals.py +124 -0
  46. jevlikedecisions-0.1.0/src/jevlikedecisions/guards.py +84 -0
  47. jevlikedecisions-0.1.0/src/jevlikedecisions/http_surface.py +114 -0
  48. jevlikedecisions-0.1.0/src/jevlikedecisions/mcp_surface.py +192 -0
  49. jevlikedecisions-0.1.0/src/jevlikedecisions/ports.py +19 -0
  50. jevlikedecisions-0.1.0/src/jevlikedecisions/pricing_metering.py +105 -0
  51. jevlikedecisions-0.1.0/src/jevlikedecisions/receipts.py +22 -0
  52. jevlikedecisions-0.1.0/src/jevlikedecisions/registry.py +19 -0
  53. jevlikedecisions-0.1.0/src/jevlikedecisions/surface_contract.py +84 -0
  54. jevlikedecisions-0.1.0/src/jevlikedecisions/validation.py +74 -0
  55. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/PKG-INFO +136 -0
  56. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/SOURCES.txt +79 -0
  57. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/dependency_links.txt +1 -0
  58. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/entry_points.txt +4 -0
  59. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/requires.txt +5 -0
  60. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/scm_file_list.json +75 -0
  61. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/scm_version.json +8 -0
  62. jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/top_level.txt +1 -0
  63. jevlikedecisions-0.1.0/tests/conftest.py +36 -0
  64. jevlikedecisions-0.1.0/tests/fixtures/subscription_glm_captured_reply.txt +1 -0
  65. jevlikedecisions-0.1.0/tests/fixtures/subscription_probe_questions.json +698 -0
  66. jevlikedecisions-0.1.0/tests/test_adapter_wire_shapes.py +27 -0
  67. jevlikedecisions-0.1.0/tests/test_backend_alias_contract.py +314 -0
  68. jevlikedecisions-0.1.0/tests/test_capability_contract_conformance.py +162 -0
  69. jevlikedecisions-0.1.0/tests/test_capability_documentation_judging_record.py +790 -0
  70. jevlikedecisions-0.1.0/tests/test_decision_audit_store.py +109 -0
  71. jevlikedecisions-0.1.0/tests/test_g1_g2_guards.py +20 -0
  72. jevlikedecisions-0.1.0/tests/test_hard_requirements.py +50 -0
  73. jevlikedecisions-0.1.0/tests/test_live_backend_offline_contracts.py +222 -0
  74. jevlikedecisions-0.1.0/tests/test_metered_and_naming.py +53 -0
  75. jevlikedecisions-0.1.0/tests/test_non_persisting_estimate.py +227 -0
  76. jevlikedecisions-0.1.0/tests/test_o1_o2_conformance.py +23 -0
  77. jevlikedecisions-0.1.0/tests/test_provenance_selection_and_audit.py +172 -0
  78. jevlikedecisions-0.1.0/tests/test_single_use_approval_gate.py +196 -0
  79. jevlikedecisions-0.1.0/tests/test_stdlib_mcp_and_dependency_boundary.py +295 -0
  80. jevlikedecisions-0.1.0/tests/test_surfaces_and_whole_request_estimate.py +331 -0
  81. jevlikedecisions-0.1.0/tests/test_typed_refusals_configuration_and_live_proof.py +416 -0
@@ -0,0 +1,184 @@
1
+ name: Publish root Python package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ env:
9
+ DISTRIBUTION_DIRECTORY_NAME: distributions
10
+
11
+ jobs:
12
+ build-verified-distributions:
13
+ name: Build and verify tagged distributions
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ env:
18
+ PROJECT_METADATA_FILE: ${{ github.workspace }}/pyproject.toml
19
+ DISTRIBUTION_ARTIFACT_NAME: verified-distributions
20
+ steps:
21
+ - name: Initialize the build distribution directory
22
+ env:
23
+ RUNNER_TEMP_DIRECTORY: ${{ runner.temp }}
24
+ run: echo "DISTRIBUTION_DIRECTORY=$RUNNER_TEMP_DIRECTORY/$DISTRIBUTION_DIRECTORY_NAME" >> "$GITHUB_ENV"
25
+
26
+ - name: Check out the complete tagged history
27
+ uses: actions/checkout@v4
28
+ with:
29
+ fetch-depth: 0
30
+
31
+ - name: Derive the build Python version from project metadata
32
+ id: build-python
33
+ shell: bash
34
+ run: |
35
+ python - <<'PY'
36
+ import os
37
+ import pathlib
38
+ import tomllib
39
+
40
+ metadata_path = pathlib.Path(os.environ["PROJECT_METADATA_FILE"])
41
+ requires_python = tomllib.loads(metadata_path.read_text())["project"]["requires-python"]
42
+ lower_bounds = [
43
+ bound.strip().removeprefix(">=")
44
+ for bound in requires_python.split(",")
45
+ if bound.strip().startswith(">=")
46
+ ]
47
+ if not lower_bounds:
48
+ raise SystemExit(
49
+ "::error title=Python floor derivation failed::"
50
+ "project.requires-python has no inclusive lower bound"
51
+ )
52
+ declared_floor_minor = max(lower_bounds, key=lambda value: tuple(map(int, value.split("."))))
53
+ with pathlib.Path(os.environ["GITHUB_OUTPUT"]).open("a") as output_file:
54
+ output_file.write(f"version={declared_floor_minor}\n")
55
+ PY
56
+
57
+ - name: Set up the metadata-declared build Python
58
+ uses: actions/setup-python@v5
59
+ with:
60
+ python-version: ${{ steps.build-python.outputs.version }}
61
+
62
+ - name: Install distribution tooling
63
+ run: python -m pip install build packaging twine
64
+
65
+ - name: Build the source and wheel distributions once
66
+ run: python -m build --outdir "$DISTRIBUTION_DIRECTORY"
67
+
68
+ - name: Assert artifact versions match the triggering tag
69
+ env:
70
+ TRIGGERING_TAG: ${{ github.ref_name }}
71
+ shell: bash
72
+ run: |
73
+ python - <<'PY'
74
+ import email
75
+ import glob
76
+ import os
77
+ import pathlib
78
+ import tarfile
79
+ import zipfile
80
+
81
+ from packaging.version import InvalidVersion, Version
82
+
83
+ distribution_directory = pathlib.Path(os.environ["DISTRIBUTION_DIRECTORY"])
84
+ wheel_paths = [pathlib.Path(path) for path in glob.glob(str(distribution_directory / "*.whl"))]
85
+ sdist_paths = [pathlib.Path(path) for path in glob.glob(str(distribution_directory / "*.tar.gz"))]
86
+ if len(wheel_paths) != 1 or len(sdist_paths) != 1:
87
+ raise SystemExit(
88
+ "::error title=Distribution set invalid::"
89
+ f"expected one wheel and one sdist, found {len(wheel_paths)} wheel(s) "
90
+ f"and {len(sdist_paths)} sdist(s)"
91
+ )
92
+
93
+ with zipfile.ZipFile(wheel_paths[0]) as wheel_archive:
94
+ wheel_metadata_names = [
95
+ name for name in wheel_archive.namelist() if name.endswith(".dist-info/METADATA")
96
+ ]
97
+ if len(wheel_metadata_names) != 1:
98
+ raise SystemExit(
99
+ "::error title=Wheel metadata invalid::"
100
+ f"expected one METADATA file, found {len(wheel_metadata_names)}"
101
+ )
102
+ wheel_metadata = email.message_from_bytes(wheel_archive.read(wheel_metadata_names[0]))
103
+
104
+ with tarfile.open(sdist_paths[0], mode="r:gz") as sdist_archive:
105
+ sdist_metadata_members = [
106
+ member
107
+ for member in sdist_archive.getmembers()
108
+ if pathlib.PurePosixPath(member.name).name == "PKG-INFO"
109
+ and len(pathlib.PurePosixPath(member.name).parts) == 2
110
+ ]
111
+ if len(sdist_metadata_members) != 1:
112
+ raise SystemExit(
113
+ "::error title=Source distribution metadata invalid::"
114
+ f"expected one root package PKG-INFO file, found {len(sdist_metadata_members)}"
115
+ )
116
+ extracted_metadata = sdist_archive.extractfile(sdist_metadata_members[0])
117
+ if extracted_metadata is None:
118
+ raise SystemExit(
119
+ "::error title=Source distribution metadata unreadable::"
120
+ "could not read PKG-INFO"
121
+ )
122
+ sdist_metadata = email.message_from_binary_file(extracted_metadata)
123
+
124
+ tag_name = os.environ["TRIGGERING_TAG"]
125
+ if not tag_name.startswith("v") or len(tag_name) == 1:
126
+ raise SystemExit(
127
+ "::error title=Triggering tag invalid::"
128
+ f"expected a v-prefixed version tag, received {tag_name!r}"
129
+ )
130
+ try:
131
+ expected_version = Version(tag_name.removeprefix("v"))
132
+ wheel_version = Version(wheel_metadata["Version"])
133
+ sdist_version = Version(sdist_metadata["Version"])
134
+ except (InvalidVersion, TypeError) as version_error:
135
+ raise SystemExit(
136
+ "::error title=Distribution version unreadable::"
137
+ f"could not parse the tag or artifact version: {version_error}"
138
+ ) from version_error
139
+
140
+ if wheel_version != expected_version or sdist_version != expected_version:
141
+ raise SystemExit(
142
+ "::error title=Distribution version mismatch::"
143
+ f"tag expects {expected_version}; wheel contains {wheel_version}; "
144
+ f"source distribution contains {sdist_version}"
145
+ )
146
+ PY
147
+
148
+ - name: Validate both distributions with Twine
149
+ run: python -m twine check "$DISTRIBUTION_DIRECTORY"/*
150
+
151
+ - name: Upload the verified distribution bytes
152
+ uses: actions/upload-artifact@v4
153
+ with:
154
+ name: ${{ env.DISTRIBUTION_ARTIFACT_NAME }}
155
+ path: ${{ env.DISTRIBUTION_DIRECTORY }}
156
+ if-no-files-found: error
157
+
158
+ publish-verified-distributions-to-pypi:
159
+ name: Publish verified distributions to PyPI
160
+ needs: build-verified-distributions
161
+ runs-on: ubuntu-latest
162
+ environment:
163
+ name: pypi
164
+ permissions:
165
+ contents: read
166
+ id-token: write
167
+ env:
168
+ DISTRIBUTION_ARTIFACT_NAME: verified-distributions
169
+ steps:
170
+ - name: Initialize the PyPI distribution directory
171
+ env:
172
+ RUNNER_TEMP_DIRECTORY: ${{ runner.temp }}
173
+ run: echo "DISTRIBUTION_DIRECTORY=$RUNNER_TEMP_DIRECTORY/$DISTRIBUTION_DIRECTORY_NAME" >> "$GITHUB_ENV"
174
+
175
+ - name: Download the verified distribution bytes
176
+ uses: actions/download-artifact@v4
177
+ with:
178
+ name: ${{ env.DISTRIBUTION_ARTIFACT_NAME }}
179
+ path: ${{ env.DISTRIBUTION_DIRECTORY }}
180
+
181
+ - name: Publish verified distributions to PyPI
182
+ uses: pypa/gh-action-pypi-publish@release/v1
183
+ with:
184
+ packages-dir: ${{ env.DISTRIBUTION_DIRECTORY }}
@@ -0,0 +1,288 @@
1
+ name: Test root Python package
2
+
3
+ on:
4
+ # PR commits run via pull_request; scope pushes to main to avoid duplicate runs.
5
+ push:
6
+ branches:
7
+ - main
8
+ pull_request:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ lint-github-actions-workflows:
15
+ name: Lint every GitHub Actions workflow
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Check out workflow files
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Validate every workflow and expression context
22
+ uses: raven-actions/actionlint@v2
23
+ with:
24
+ version: 1.7.7
25
+
26
+ derive-python-test-matrix:
27
+ name: Derive Python test versions from package metadata
28
+ runs-on: ubuntu-latest
29
+ outputs:
30
+ matrix: ${{ steps.python-test-matrix.outputs.matrix }}
31
+ steps:
32
+ - name: Check out package metadata
33
+ uses: actions/checkout@v4
34
+
35
+ - name: Derive every supported stable Python minor from package metadata
36
+ id: python-test-matrix
37
+ env:
38
+ PROJECT_METADATA_FILE: ./pyproject.toml
39
+ # setup-python resolves releases from this maintained manifest; using the
40
+ # same source keeps the matrix aligned with versions the action can install.
41
+ PYTHON_VERSIONS_MANIFEST_URL: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
42
+ shell: python
43
+ run: |
44
+ import json
45
+ import os
46
+ import pathlib
47
+ import re
48
+ import tomllib
49
+ import urllib.error
50
+ import urllib.request
51
+
52
+ VERSION_PATTERN = re.compile(r"^\d+(?:\.\d+)+$")
53
+ SPECIFIER_PATTERN = re.compile(r"^(>=|<=|<)\s*(\d+(?:\.\d+)+)$")
54
+ BOUND_PATTERN = re.compile(
55
+ r"(?:^|,)\s*(>=|<=|<)\s*(\d+(?:\.\d+)+)\s*(?=,|$)"
56
+ )
57
+
58
+ def version_tuple(version_text):
59
+ return tuple(int(part) for part in version_text.split("."))
60
+
61
+ def padded_version(version, width):
62
+ return version + (0,) * (width - len(version))
63
+
64
+ def version_satisfies_bound(version, operator, bound):
65
+ width = max(len(version), len(bound))
66
+ comparable_version = padded_version(version, width)
67
+ comparable_bound = padded_version(bound, width)
68
+ if operator == ">=":
69
+ return comparable_version >= comparable_bound
70
+ if operator == "<":
71
+ return comparable_version < comparable_bound
72
+ return comparable_version <= comparable_bound
73
+
74
+ metadata_path = pathlib.Path(os.environ["PROJECT_METADATA_FILE"])
75
+ with metadata_path.open("rb") as metadata_file:
76
+ metadata = tomllib.load(metadata_file)
77
+
78
+ requires_python = metadata["project"].get("requires-python", "")
79
+ specifiers = [specifier.strip() for specifier in requires_python.split(",")]
80
+ if not specifiers or any(
81
+ not SPECIFIER_PATTERN.fullmatch(specifier) for specifier in specifiers
82
+ ):
83
+ raise SystemExit(
84
+ "UNSUPPORTED_REQUIRES_PYTHON_SPECIFIER: "
85
+ "expected only >=, <, or <= version bounds"
86
+ )
87
+ bounds = [
88
+ (operator, version_tuple(version_text))
89
+ for operator, version_text in BOUND_PATTERN.findall(requires_python)
90
+ ]
91
+ lower_bounds = [bound for operator, bound in bounds if operator == ">="]
92
+ if not lower_bounds:
93
+ raise SystemExit(
94
+ "MISSING_INCLUSIVE_PYTHON_LOWER_BOUND: "
95
+ "project.requires-python must contain an inclusive >= lower bound"
96
+ )
97
+
98
+ manifest_url = os.environ["PYTHON_VERSIONS_MANIFEST_URL"]
99
+ try:
100
+ with urllib.request.urlopen(manifest_url) as manifest_response:
101
+ manifest = json.load(manifest_response)
102
+ except (OSError, urllib.error.URLError, json.JSONDecodeError) as error:
103
+ raise SystemExit(f"PYTHON_VERSION_DISCOVERY_FAILED: {error}") from error
104
+
105
+ if not isinstance(manifest, list):
106
+ raise SystemExit(
107
+ "PYTHON_VERSION_MANIFEST_SHAPE_CHANGED: expected a JSON list"
108
+ )
109
+
110
+ stable_versions = []
111
+ for release in manifest:
112
+ if not isinstance(release, dict) or "version" not in release or "stable" not in release:
113
+ raise SystemExit(
114
+ "PYTHON_VERSION_MANIFEST_SHAPE_CHANGED: "
115
+ "each release must contain version and stable fields"
116
+ )
117
+ version_text = release["version"]
118
+ if release["stable"] is True and VERSION_PATTERN.fullmatch(version_text):
119
+ stable_versions.append(version_tuple(version_text))
120
+
121
+ if not stable_versions:
122
+ raise SystemExit(
123
+ "EMPTY_STABLE_PYTHON_VERSION_DISCOVERY: "
124
+ "the versions manifest contained no stable releases"
125
+ )
126
+
127
+ supported_minors = sorted(
128
+ {
129
+ release[:2]
130
+ for release in stable_versions
131
+ if all(
132
+ version_satisfies_bound(release, operator, bound)
133
+ for operator, bound in bounds
134
+ )
135
+ }
136
+ )
137
+ if not supported_minors:
138
+ raise SystemExit(
139
+ "EMPTY_SUPPORTED_PYTHON_MINOR_RANGE: "
140
+ "no discovered stable release satisfies project.requires-python"
141
+ )
142
+
143
+ declared_floor_minor = max(lower_bounds)[:2]
144
+ latest_stable_minor = max(stable_versions)[:2]
145
+ matrix_entries = []
146
+ for supported_minor in supported_minors:
147
+ if supported_minor == declared_floor_minor:
148
+ version_source = "declared-floor"
149
+ elif supported_minor == latest_stable_minor:
150
+ version_source = "latest-stable"
151
+ else:
152
+ version_source = "intermediate-supported-minor"
153
+ matrix_entries.append(
154
+ {
155
+ "python-version": ".".join(map(str, supported_minor)),
156
+ "version-source": version_source,
157
+ }
158
+ )
159
+
160
+ matrix = {"include": matrix_entries}
161
+ matrix_json = json.dumps(matrix, separators=(",", ":"))
162
+ print(matrix_json)
163
+ github_output = os.environ.get("GITHUB_OUTPUT")
164
+ if github_output:
165
+ with pathlib.Path(github_output).open("a") as output_file:
166
+ print(f"matrix={matrix_json}", file=output_file)
167
+
168
+ test-root-python-package:
169
+ name: Test root package on Python ${{ matrix.python-version }} (${{ matrix.version-source }})
170
+ needs: derive-python-test-matrix
171
+ runs-on: ubuntu-latest
172
+ strategy:
173
+ fail-fast: false
174
+ matrix: ${{ fromJSON(needs.derive-python-test-matrix.outputs.matrix) }}
175
+ steps:
176
+ - name: Check out root package for Python ${{ matrix.python-version }}
177
+ uses: actions/checkout@v4
178
+
179
+ - name: Set up Python ${{ matrix.python-version }}
180
+ uses: actions/setup-python@v5
181
+ with:
182
+ python-version: ${{ matrix.python-version }}
183
+
184
+ - name: Install root package with dev dependencies on Python ${{ matrix.python-version }}
185
+ run: python -m pip install -e ".[dev]"
186
+
187
+ - name: Run whole test suite on Python ${{ matrix.python-version }}
188
+ run: python -m pytest tests -q
189
+
190
+ prove-built-distributions:
191
+ name: Prove built distributions on Python ${{ matrix.python-version }}
192
+ needs: derive-python-test-matrix
193
+ runs-on: ubuntu-latest
194
+ strategy:
195
+ fail-fast: false
196
+ matrix: ${{ fromJSON(needs.derive-python-test-matrix.outputs.matrix) }}
197
+ steps:
198
+ - name: Check out complete version history for distribution metadata
199
+ uses: actions/checkout@v4
200
+ with:
201
+ fetch-depth: 0
202
+
203
+ - name: Set up derived Python ${{ matrix.python-version }}
204
+ uses: actions/setup-python@v5
205
+ with:
206
+ python-version: ${{ matrix.python-version }}
207
+
208
+ - name: Install distribution proof tools
209
+ run: python -m pip install build twine
210
+
211
+ - name: Build wheel and source distribution
212
+ run: python -m build
213
+
214
+ - name: Assert packaged configuration is in wheel and source distribution
215
+ run: |
216
+ python - <<'PY'
217
+ import pathlib
218
+ import tarfile
219
+ import zipfile
220
+
221
+ distribution_directory = pathlib.Path("dist")
222
+ wheel_paths = tuple(distribution_directory.glob("*.whl"))
223
+ source_distribution_paths = tuple(distribution_directory.glob("*.tar.gz"))
224
+ expected_resource_suffix = "jevlikedecisions/config/jevlikedecisions.json"
225
+
226
+ if len(wheel_paths) != 1:
227
+ raise AssertionError(f"WHEEL_ARTIFACT_COUNT_ERROR: found {len(wheel_paths)} wheels")
228
+ if len(source_distribution_paths) != 1:
229
+ raise AssertionError(
230
+ "SOURCE_DISTRIBUTION_ARTIFACT_COUNT_ERROR: "
231
+ f"found {len(source_distribution_paths)} source distributions"
232
+ )
233
+
234
+ with zipfile.ZipFile(wheel_paths[0]) as wheel_archive:
235
+ wheel_members = wheel_archive.namelist()
236
+ if expected_resource_suffix not in wheel_members:
237
+ raise AssertionError(
238
+ f"WHEEL_CONFIG_RESOURCE_MISSING: {expected_resource_suffix}"
239
+ )
240
+
241
+ with tarfile.open(source_distribution_paths[0], mode="r:gz") as source_archive:
242
+ source_distribution_members = source_archive.getnames()
243
+ if not any(
244
+ member.endswith(f"/{expected_resource_suffix}")
245
+ for member in source_distribution_members
246
+ ):
247
+ raise AssertionError(
248
+ f"SOURCE_DISTRIBUTION_CONFIG_RESOURCE_MISSING: {expected_resource_suffix}"
249
+ )
250
+ PY
251
+
252
+ - name: Validate wheel and source distribution metadata
253
+ run: python -m twine check dist/*
254
+
255
+ - name: Prove installed wheel loads its packaged configuration off source tree
256
+ working-directory: ${{ runner.temp }}
257
+ env:
258
+ INSTALLED_WHEEL_PROOF_VENV: ${{ runner.temp }}/jevlikedecisions-installed-wheel-proof
259
+ run: |
260
+ python -m venv "$INSTALLED_WHEEL_PROOF_VENV"
261
+ "$INSTALLED_WHEEL_PROOF_VENV/bin/python" -m pip install "$GITHUB_WORKSPACE"/dist/*.whl
262
+ "$INSTALLED_WHEEL_PROOF_VENV/bin/python" - <<'PY'
263
+ import pathlib
264
+ import os
265
+ from importlib.resources import files
266
+
267
+ import jevlikedecisions
268
+ from jevlikedecisions.configuration import load_decision_configuration
269
+
270
+ repository_root = pathlib.Path(os.environ["GITHUB_WORKSPACE"]).resolve()
271
+ installed_module_path = pathlib.Path(jevlikedecisions.__file__).resolve()
272
+ installed_package_resource = files("jevlikedecisions")
273
+ installed_config_resource = installed_package_resource.joinpath(
274
+ "config/jevlikedecisions.json"
275
+ )
276
+
277
+ if installed_module_path.is_relative_to(repository_root):
278
+ raise AssertionError(
279
+ f"SOURCE_TREE_IMPORT_FALLBACK_ERROR: imported {installed_module_path}"
280
+ )
281
+ if not installed_config_resource.is_file():
282
+ raise AssertionError(
283
+ f"INSTALLED_CONFIG_RESOURCE_MISSING: {installed_config_resource}"
284
+ )
285
+ installed_configuration = load_decision_configuration()
286
+ if not isinstance(installed_configuration, dict) or not installed_configuration:
287
+ raise AssertionError("INSTALLED_CONFIG_LOAD_ERROR: loader returned no configuration")
288
+ PY
@@ -0,0 +1,16 @@
1
+ .DS_Store
2
+
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+
9
+ .pytest_cache/
10
+ .coverage
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+
14
+ .venv.nosync/
15
+ .venv/
16
+ venv/
@@ -0,0 +1,9 @@
1
+ # Ports-and-adapters design
2
+
3
+ The core validates canonical questions, selects an adapter by configured name, estimates through spendguard pricing, and validates canonical answers. It contains no provider endpoint, model, key, or price.
4
+
5
+ `DecisionBackendPort` is structural. Each adapter alone owns its wire envelope and declares its supported question types. An unforeseen adapter is registered in the internal registry and enters the identical orchestration without editing the core or another adapter. An unsupported question type raises a typed refusal; there is no fallback.
6
+
7
+ Paid transports implement the runtime-checked `MeteredDecisionTransportPort`. The port returns provider data together with usage, ledger id, cost-completeness state, and the known input-cost floor. The default transport requires the active spendguard gate, uses the provider key named by config, records actual response usage through spendguard, and withholds a completely settled result unless a ledger id is returned. Tests may inject a conforming transport fixture; this dispatch deliberately made no live call and spent zero dollars.
8
+
9
+ Named gap: `anyjev` has no separately verified schema in the fetched evidence, so v1 does not invent an adapter.
@@ -0,0 +1,10 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ash Damle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
+
@@ -0,0 +1,4 @@
1
+ include LICENSE README.md DESIGN_PORTS_AND_ADAPTERS.md
2
+ recursive-include src/jevlikedecisions/config *.json
3
+ recursive-include tests/fixtures *.json
4
+
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: jevlikedecisions
3
+ Version: 0.1.0
4
+ Summary: One typed decision abstraction over jev-like decision-model providers.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: llm-spendguard>=0.16.30
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8; extra == "dev"
12
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
13
+ Dynamic: license-file
14
+
15
+ # jevlikedecisions
16
+
17
+ `jevlikedecisions.decide(...)` is one typed decision entrypoint over a family of provider adapters. Shared evidence is sent once, in full, and responses are checked against both the provider shape and the originating questions. Backend selection is mandatory: no call silently falls back to the deterministic `mock_jevlike` backend.
18
+
19
+ ## Installation and spend-gate setup
20
+
21
+ Python 3.11 or newer is required.
22
+
23
+ Install the package and its sole runtime dependency, `llm-spendguard`, with pip:
24
+
25
+ ```console
26
+ python -m pip install jevlikedecisions
27
+ ```
28
+
29
+ > **Pending first release:** The package is not yet published on PyPI. Until the first release is published, clone this repository, change into the clone, and run `python -m pip install .`. Delete this note when publishing the first release.
30
+
31
+ Before making a model call, arm the spend gate in the virtual environment. This is required: `jevlikedecisions` deliberately refuses to call a model from an interpreter where spendguard is not enforcing. With the target virtual environment activated, run:
32
+
33
+ ```console
34
+ spendguard install-hook --venv "$VIRTUAL_ENV" --no-pkg
35
+ ```
36
+
37
+ > **Temporary upstream workaround:** `--no-pkg` avoids a `FileNotFoundError` for pip-installed, non-editable `llm-spendguard` environments where `install-hook` otherwise looks for a repository `pyproject.toml`. Remove this flag and note after that upstream defect is fixed.
38
+
39
+ Verify the activated interpreter:
40
+
41
+ ```console
42
+ spendguard doctor
43
+ ```
44
+
45
+ The successful verdict line is `ENFORCING HERE: 🟢 YES — calls from this interpreter are gated`.
46
+
47
+ > **Temporary upstream workaround:** `spendguard doctor` may currently exit non-zero after printing that successful verdict, with `ValueError: Pricing returned no token cost`. Read the verdict line rather than the exit code; remove this note after that upstream defect is fixed.
48
+
49
+ If gate setup is skipped, `decide()` refuses model calls through `require_enforcing_spendguard`.
50
+
51
+ ## Request and question schema
52
+
53
+ Every surface accepts the same JSON object:
54
+
55
+ ```json
56
+ {
57
+ "input_parts": ["complete evidence"],
58
+ "questions": [{"name": "safe", "type": "predicate", "description": "Is it safe?"}],
59
+ "backend": "mock_jevlike",
60
+ "intent": "documented-example",
61
+ "prompt_cache_key": null
62
+ }
63
+ ```
64
+
65
+ `intent` and `backend` are required. Every decision result identifies `backend`, `provider`, `model`, `route`, `via`, `ledger_id`, `usage`, `cost_complete`, and `known_input_cost_floor_usd`. Inspect `route` to distinguish in-process and metered results. `cost_complete` says whether the cost is fully settled; when a provider omits output-token usage, a metered result has `cost_complete: false`, no ledger id, and only the known input-cost floor. In-process results are settled at zero cost. Every question has `name`, `type`, and `description`. `type` is `predicate`, `choice`, or `score`. A choice also has `choices`, whose entries contain `value` and `description`. A score also has `levels`, whose entries contain `label` and `description`. Library validation is authoritative for every surface.
66
+
67
+ Backend names and aliases come from the package configuration. An alias is accepted only at selection; estimates, approvals, receipts, audit evidence, and returned provenance always use the canonical backend name. Omitting `backend` never selects a default and produces a typed `DecisionUnavailable` refusal.
68
+
69
+ | Canonical backend | Accepted aliases | Provider | Model | Usability and precondition |
70
+ |---|---|---|---|---|
71
+ | `openai_decisions` | `openai` | `openai` | `gpt-6-luna` | Usable only with `OPENAI_API_KEY`, an enforcing spendguard interpreter, and a matching estimate approval. The sole proven call is ledger `2bed00b5f31d4858`: 319 input tokens costing `$3.19e-05`; its output side is unproven. |
72
+ | `typesafe_jev` | `jev` | `typesafe` | `jev-1.14.0` | Usable only with `TYPESAFE_API_KEY`, an enforcing spendguard interpreter, and a matching estimate approval. Input costs `$0.042` per 1M tokens and output is free, as documented at `docs.typesafe.ai/models`; the wire schema is documented at `docs.typesafe.ai/api/system-one`. No live TypeSafe call has yet been proven. |
73
+ | `mock_jevlike` | none | `mock` | `deterministic` | Usable in-process and never selected implicitly. Predicate probability is exactly `0.5`; choice selects the first option with probability/confidence `1.0` and all others `0.0`; score selects the first level with score `0.0`, probability/confidence `1.0`, and all other levels `0.0`; reported input/output tokens are both `0`. |
74
+
75
+ ## Estimate, approve, then spend
76
+
77
+ The shipped configuration contains exactly three backends: metered `typesafe_jev` and `openai_decisions`, plus the in-process `mock_jevlike` test double. Their only transport kinds are `http` and `in_process`. A backend that declares the removed `subscription` transport is refused when configuration loads; subscription-lane LLM access belongs in llm-spendguard.
78
+
79
+ Choice questions may declare `"calibrated_choice_distribution": "required"` or `"calibrated_choice_distribution": "omitted"`. Omitting the declaration preserves the current `required` contract: `choice`, `probabilities` for every declared choice in order, and `confidence`. Declaring `omitted` explicitly requests only `name`, `type`, and `choice`; returning either `probabilities` or `confidence` is a named nonconformance, even if null. Unknown or malformed declarations are refused with the permitted values from the packaged `choice_distribution_contract` configuration beside `limits`. Mixed declarations in one call are supported. The shared validator enforces the declared answer fields across Python, CLI, HTTP, and MCP. Score questions are unchanged.
80
+
81
+ Self-reported confidence is **not calibrated**. In the measured eight-question probe with twenty declared choices per question, gpt-6-luna returned top probability/confidence `1.0` on every item (entropy approximately zero; effective support `1.00`), while glm-5.3 returned top probability/confidence `0.99` on every item (entropy `0.0629`; effective support `1.06`). Those degenerate vectors do not establish calibrated uncertainty. At the working shape of 32 questions × 29 choices, the measured output grew from 2,149 to 23,270 tokens. The declaration allows callers to omit that unvalidated distribution rather than pay for it. The field describes the requested distribution contract; it does not certify calibration when required.
82
+
83
+ Paid decisions fail closed. First call `decide(..., estimate_only=True)`: it counts the complete provider request, prices it through `spendguard.pricing`, spends `$0`, and persists a matching approval. Only a later identical `decide(...)` may reach the gated metered transport. Provider usage remains authoritative for the real receipt. Configure approval storage with `JEVLIKEDECISIONS_APPROVAL_DIR`, approval signing with `JEVLIKEDECISIONS_APPROVAL_INTEGRITY_KEY`, receipt storage with `JEVLIKEDECISIONS_RECEIPT_DIR`, and an alternate package configuration with `JEVLIKEDECISIONS_CONFIG`. Credentials are read only from the environment variable named by the selected backend configuration.
84
+
85
+ ## Python, CLI, HTTP, and MCP
86
+
87
+ Python:
88
+
89
+ ```python
90
+ from jevlikedecisions import decide
91
+
92
+ estimate = decide(input_parts, questions, backend=backend, intent=intent, estimate_only=True)
93
+ result = decide(input_parts, questions, backend=backend, intent=intent)
94
+ ```
95
+
96
+ CLI input comes from a JSON file or stdin. `estimate` prints the estimate and its `approval_path`; `decide` prints answers and receipt fields. The `backend` value accepts either a canonical name or an alias reported by `backends`.
97
+
98
+ ```console
99
+ jevlikedecisions backends
100
+ jevlikedecisions estimate --backend "$JEVLIKEDECISIONS_BACKEND" request.json
101
+ jevlikedecisions decide --backend "$JEVLIKEDECISIONS_BACKEND" request.json
102
+ ```
103
+
104
+ The CLI flag fills a missing request `backend`; if both the flag and JSON field are present they must agree or the command fails. CLI exit codes follow `sysexits`: `0` (`EX_OK`) for success, `64` (`EX_USAGE`) for malformed input, `65` (`EX_DATAERR`) for a typed decision refusal, and `70` (`EX_SOFTWARE`) for an internal fault. The CLI reports every failure as `TypeName: message` on stderr; catching internal faults gives automation a stable code without hiding the fault.
105
+
106
+ The standard-library HTTP server exposes `POST /backends`, `POST /estimate`, and `POST /decide`; its `backend` field accepts the same config-derived canonical names and aliases. HTTP `400` identifies a malformed request, `422` identifies a typed decision refusal, `503` specifically identifies `DecisionUnavailable`, and `500` identifies an internal fault. Keeping unavailable decisions at `503` preserves the established retry signal, while the JSON `error` field retains the exact typed refusal. Set `JEVLIKEDECISIONS_HTTP_PORT`; optionally set `JEVLIKEDECISIONS_HTTP_HOST`. With no `JEVLIKEDECISIONS_OPERATOR_TOKEN` it binds only to loopback. When that token is set, callers must send it as an operator bearer token. A paid caller must successfully call `/estimate` before `/decide` with the identical request.
107
+
108
+ ```console
109
+ jevlikedecisions-http --host "$JEVLIKEDECISIONS_HTTP_HOST" --port "$JEVLIKEDECISIONS_HTTP_PORT"
110
+ ```
111
+
112
+ The package's sole runtime dependency is `llm-spendguard`; its MCP surface is a standard-library JSON-RPC stdio server. The `list_decision_backends`, `estimate_decision`, and `make_decision` tools expose the same selection contract and send evidence whole. Enumeration is derived from configuration and reports canonical name, aliases, provider, model, usability, and every missing precondition; both decision tools accept exactly those canonical names and aliases. The approval from the estimate tool must exist before a paid call through the decision tool.
113
+
114
+ ```console
115
+ jevlikedecisions-mcp
116
+ ```
117
+
118
+ ## Paid live-proof utility
119
+
120
+ `scripts/run_paid_decision_live_proof.py` preserves the reproducible paid-call proof harness in the repository. It takes a configuration path rather than embedding one; `examples/paid_decision_live_proof.example.json` shows the input shape. By default it estimates every configured backend, prints the per-backend and total token/USD estimates, and exits without making a paid call. After reviewing that complete estimate, rerun with the explicit `--authorize-paid-calls` flag to execute every configured proof. Its records include reported-output-token status, cost completeness, and the known input-cost floor alongside the ledger and receipt evidence.
121
+
122
+ ```console
123
+ PYTHONPATH=src python scripts/run_paid_decision_live_proof.py examples/paid_decision_live_proof.example.json
124
+ PYTHONPATH=src python scripts/run_paid_decision_live_proof.py examples/paid_decision_live_proof.example.json --authorize-paid-calls
125
+ ```
126
+
127
+ `assert_all_decisions_via_jevlikedecisions(...)` cross-checks package-owned receipts against spendguard rows by `ledger_id` and fails closed when there are no auditable receipts. Spendguard's calls table has no endpoint, route, or via columns, so this proves receipt-backed package calls; it cannot prove that no outside caller contacted a Decisions endpoint.
128
+
129
+ ## Troubleshooting
130
+
131
+ A framework Python without a usable certificate bundle may need `SSL_CERT_FILE` set to the path of an installed CA bundle, such as the one supplied by certifi, before a real HTTPS call. Never put keys or secret values in configuration or examples.
132
+
133
+ `anyjev` remains a named v1 schema gap: its authoritative independent wire contract was unavailable, so no fields were invented. It can be added as another adapter without changing `decide()`.
134
+ ## General LLM generation
135
+
136
+ This package wraps the jev and OpenAI Decisions APIs only. General text generation was removed; use `llm-spendguard` for general LLM calls.