llm-ports 0.12.1.dev14__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 (99) hide show
  1. llm_ports-0.12.1.dev14/.gitattributes +1 -0
  2. llm_ports-0.12.1.dev14/.github/workflows/ci.yml +152 -0
  3. llm_ports-0.12.1.dev14/.github/workflows/release.yml +110 -0
  4. llm_ports-0.12.1.dev14/.github/zizmor.yml +9 -0
  5. llm_ports-0.12.1.dev14/.gitignore +9 -0
  6. llm_ports-0.12.1.dev14/.pre-commit-config.yaml +13 -0
  7. llm_ports-0.12.1.dev14/CHANGELOG.md +237 -0
  8. llm_ports-0.12.1.dev14/CONTRIBUTING.md +52 -0
  9. llm_ports-0.12.1.dev14/LICENSE +21 -0
  10. llm_ports-0.12.1.dev14/PKG-INFO +170 -0
  11. llm_ports-0.12.1.dev14/README.md +126 -0
  12. llm_ports-0.12.1.dev14/ci/jscpd-baseline.json +4 -0
  13. llm_ports-0.12.1.dev14/docs/REDESIGN.md +337 -0
  14. llm_ports-0.12.1.dev14/docs/adr/0001-named-outcomes-never-raise.md +71 -0
  15. llm_ports-0.12.1.dev14/docs/adr/0002-grounded-prose-by-construction.md +59 -0
  16. llm_ports-0.12.1.dev14/docs/adr/0003-the-number-law-compares-by-value.md +61 -0
  17. llm_ports-0.12.1.dev14/docs/adr/0004-digit-free-context.md +49 -0
  18. llm_ports-0.12.1.dev14/docs/adr/0005-character-whitelist-grammar.md +47 -0
  19. llm_ports-0.12.1.dev14/docs/adr/0006-data-rights-in-prov-o-terms.md +48 -0
  20. llm_ports-0.12.1.dev14/docs/adr/0007-one-observation-lifecycle.md +52 -0
  21. llm_ports-0.12.1.dev14/docs/adr/0008-prompt-packs-named-by-content-sha.md +53 -0
  22. llm_ports-0.12.1.dev14/docs/adr/README.md +20 -0
  23. llm_ports-0.12.1.dev14/llm_ports/__init__.py +88 -0
  24. llm_ports-0.12.1.dev14/llm_ports/call.py +301 -0
  25. llm_ports-0.12.1.dev14/llm_ports/call_graders.py +297 -0
  26. llm_ports-0.12.1.dev14/llm_ports/chunking.py +94 -0
  27. llm_ports-0.12.1.dev14/llm_ports/claim_refute.py +200 -0
  28. llm_ports-0.12.1.dev14/llm_ports/content.py +252 -0
  29. llm_ports-0.12.1.dev14/llm_ports/context.py +77 -0
  30. llm_ports-0.12.1.dev14/llm_ports/failure.py +90 -0
  31. llm_ports-0.12.1.dev14/llm_ports/grounder_http.py +98 -0
  32. llm_ports-0.12.1.dev14/llm_ports/grounding.py +94 -0
  33. llm_ports-0.12.1.dev14/llm_ports/index_order.py +74 -0
  34. llm_ports-0.12.1.dev14/llm_ports/json_schema.py +99 -0
  35. llm_ports-0.12.1.dev14/llm_ports/judge_sampling.py +231 -0
  36. llm_ports-0.12.1.dev14/llm_ports/langfuse_prompt_source.py +172 -0
  37. llm_ports-0.12.1.dev14/llm_ports/langfuse_tracer.py +223 -0
  38. llm_ports-0.12.1.dev14/llm_ports/lettucedetect_grounder.py +100 -0
  39. llm_ports-0.12.1.dev14/llm_ports/manifest.py +115 -0
  40. llm_ports-0.12.1.dev14/llm_ports/numbers.py +180 -0
  41. llm_ports-0.12.1.dev14/llm_ports/observation.py +198 -0
  42. llm_ports-0.12.1.dev14/llm_ports/otel_gen_ai.py +181 -0
  43. llm_ports-0.12.1.dev14/llm_ports/packs.py +218 -0
  44. llm_ports-0.12.1.dev14/llm_ports/ports.py +671 -0
  45. llm_ports-0.12.1.dev14/llm_ports/py.typed +0 -0
  46. llm_ports-0.12.1.dev14/llm_ports/pydantic_ai_embedder.py +209 -0
  47. llm_ports-0.12.1.dev14/llm_ports/pydantic_ai_generator.py +383 -0
  48. llm_ports-0.12.1.dev14/llm_ports/pydantic_ai_replay.py +121 -0
  49. llm_ports-0.12.1.dev14/llm_ports/pydantic_evals_judge.py +205 -0
  50. llm_ports-0.12.1.dev14/llm_ports/reference.py +165 -0
  51. llm_ports-0.12.1.dev14/llm_ports/reranker_http.py +139 -0
  52. llm_ports-0.12.1.dev14/llm_ports/retrieval_metrics.py +66 -0
  53. llm_ports-0.12.1.dev14/llm_ports/roles.py +179 -0
  54. llm_ports-0.12.1.dev14/llm_ports/slots.py +168 -0
  55. llm_ports-0.12.1.dev14/llm_ports/sql_guard.py +214 -0
  56. llm_ports-0.12.1.dev14/llm_ports/testing.py +316 -0
  57. llm_ports-0.12.1.dev14/llm_ports/testing_langfuse.py +167 -0
  58. llm_ports-0.12.1.dev14/pyproject.toml +111 -0
  59. llm_ports-0.12.1.dev14/tests/__init__.py +0 -0
  60. llm_ports-0.12.1.dev14/tests/conftest.py +15 -0
  61. llm_ports-0.12.1.dev14/tests/golden/rubric_v0_11.json +18 -0
  62. llm_ports-0.12.1.dev14/tests/llm_tiers.toml +24 -0
  63. llm_ports-0.12.1.dev14/tests/packs/demo.multi/pack.yaml +6 -0
  64. llm_ports-0.12.1.dev14/tests/packs/demo.multi/prompt.j2 +1 -0
  65. llm_ports-0.12.1.dev14/tests/packs/demo.multi/second.j2 +1 -0
  66. llm_ports-0.12.1.dev14/tests/packs/demo.one/pack.yaml +5 -0
  67. llm_ports-0.12.1.dev14/tests/packs/demo.one/prompt.j2 +2 -0
  68. llm_ports-0.12.1.dev14/tests/packs/demo.refute/pack.yaml +5 -0
  69. llm_ports-0.12.1.dev14/tests/packs/demo.refute/prompt.j2 +5 -0
  70. llm_ports-0.12.1.dev14/tests/test_call.py +274 -0
  71. llm_ports-0.12.1.dev14/tests/test_call_graders.py +162 -0
  72. llm_ports-0.12.1.dev14/tests/test_chunking.py +116 -0
  73. llm_ports-0.12.1.dev14/tests/test_claim_refute.py +97 -0
  74. llm_ports-0.12.1.dev14/tests/test_content.py +94 -0
  75. llm_ports-0.12.1.dev14/tests/test_failure.py +53 -0
  76. llm_ports-0.12.1.dev14/tests/test_grounder_http.py +86 -0
  77. llm_ports-0.12.1.dev14/tests/test_grounding.py +54 -0
  78. llm_ports-0.12.1.dev14/tests/test_json_schema.py +102 -0
  79. llm_ports-0.12.1.dev14/tests/test_judge_sampling.py +152 -0
  80. llm_ports-0.12.1.dev14/tests/test_langfuse_prompt_source.py +218 -0
  81. llm_ports-0.12.1.dev14/tests/test_langfuse_tracer.py +197 -0
  82. llm_ports-0.12.1.dev14/tests/test_lettucedetect_grounder.py +96 -0
  83. llm_ports-0.12.1.dev14/tests/test_numbers.py +53 -0
  84. llm_ports-0.12.1.dev14/tests/test_observation.py +113 -0
  85. llm_ports-0.12.1.dev14/tests/test_otel_gen_ai.py +155 -0
  86. llm_ports-0.12.1.dev14/tests/test_packs.py +117 -0
  87. llm_ports-0.12.1.dev14/tests/test_ports_contract.py +44 -0
  88. llm_ports-0.12.1.dev14/tests/test_pydantic_ai_embedder.py +179 -0
  89. llm_ports-0.12.1.dev14/tests/test_pydantic_ai_generator.py +243 -0
  90. llm_ports-0.12.1.dev14/tests/test_pydantic_ai_replay.py +78 -0
  91. llm_ports-0.12.1.dev14/tests/test_pydantic_evals_judge.py +204 -0
  92. llm_ports-0.12.1.dev14/tests/test_reference.py +160 -0
  93. llm_ports-0.12.1.dev14/tests/test_reranker_http.py +149 -0
  94. llm_ports-0.12.1.dev14/tests/test_retrieval_metrics.py +57 -0
  95. llm_ports-0.12.1.dev14/tests/test_roles.py +81 -0
  96. llm_ports-0.12.1.dev14/tests/test_rubric_golden.py +51 -0
  97. llm_ports-0.12.1.dev14/tests/test_slots.py +89 -0
  98. llm_ports-0.12.1.dev14/tests/test_sql_guard.py +138 -0
  99. llm_ports-0.12.1.dev14/uv.lock +2777 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,152 @@
