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.
- jevlikedecisions-0.1.0/.github/workflows/publish-root-python-package.yml +184 -0
- jevlikedecisions-0.1.0/.github/workflows/test-root-python-package.yml +288 -0
- jevlikedecisions-0.1.0/.gitignore +16 -0
- jevlikedecisions-0.1.0/DESIGN_PORTS_AND_ADAPTERS.md +9 -0
- jevlikedecisions-0.1.0/LICENSE +10 -0
- jevlikedecisions-0.1.0/MANIFEST.in +4 -0
- jevlikedecisions-0.1.0/PKG-INFO +136 -0
- jevlikedecisions-0.1.0/README.md +122 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_backend_aliases_and_selection_docs.md +135 -0
- 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
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_create.md +190 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_decision_audit_store_dispatch2_worktree_baseline_robust.md +122 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_decision_audit_store_records_request_reply_outcome_for_successes_and_failures.md +117 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_false_constant_name_and_unknown_backend_misdiagnosis.md +113 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_fix_provenance_and_falsifiable_route_guard.md +205 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_fix_t1_single_use_approval_gate.md +92 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_http_and_cli_surfaces_do_not_honour_the_settled_error_contract.md +121 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_live_openai_and_jev_provider_proof.md +119 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_schema_verify_and_zero_spend_estimate.md +85 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_sole_dependency_is_llm_spendguard.md +156 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_surface_completeness_audit.md +135 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_surfaces_cli_api_mcp_and_estimate_fix.md +122 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_typed_refusals_config_validation_and_live_proof_in_repo.md +222 -0
- jevlikedecisions-0.1.0/docs/handoffs/jevlikedecisions_unknown_output_tokens_must_not_become_a_ledger_zero.md +160 -0
- jevlikedecisions-0.1.0/examples/paid_decision_live_proof.example.json +15 -0
- jevlikedecisions-0.1.0/pyproject.toml +36 -0
- jevlikedecisions-0.1.0/scripts/judge_capability_documentation.py +474 -0
- jevlikedecisions-0.1.0/scripts/migrate_capability_documentation_judging_instrument.py +52 -0
- jevlikedecisions-0.1.0/scripts/run_paid_decision_live_proof.py +89 -0
- jevlikedecisions-0.1.0/setup.cfg +4 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/__init__.py +25 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/__init__.py +4 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/mock_jevlike.py +22 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/openai_decisions.py +73 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/adapters/typesafe_jev.py +54 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/capability_contracts.py +187 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/cli.py +100 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/config/capability_documentation_verdicts.json +550 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/config/jevlikedecisions.json +91 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/configuration.py +243 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/core.py +218 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/decision_audit_store.py +95 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/documentation_verdicts.py +86 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/errors.py +25 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/estimate_approvals.py +124 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/guards.py +84 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/http_surface.py +114 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/mcp_surface.py +192 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/ports.py +19 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/pricing_metering.py +105 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/receipts.py +22 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/registry.py +19 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/surface_contract.py +84 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions/validation.py +74 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/PKG-INFO +136 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/SOURCES.txt +79 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/dependency_links.txt +1 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/entry_points.txt +4 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/requires.txt +5 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/scm_file_list.json +75 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/scm_version.json +8 -0
- jevlikedecisions-0.1.0/src/jevlikedecisions.egg-info/top_level.txt +1 -0
- jevlikedecisions-0.1.0/tests/conftest.py +36 -0
- jevlikedecisions-0.1.0/tests/fixtures/subscription_glm_captured_reply.txt +1 -0
- jevlikedecisions-0.1.0/tests/fixtures/subscription_probe_questions.json +698 -0
- jevlikedecisions-0.1.0/tests/test_adapter_wire_shapes.py +27 -0
- jevlikedecisions-0.1.0/tests/test_backend_alias_contract.py +314 -0
- jevlikedecisions-0.1.0/tests/test_capability_contract_conformance.py +162 -0
- jevlikedecisions-0.1.0/tests/test_capability_documentation_judging_record.py +790 -0
- jevlikedecisions-0.1.0/tests/test_decision_audit_store.py +109 -0
- jevlikedecisions-0.1.0/tests/test_g1_g2_guards.py +20 -0
- jevlikedecisions-0.1.0/tests/test_hard_requirements.py +50 -0
- jevlikedecisions-0.1.0/tests/test_live_backend_offline_contracts.py +222 -0
- jevlikedecisions-0.1.0/tests/test_metered_and_naming.py +53 -0
- jevlikedecisions-0.1.0/tests/test_non_persisting_estimate.py +227 -0
- jevlikedecisions-0.1.0/tests/test_o1_o2_conformance.py +23 -0
- jevlikedecisions-0.1.0/tests/test_provenance_selection_and_audit.py +172 -0
- jevlikedecisions-0.1.0/tests/test_single_use_approval_gate.py +196 -0
- jevlikedecisions-0.1.0/tests/test_stdlib_mcp_and_dependency_boundary.py +295 -0
- jevlikedecisions-0.1.0/tests/test_surfaces_and_whole_request_estimate.py +331 -0
- 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,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,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.
|