1
+ # llm-ports' own gate. Every job starts from `permissions: {}` and grants only what it needs.
2
+ #
3
+ # lint ruff check + ruff format --check, pre-commit (all files), actionlint + zizmor over
4
+ # the workflows
5
+ # pytest the locked suite on 3.12 / 3.13 / 3.14 / 3.15 (ubuntu) and 3.13 (windows), with a
6
+ # coverage floor (pyproject `[tool.coverage.report] fail_under`, raised only); one leg
7
+ # uploads to Codecov over OIDC.
8
+ # test-lowest every direct dependency at its declared lower bound (`--resolution lowest-direct`)
9
+ # build sdist + wheel, `twine check --strict`
10
+ # test the one required check: green only when every job above is
11
+ # gitleaks / dup-code ci-kit's reusable workflows, called the way every consumer calls them
12
+ #
13
+ # release.yml calls this whole workflow (`workflow_call`) before it builds anything to publish.
14
+ name: ci
15
+
16
+ on:
17
+ push:
18
+ branches: [main]
19
+ pull_request:
20
+ workflow_dispatch:
21
+ workflow_call:
22
+
23
+ permissions: {}
24
+
25
+ concurrency:
26
+ group: ${{ github.workflow }}-${{ github.ref }}
27
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
28
+
29
+ jobs:
30
+ lint:
31
+ name: lint
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 10
34
+ permissions:
35
+ contents: read
36
+ steps:
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+ with:
39
+ persist-credentials: false
40
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
41
+ - run: uv sync --frozen --group lint
42
+ - name: ruff check
43
+ run: uv run --no-sync ruff check --output-format=github .
44
+ - name: ruff format
45
+ run: uv run --no-sync ruff format --check .
46
+ - name: pre-commit
47
+ run: uv run --no-sync pre-commit run --all-files --show-diff-on-failure
48
+ - name: actionlint
49
+ run: uv run --no-sync actionlint
50
+ - name: zizmor
51
+ env:
52
+ GH_TOKEN: ${{ github.token }} # online audits (impostor commits, known-vulnerable actions)
53
+ run: uv run --no-sync zizmor --format=github .
54
+
55
+ pytest:
56
+ name: pytest (${{ matrix.os }}, ${{ matrix.python }})
57
+ runs-on: ${{ matrix.os }}
58
+ timeout-minutes: 15
59
+ permissions:
60
+ contents: read
61
+ id-token: write # Codecov OIDC upload, no stored token
62
+ strategy:
63
+ fail-fast: false
64
+ matrix:
65
+ os: [ubuntu-latest]
66
+ python: ["3.12", "3.13", "3.14", "3.15"]
67
+ include:
68
+ - { os: windows-latest, python: "3.13" }
69
+ steps:
70
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
71
+ with:
72
+ persist-credentials: false
73
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
74
+ with:
75
+ python-version: ${{ matrix.python }}
76
+ - run: uv sync --frozen
77
+ - name: pytest
78
+ run: uv run --no-sync pytest -q --cov --cov-report=term --cov-report=xml
79
+ - name: codecov
80
+ if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13'
81
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
82
+ with:
83
+ use_oidc: true
84
+ files: coverage.xml
85
+ fail_ci_if_error: true
86
+
87
+ test-lowest:
88
+ name: test-lowest (lowest-direct, 3.12)
89
+ runs-on: ubuntu-latest
90
+ timeout-minutes: 15
91
+ permissions:
92
+ contents: read
93
+ steps:
94
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
95
+ with:
96
+ persist-credentials: false
97
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
98
+ with:
99
+ python-version: "3.12"
100
+ # Every extra CI can install (lettucedetect pulls torch; its tests run over a stub detector).
101
+ - run: uv venv
102
+ - run: uv pip install --resolution lowest-direct -e ".[evals,langfuse,otel,sql]" --group dev
103
+ - run: uv pip list
104
+ - name: pytest
105
+ run: .venv/bin/python -m pytest -q
106
+
107
+ build:
108
+ name: build
109
+ runs-on: ubuntu-latest
110
+ timeout-minutes: 10
111
+ permissions:
112
+ contents: read
113
+ steps:
114
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
115
+ with:
116
+ persist-credentials: false
117
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
118
+ - run: uv build
119
+ - name: twine check
120
+ run: uv run --frozen --only-group lint twine check --strict dist/*
121
+
122
+ # The required status check is named `test`; it aggregates every job above so a new leg is
123
+ # covered without editing branch protection.
124
+ test:
125
+ name: test
126
+ if: always()
127
+ needs: [lint, pytest, test-lowest, build]
128
+ runs-on: ubuntu-latest
129
+ timeout-minutes: 5
130
+ permissions: {}
131
+ steps:
132
+ - uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
133
+ with:
134
+ jobs: ${{ toJSON(needs) }}
135
+
136
+ gitleaks:
137
+ name: gitleaks (secret scan)
138
+ permissions:
139
+ contents: read
140
+ pull-requests: read
141
+ uses: mjdevaccount/ci-kit/.github/workflows/gitleaks.yml@v1
142
+ with:
143
+ mode: history
144
+
145
+ # The same code written twice fails the PR: ci-kit's `ci-dup-code` (jscpd behind a shrink-only
146
+ # fingerprint baseline, ci/jscpd-baseline.json). A NEW clone >= 70 tokens / 7 lines between two
147
+ # tracked files fails; a removed one may re-freeze smaller (`--write-baseline` refuses to grow).
148
+ # Reports as "dup-code / lint".
149
+ dup-code:
150
+ permissions:
151
+ contents: read
152
+ uses: mjdevaccount/ci-kit/.github/workflows/dup-code.yml@v1
@@ -0,0 +1,110 @@
1
+ # Publishing. Two triggers, one pipeline, one index:
2
+ #
3
+ # push of a `v*` tag ci (the whole gate, reused) -> build X.Y.Z -> PyPI
4
+ # -> GitHub release with dist/* attached
5
+ # workflow_dispatch ci -> build a PEP 440 dev release -> PyPI
6
+ #
7
+ # The dispatch build is `X.Y.Z.devN` (N = this workflow's run number): pyproject's version when it is
8
+ # already a dev version, else the next patch (0.12.0 -> 0.12.1.devN). Installers skip pre-releases
9
+ # unless asked (`pip install --pre`), so a dev upload is the end-to-end dry run of a real release.
10
+ #
11
+ # Uploads use PyPI trusted publishing: GitHub's OIDC token is exchanged for a short-lived upload
12
+ # token, so no secret is stored. PyPI's publisher names this repo, this file and the `pypi`
13
+ # environment; renaming any of the three breaks publishing.
14
+ #
15
+ # The GitHub release does NOT depend on the PyPI upload: consumers pin the wheel URL
16
+ # llm-ports @ https://github.com/mjdevaccount/llm-ports/releases/download/vX.Y.Z/llm_ports-X.Y.Z-py3-none-any.whl
17
+ # and that asset must exist for every tag whatever the index did. A tag that does not match
18
+ # pyproject's version publishes nothing.
19
+ name: release
20
+
21
+ on:
22
+ push:
23
+ tags: ["v*"]
24
+ workflow_dispatch:
25
+
26
+ permissions: {}
27
+
28
+ jobs:
29
+ ci:
30
+ name: ci
31
+ permissions:
32
+ contents: read
33
+ id-token: write # ci.yml's Codecov upload
34
+ pull-requests: read # ci-kit gitleaks
35
+ # A job-level reusable workflow resolves from this commit, never the runner's filesystem; the
36
+ # `$/` self-repository form is not yet parsed by actionlint 1.7.12.
37
+ uses: ./.github/workflows/ci.yml # zizmor: ignore[self-repository]
38
+
39
+ build:
40
+ name: build
41
+ needs: ci
42
+ runs-on: ubuntu-latest
43
+ timeout-minutes: 10
44
+ permissions:
45
+ contents: read
46
+ steps:
47
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
48
+ with:
49
+ persist-credentials: false
50
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
51
+ with:
52
+ enable-cache: false # a release builds from a clean resolve, never a shared cache
53
+ - name: tag matches pyproject version
54
+ if: github.event_name == 'push'
55
+ run: |
56
+ v=$(uv version --short)
57
+ [ "v$v" = "$GITHUB_REF_NAME" ] || { echo "::error::tag $GITHUB_REF_NAME != pyproject v$v"; exit 1; }
58
+ - name: dev version
59
+ if: github.event_name == 'workflow_dispatch'
60
+ run: |
61
+ case "$(uv version --short)" in
62
+ *.dev*) uv version --frozen --bump "dev=$GITHUB_RUN_NUMBER" ;;
63
+ *) uv version --frozen --bump patch --bump "dev=$GITHUB_RUN_NUMBER" ;;
64
+ esac
65
+ - run: uv build --no-sources
66
+ - name: twine check
67
+ run: uv run --frozen --only-group lint twine check --strict dist/*
68
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+ if-no-files-found: error
73
+
74
+ pypi:
75
+ name: publish (PyPI)
76
+ needs: build
77
+ runs-on: ubuntu-latest
78
+ timeout-minutes: 10
79
+ environment:
80
+ name: pypi
81
+ url: https://pypi.org/project/llm-ports/
82
+ permissions:
83
+ id-token: write # trusted publishing; the action also uploads PEP 740 attestations
84
+ steps:
85
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
86
+ with:
87
+ name: dist
88
+ path: dist/
89
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
90
+
91
+ github-release:
92
+ name: GitHub release
93
+ if: github.event_name == 'push'
94
+ needs: build # never on `pypi`: the wheel URL consumers pin must exist whatever the index did
95
+ runs-on: ubuntu-latest
96
+ timeout-minutes: 10
97
+ permissions:
98
+ contents: write # create the release and upload its assets
99
+ steps:
100
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
101
+ with:
102
+ name: dist
103
+ path: dist/
104
+ - name: create release
105
+ env:
106
+ GH_TOKEN: ${{ github.token }}
107
+ run: |
108
+ # no --clobber fallback: a published wheel's bytes are what consumers locked a hash of
109
+ gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --verify-tag \
110
+ --title "$GITHUB_REF_NAME" --generate-notes
@@ -0,0 +1,9 @@
1
+ # zizmor (run in ci.yml's lint job). Third-party actions are pinned by commit SHA; ci-kit's
2
+ # reusable workflows are this owner's own and ship fixes by moving their `v1` tag, so a ref pin is
3
+ # the contract there.
4
+ rules:
5
+ unpinned-uses:
6
+ config:
7
+ policies:
8
+ "mjdevaccount/*": ref-pin
9
+ "*": hash-pin
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ .coverage
8
+ .coverage.*
9
+ coverage.xml
@@ -0,0 +1,13 @@
1
+ # Run in CI (ci.yml, lint job) and locally after `uv run pre-commit install`.
2
+ repos:
3
+ - repo: local
4
+ hooks:
5
+ # Docstrings state the contract; deployment and incident detail lives in docs/adr and
6
+ # CHANGELOG.md (CONTRIBUTING.md). The lookahead admits one string: `GenRequest.label`'s
7
+ # default value is public behaviour until 1.0 renames it.
8
+ - id: no-deployment-vocabulary
9
+ name: no deployment vocabulary in the package
10
+ language: pygrep
11
+ entry: 'EDGE|edge-host|\bbox[_-](?!chat")|Tradier|trade[-_.]data|narrator|\b\d+B\b|20\d\d-\d\d-\d\d'
12
+ files: ^llm_ports/
13
+ types: [python]
@@ -0,0 +1,237 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Why a design is the way it is lives in
6
+ [`docs/adr/`](docs/adr/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - Releases publish to PyPI through trusted publishing (no stored token), with PEP 740
13
+ attestations; a tag still attaches the wheel and sdist to a GitHub release, independently of the
14
+ upload. A manual run of `release.yml` uploads a `.devN` pre-release as the dry run.
15
+ - Package metadata: project URLs, classifiers and keywords.
16
+ - CI: `actionlint` over the workflows; `ci.yml` is callable (`workflow_call`), and a release runs
17
+ all of it before building.
18
+
19
+ ## [0.12.0] - 2026-11-03
20
+
21
+ No behaviour change and no public name change; the release carries documentation, tooling and
22
+ dependency floors.
23
+
24
+ ### Added
25
+
26
+ - `numbers.law_text(mode, *, web)`: the number law's one English statement, beside the check it
27
+ states (`numbers.loose`). `judge_sampling.rubric` composes it; its output is byte-identical to
28
+ 0.11.0 for every grounding mode and web flag, pinned by `tests/test_rubric_golden.py`.
29
+ - Architecture decision records `docs/adr/0001`–`0008` (MADR), holding the rationale and the
30
+ measured evidence that used to live in docstrings.
31
+ - `CONTRIBUTING.md`: the docstring rules, where history goes, how to release.
32
+ - A `pre-commit` hook that rejects deployment vocabulary in `llm_ports/`, run in CI.
33
+ - CI: pytest on Python 3.12, 3.13, 3.14 and 3.15 (Ubuntu) and 3.13 (Windows); the suite at every
34
+ dependency's declared lower bound (`--resolution lowest-direct`); a branch-coverage floor with
35
+ Codecov upload over OIDC; `ruff format --check`; `twine check --strict` on the build; `zizmor`
36
+ over the workflows; every workflow defaults to `permissions: {}`.
37
+
38
+ ### Changed
39
+
40
+ - Every docstring states the contract in Google style, with invariants in an `Invariants:`
41
+ section; ruff's `D` rules (Google convention) enforce the layout.
42
+ - The tree is formatted with `ruff format`.
43
+ - Dependency floors match what is tested: `pydantic>=2.12` (already required by
44
+ `pydantic-ai-slim>=2.57`) and, for the `sql` extra, `sqlglot>=30.22`. Older sqlglot drops
45
+ `LIMIT ALL` at parse time, so `sql_guard.guard` capped the query instead of refusing it.
46
+
47
+ ### Fixed
48
+
49
+ - `content.print_gate` and `DataClass.allows` document the `ValueError` they raise for an audience
50
+ outside `AUDIENCES`; the old text said `print_gate` never raised.
51
+
52
+ ## [0.11.0] - 2026-11-03
53
+
54
+ ### Added
55
+
56
+ - `ports.Reranker`, the tenth port, and `Reranked` (scores in input order, order-only; never
57
+ raises for a model, wire or gate failure). Also exported from the package root.
58
+ - `reranker_http`: `HttpReranker` over the Cohere/Jina-shaped `/rerank` wire, `payload`,
59
+ `scores_from`. Every document is scored exactly once (indices exactly `0..n-1`) and every score
60
+ is finite, or the result is `invalid`.
61
+ - `observation`: `observe(tracer, ...)` opens one generation around a block and ends it once
62
+ (`ERROR` and `latency_ms` on an exception); `PortObservation` (was `call._PortObservation`) with
63
+ `usage_counts()` and `fail(exc, metadata)`; `opt_int`. A call that reported neither usage count
64
+ stamps `tokens_unmeasured: true`. `call.tracer_trace` is built on `observe`.
65
+ - `langfuse_tracer` (extra `langfuse`): `LangfuseTracer(client, ..., on_event=)`,
66
+ `LangfuseGeneration`, `usage_details`, `trace_id`.
67
+ - `langfuse_prompt_source`: `LangfusePromptSource(client, seed_for, ..., on_event=)`; the repo
68
+ leads and the label mirrors. Duck-typed client, no SDK import.
69
+ - `otel_gen_ai` (extra `otel`): `gen_ai_span`, `record_gen_ai` (back-dated), `GenAiRecorder`;
70
+ attribute names from `opentelemetry-semantic-conventions`, emitting `gen_ai.provider.name`
71
+ beside the deprecated `gen_ai.system`.
72
+ - `sql_guard` (extra `sql`): `guard(sql, policy)`, `SqlGuardPolicy` (with `dialect`, default
73
+ postgres), `SqlGuardError`, `FORBIDDEN_NODES`, `POSTGRES_DENY_FUNCTIONS`.
74
+ - `testing.StubReranker`; `testing_langfuse` (extra `langfuse`): `LangfuseRig`, `observed`,
75
+ `FakeLangfusePrompts`, `RefusingLangfuse`.
76
+
77
+ ### Changed
78
+
79
+ - `index_order.IndexOrderedTransport` moved out of `pydantic_ai_embedder` (still importable
80
+ there, the same object) and also orders `/rerank` replies' `results`.
81
+
82
+ ## [0.10.0] - 2026-11-03
83
+
84
+ ### Added
85
+
86
+ - `pydantic_evals_judge` (extra `evals`): `PydanticEvalsJudge(role, model=|model_for=)`,
87
+ `JUDGE_PROFILE`, `clean_reason`, `verdict_of`, `judge_inputs`.
88
+ - `judge_sampling`: `Seen`, `seen_for`, `schema_of`, `judge_input`, `reply_text`, `sample_key`,
89
+ `selected`, `rubric(role, seen, *, platform, mode, today, tz_label)`, `verdict_label`.
90
+ - `lettucedetect_grounder` (extra `lettucedetect`): `LettuceDetectGrounder`, `load_detector`,
91
+ `Detector`, `MODEL`, `MAX_CONTEXT_CHARS`, `WINDOW_REASON`.
92
+ - `grounder_http`: `HttpGrounder`, `GrounderUnavailable`, `payload`.
93
+ - `claim_refute`: `refute_slots`, `refute_prompt(pack, ...)`, `refute(generate, role, *, pack, ...)`,
94
+ `RefuteClaim`, `GenerateFn`.
95
+ - `json_schema`: `research_schema`, `RESEARCH_ANALYSIS`, `ANALYSIS_KEY`, `without_analysis`,
96
+ `research(generate, role, ...)`.
97
+ - `testing.StubDetector`.
98
+
99
+ ## [0.9.0] - 2026-11-03
100
+
101
+ ### Added
102
+
103
+ - `chunking`: `chunk_text`, `break_floor`, `max_chars_for_chunks`, `content_hash`,
104
+ `should_ingest`, `CHUNK_CHARS`, `CHUNK_OVERLAP`.
105
+ - `retrieval_metrics`: `first_rank`, `hit_at_k`, `reciprocal_rank`, `agree_at_k`, `mean`, `clears`.
106
+ - `reference`: `GauntletEvaluator`, `NumberLawGrounder`, `RepoPromptSource`, `NoopTracer`,
107
+ `NoopGeneration`.
108
+ - `pydantic_ai_replay`: `Recorder`, `replay`, `Recording`, `key`, `NotRecorded`.
109
+ - `testing.StubCiter`, `testing.StubPromptSource`; `ports.Seed` (also exported from the root).
110
+ - `call_graders.TEXT_ONLY_GRADERS`, `graders_for_inputs`.
111
+
112
+ ### Changed
113
+
114
+ - `call.generate` drops `TEXT_ONLY_GRADERS` for a call with `images=` or the new `web=True`, so an
115
+ image or a web tier no longer fails `numbers_from_inputs` by construction. With no `trace` hook
116
+ the call observes into `tracer_trace(NoopTracer())`, the package's one null sink.
117
+
118
+ ## [0.8.1] - 2026-11-03
119
+
120
+ ### Fixed
121
+
122
+ - `IndexOrderedTransport` re-sorts an `/embeddings` reply's `data` by `index`. pydantic-ai drops
123
+ `index`, so a server answering out of order put every vector on its neighbour's row, which the
124
+ count check cannot see. `openai_compatible_embeddings` always wraps its transport with it.
125
+
126
+ ## [0.8.0] - 2026-11-03
127
+
128
+ ### Added
129
+
130
+ - `ports.Embedder`, the ninth port, and `Embedded(vectors, outcome, stamp, reason, input_tokens)`:
131
+ one vector per text in one named space; `input_tokens=None` when the wire reported no usage.
132
+ - `pydantic_ai_embedder.PydanticAIEmbedder` on pydantic-ai's `OpenAIEmbeddingModel`: count and
133
+ dimension checks (a short batch or a wrong dimension is `invalid`, never a partial result),
134
+ batching, an asymmetric query prefix, and the generator's never-raise outcome classification.
135
+ - `openai_compatible_embeddings(base_url, model_name, key, transport=)`.
136
+ - `testing.StubEmbedder`: deterministic hashed unit vectors.
137
+
138
+ ## [0.7.0] - 2026-11-03
139
+
140
+ ### Added
141
+
142
+ - `content`: `ContentItem` (`prov:Entity`), `Activity` (`prov:Activity`), `DataClass` (rights
143
+ `public < owned < licensed < scraped`, `private`, `link_required`; `join` takes the most
144
+ restrictive), `CEILING` per audience, and `print_gate(item, audience) -> GateVerdict`: ok, or one
145
+ named refusal (`rights`, `unclaimed`, `uncited`, `unsourced`, `link`).
146
+
147
+ ## [0.6.0] - 2026-10-31
148
+
149
+ ### Added
150
+
151
+ - `slots.to_prose(text)`: context a digit-free grammar can read with no figure to reach for. Every
152
+ digit run is removed whole (`numbers.DIGITS`), every character outside `PROSE_CHARS` becomes a
153
+ space.
154
+
155
+ ### Fixed
156
+
157
+ - `__version__` matches the package version (it said 0.3.0).
158
+
159
+ ## [0.5.0] - 2026-10-31
160
+
161
+ ### Added
162
+
163
+ - `call_graders.nullable_paths(model)`: the nullable leaf paths of a pydantic model.
164
+
165
+ ### Changed
166
+
167
+ - `call_graders.schema_complete` reads a None on a nullable pydantic field as an answer; a None on
168
+ a required field still fails, and a dict value is graded as before.
169
+
170
+ ## [0.4.0] - 2026-10-29
171
+
172
+ ### Added
173
+
174
+ - `numbers.loose(text, allowed, words_ok)`: the number law in one function. Digit runs by value
175
+ not in `allowed`, then spelled numbers and number-word tails not in `words_ok`, in order and
176
+ named.
177
+ - `grounding.loose`, `slots.loose`; `grounding.belt` and `slots.belt` count `loose`.
178
+
179
+ ### Changed
180
+
181
+ - `numbers.uncited` is `loose` sorted into the cited-spans shape (same results on every pinned case).
182
+ - `call_graders.numbers_from_inputs` scores the distinct figures of `loose`, so spelled numbers
183
+ count; the prompt's own words and a `%` license them.
184
+
185
+ ## [0.3.0] - 2026-10-28
186
+
187
+ ### Added
188
+
189
+ - `slots`: grounded prose by construction. `PROSE_CHARS`, `prose_pattern`, `slot_pattern` (the
190
+ digit-free grammar), `SLOT`, `ID`, `cited`, `fill`, `problems` (unknown slot, stray brace, cited
191
+ twice), `belt` over filled text.
192
+ - `numbers.canon` and `numbers.digits`: digit runs compare by value ("2.20" == "2.2").
193
+
194
+ ### Changed
195
+
196
+ - `grounding.belt` compares digit runs by value.
197
+
198
+ ## [0.2.0] - 2026-10-23
199
+
200
+ ### Added
201
+
202
+ - `grounding`: `Problem(path, reason, detail="")`, `describe`, `retry_message`,
203
+ `belt(text, allowed, words_ok, *, strip_links)`, `allowed_numbers(texts)`.
204
+ - `failure_reason(exc, *, with_cause=False)`: an optional ` <- <cause reason>` tail, cut by the
205
+ same body-safe rule and within `REASON_MAX`.
206
+
207
+ ## [0.1.1] - 2026-10-22
208
+
209
+ ### Added
210
+
211
+ - `py.typed` (PEP 561).
212
+ - A release workflow: a `v*` tag builds the wheel and sdist from the tagged commit, refuses a
213
+ tag that does not match the package version, and attaches both to a GitHub release.
214
+
215
+ ## [0.1.0] - 2026-10-22
216
+
217
+ ### Added
218
+
219
+ - The ports (Protocols and frozen value types), roles over a consumer's tier table, prompt packs
220
+ named by content sha, per-call graders, `generate()` with named outcomes and `Hooks`, the
221
+ pydantic-ai `Generator` adapter, and test doubles.
222
+
223
+ [Unreleased]: https://github.com/mjdevaccount/llm-ports/compare/v0.12.0...HEAD
224
+ [0.12.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.11.0...v0.12.0
225
+ [0.11.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.10.0...v0.11.0
226
+ [0.10.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.9.0...v0.10.0
227
+ [0.9.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.8.1...v0.9.0
228
+ [0.8.1]: https://github.com/mjdevaccount/llm-ports/compare/v0.8.0...v0.8.1
229
+ [0.8.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.7.0...v0.8.0
230
+ [0.7.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.6.0...v0.7.0
231
+ [0.6.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.5.0...v0.6.0
232
+ [0.5.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.4.0...v0.5.0
233
+ [0.4.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.3.0...v0.4.0
234
+ [0.3.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.2.0...v0.3.0
235
+ [0.2.0]: https://github.com/mjdevaccount/llm-ports/compare/v0.1.1...v0.2.0
236
+ [0.1.1]: https://github.com/mjdevaccount/llm-ports/compare/v0.1.0...v0.1.1
237
+ [0.1.0]: https://github.com/mjdevaccount/llm-ports/releases/tag/v0.1.0
@@ -0,0 +1,52 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```
6
+ uv sync # the locked dev environment
7
+ uv run pre-commit install # the docstring-vocabulary hook, on every commit
8
+ uv run pytest -q --cov # the suite, with the coverage floor
9
+ uv run ruff check . && uv run ruff format --check .
10
+ ```
11
+
12
+ CI (`.github/workflows/ci.yml`) runs the same commands on Python 3.12–3.14 and Windows, plus the
13
+ suite at every dependency's declared lower bound, `twine check --strict` on the build, and
14
+ `zizmor` over the workflows.
15
+
16
+ ## Docstrings
17
+
18
+ A docstring states the contract. Its reader is someone calling the function, not someone
19
+ auditing why it was written.
20
+
21
+ 1. **Contract only.** What it does, `Args:`, `Returns:`, `Raises:` (or that it never raises), in
22
+ the present tense, Google style. Rules that always hold go in an `Invariants:` section.
23
+ 2. **No history and no deployment detail.** No incident dates, replay counts, host, service or
24
+ consumer names, model sizes, or ticket ids. A *compatibility fact* stays ("llama.cpp's grammar
25
+ compiler ignores `maxLength` beside `pattern`"). The *reason* for a design choice moves to an
26
+ ADR, and the docstring keeps a one-line pointer (`Rationale: ADR-0002.`).
27
+ 3. **Emphasis by structure, not capitals.** An invariant goes in `Invariants:`; a warning goes in
28
+ its own sentence. Capitals are for constants and acronyms.
29
+ 4. **Neutral examples.** Orders, documents, tickets, invoices.
30
+
31
+ `ruff` enforces the Google layout (`D` rules). A `pre-commit` hook (`.pre-commit-config.yaml`)
32
+ rejects deployment vocabulary in `llm_ports/`.
33
+
34
+ ## Where history goes
35
+
36
+ - **What changed**: `CHANGELOG.md` ([Keep a Changelog](https://keepachangelog.com/en/1.1.0/)).
37
+ - **Why, with the evidence**: `docs/adr/NNNN-title.md` ([MADR](https://adr.github.io/madr/)).
38
+ Evidence is stated without naming the system it came from ("on a production summarizer, 40 of
39
+ 64 replies invented a figure with the instruction in place").
40
+
41
+ ## Releasing
42
+
43
+ Bump `version` in `pyproject.toml`, move `Unreleased` in `CHANGELOG.md` under the new version, and
44
+ merge. Then push a `vX.Y.Z` tag on `main`. `release.yml` runs the whole of `ci.yml`, checks the tag
45
+ against the version, builds, runs `twine check --strict`, then, independently of each other,
46
+ uploads to PyPI (trusted publishing through the `pypi` environment, with PEP 740 attestations) and
47
+ attaches the wheel and sdist to a GitHub release. The release assets never wait on the upload:
48
+ consumers that pin the wheel URL get it whatever PyPI did. A published file is never replaced.
49
+
50
+ To dry-run the pipeline, run `release.yml` from `main` by hand (`gh workflow run release.yml`). It
51
+ uploads a dev release, `X.Y.Z.devN` with N the run number, which installers take only when asked
52
+ (`pip install --pre`).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mjdevaccount
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.