commissioner 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 (47) hide show
  1. commissioner-0.1.0/.editorconfig +23 -0
  2. commissioner-0.1.0/.github/workflows/ci.yml +167 -0
  3. commissioner-0.1.0/.github/workflows/release.yml +57 -0
  4. commissioner-0.1.0/.gitignore +104 -0
  5. commissioner-0.1.0/.importlinter +42 -0
  6. commissioner-0.1.0/.pre-commit-config.yaml +22 -0
  7. commissioner-0.1.0/CHANGELOG.md +46 -0
  8. commissioner-0.1.0/CONTRIBUTING.md +53 -0
  9. commissioner-0.1.0/LICENSE +201 -0
  10. commissioner-0.1.0/PKG-INFO +196 -0
  11. commissioner-0.1.0/README.md +161 -0
  12. commissioner-0.1.0/SECURITY.md +46 -0
  13. commissioner-0.1.0/acceptance/read_verdict.py +63 -0
  14. commissioner-0.1.0/acceptance/record_and_export.py +138 -0
  15. commissioner-0.1.0/docs/README.md +10 -0
  16. commissioner-0.1.0/docs/packages/commissioner/development-plan.md +91 -0
  17. commissioner-0.1.0/docs/packages/commissioner/spec.md +272 -0
  18. commissioner-0.1.0/pyproject.toml +116 -0
  19. commissioner-0.1.0/requirements/README.md +52 -0
  20. commissioner-0.1.0/requirements/ci.lock +1105 -0
  21. commissioner-0.1.0/requirements/release.in +6 -0
  22. commissioner-0.1.0/requirements/release.lock +493 -0
  23. commissioner-0.1.0/src/commissioner/__about__.py +1 -0
  24. commissioner-0.1.0/src/commissioner/__init__.py +63 -0
  25. commissioner-0.1.0/src/commissioner/errors.py +64 -0
  26. commissioner-0.1.0/src/commissioner/ledger.py +155 -0
  27. commissioner-0.1.0/src/commissioner/policy.py +119 -0
  28. commissioner-0.1.0/src/commissioner/py.typed +0 -0
  29. commissioner-0.1.0/src/commissioner/sql.py +486 -0
  30. commissioner-0.1.0/src/commissioner/types.py +295 -0
  31. commissioner-0.1.0/tests/conftest.py +157 -0
  32. commissioner-0.1.0/tests/integration/egress_subprocess.py +76 -0
  33. commissioner-0.1.0/tests/integration/hostapp/__init__.py +10 -0
  34. commissioner-0.1.0/tests/integration/hostapp/migrations/env.py +47 -0
  35. commissioner-0.1.0/tests/integration/hostapp/migrations/script.py.mako +28 -0
  36. commissioner-0.1.0/tests/integration/hostapp/migrations/versions/.gitkeep +0 -0
  37. commissioner-0.1.0/tests/integration/hostapp/models.py +33 -0
  38. commissioner-0.1.0/tests/integration/test_concurrency.py +77 -0
  39. commissioner-0.1.0/tests/integration/test_hostapp.py +275 -0
  40. commissioner-0.1.0/tests/integration/test_mounting.py +211 -0
  41. commissioner-0.1.0/tests/integration/test_sql_ledger.py +274 -0
  42. commissioner-0.1.0/tests/performance/test_sql_scaling.py +96 -0
  43. commissioner-0.1.0/tests/unit/test_decision.py +208 -0
  44. commissioner-0.1.0/tests/unit/test_ledger_memory.py +143 -0
  45. commissioner-0.1.0/tests/unit/test_ledger_surface.py +71 -0
  46. commissioner-0.1.0/tests/unit/test_payload_roundtrip.py +80 -0
  47. commissioner-0.1.0/tests/unit/test_policy_matrix.py +160 -0
@@ -0,0 +1,23 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+ indent_size = 4
10
+
11
+ [*.py]
12
+ indent_size = 4
13
+ max_line_length = 100
14
+
15
+ [*.{toml,yml,yaml,json}]
16
+ indent_size = 2
17
+
18
+ [*.md]
19
+ trim_trailing_whitespace = false
20
+ max_line_length = off
21
+
22
+ [Makefile]
23
+ indent_style = tab
@@ -0,0 +1,167 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ format:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with: { python-version: "3.12" }
15
+ - run: pip install ruff
16
+ - run: ruff format --check .
17
+
18
+ lint:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with: { python-version: "3.12" }
24
+ - run: pip install ruff
25
+ - run: ruff check .
26
+
27
+ types:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with: { python-version: "3.12" }
33
+ - run: pip install --require-hashes -r requirements/ci.lock
34
+ - run: pip install . --no-deps
35
+ - run: mypy src tests
36
+
37
+ boundaries:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with: { python-version: "3.12" }
43
+ - run: pip install --require-hashes -r requirements/ci.lock
44
+ - run: pip install . --no-deps
45
+ - run: lint-imports
46
+
47
+ tests:
48
+ runs-on: ubuntu-latest
49
+ strategy:
50
+ matrix:
51
+ python-version: ["3.12", "3.13"]
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: actions/setup-python@v5
55
+ with: { python-version: "${{ matrix.python-version }}" }
56
+ - run: pip install --require-hashes -r requirements/ci.lock
57
+ - run: pip install . --no-deps
58
+ - run: pytest -m "not live and not performance" --cov --cov-report=xml
59
+
60
+ tests-314-early-warning:
61
+ runs-on: ubuntu-latest
62
+ continue-on-error: true
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - uses: actions/setup-python@v5
66
+ with: { python-version: "3.14" }
67
+ # Deliberately unpinned: ci.lock is resolved on 3.13, and pinning a version that has no
68
+ # 3.14 wheels would defeat the purpose of an early warning.
69
+ - run: pip install -e ".[dev]"
70
+ - run: pytest -m "not live and not performance"
71
+
72
+ db-matrix:
73
+ name: tests (PostgreSQL)
74
+ runs-on: ubuntu-latest
75
+ services:
76
+ postgres:
77
+ image: postgres:16
78
+ # Credentials and database name match `DEFAULT_POSTGRES_URL` in tests/conftest.py, so the
79
+ # service this job starts is the one the integration tests look for.
80
+ env:
81
+ POSTGRES_USER: commissioner
82
+ POSTGRES_PASSWORD: commissioner
83
+ POSTGRES_DB: commissioner_test
84
+ ports: ["5432:5432"]
85
+ options: >-
86
+ --health-cmd "pg_isready -U commissioner -d commissioner_test" --health-interval 5s --health-timeout 5s --health-retries 10
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ - uses: actions/setup-python@v5
90
+ with: { python-version: "3.12" }
91
+ - run: pip install --require-hashes -r requirements/ci.lock
92
+ - run: pip install . --no-deps
93
+ - run: pytest -m "not live and not performance" tests/integration
94
+ env:
95
+ # Without this the PostgreSQL legs would *skip* here exactly as they do on a developer's
96
+ # machine, and the job would go green having tested one dialect twice. A skipped dialect
97
+ # is an untested dialect.
98
+ COMMISSIONER_REQUIRE_POSTGRES: "1"
99
+ # The tests read COMMISSIONER_POSTGRES_URL, not DATABASE_URL. Set explicitly rather than
100
+ # relying on the default, so the job states which server it is testing.
101
+ COMMISSIONER_POSTGRES_URL: postgresql+psycopg://commissioner:commissioner@localhost:5432/commissioner_test
102
+
103
+ coverage:
104
+ needs: [tests]
105
+ runs-on: ubuntu-latest
106
+ steps:
107
+ - uses: actions/checkout@v4
108
+ - uses: actions/setup-python@v5
109
+ with: { python-version: "3.12" }
110
+ - run: pip install --require-hashes -r requirements/ci.lock
111
+ - run: pip install . --no-deps
112
+ - run: pytest -m "not live and not performance" --cov --cov-report=term-missing --cov-fail-under=95
113
+
114
+ contracts:
115
+ runs-on: ubuntu-latest
116
+ steps:
117
+ - uses: actions/checkout@v4
118
+ - uses: actions/setup-python@v5
119
+ with: { python-version: "3.12" }
120
+ - run: pip install --require-hashes -r requirements/ci.lock
121
+ - run: pip install . --no-deps
122
+ - run: pytest -m contract
123
+
124
+ security:
125
+ runs-on: ubuntu-latest
126
+ steps:
127
+ - uses: actions/checkout@v4
128
+ # gitleaks scans *history*, and `actions/checkout` fetches a single commit by default.
129
+ # For a push it is handed `<first-pushed>^..<last-pushed>`, so the parent of the first
130
+ # pushed commit has to be in the object store; in a depth-1 clone it is not, and git
131
+ # answers "unknown revision", which the action reports as exit code 1. It fails the same
132
+ # way whether or not a secret exists, so a green run would not have meant anything either.
133
+ with: { fetch-depth: 0 }
134
+ - uses: actions/setup-python@v5
135
+ with: { python-version: "3.12" }
136
+ - run: pip install pip-audit
137
+ # Audit the locked sets, not the job's own environment: a bare `pip-audit` here would
138
+ # inspect an environment containing only pip-audit itself (Security Standards §11).
139
+ - run: pip-audit --require-hashes -r requirements/ci.lock
140
+ - run: pip-audit --require-hashes -r requirements/release.lock
141
+ - uses: gitleaks/gitleaks-action@v2
142
+ env:
143
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144
+
145
+ build:
146
+ runs-on: ubuntu-latest
147
+ steps:
148
+ - uses: actions/checkout@v4
149
+ - uses: actions/setup-python@v5
150
+ with: { python-version: "3.12" }
151
+ - run: pip install --require-hashes -r requirements/release.lock
152
+ - run: python -m build --no-isolation
153
+ - run: twine check dist/*
154
+ - uses: actions/upload-artifact@v4
155
+ with: { name: dist, path: dist/ }
156
+
157
+ install-check:
158
+ needs: [build]
159
+ runs-on: ubuntu-latest
160
+ steps:
161
+ - uses: actions/checkout@v4
162
+ - uses: actions/setup-python@v5
163
+ with: { python-version: "3.12" }
164
+ - uses: actions/download-artifact@v4
165
+ with: { name: dist, path: dist/ }
166
+ - run: pip install dist/*.whl
167
+ - run: python -c "import commissioner"
@@ -0,0 +1,57 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*.*.*"]
6
+ workflow_dispatch: # manual TestPyPI dry run; see publish-testpypi below
7
+
8
+ permissions:
9
+ id-token: write # required for PyPI Trusted Publishing
10
+ contents: write # required to create the GitHub release
11
+
12
+ jobs:
13
+ release:
14
+ # Tag pushes only. Without this, clicking "Run workflow" for the TestPyPI dry run below would
15
+ # also fire this job and publish to real PyPI — the exact opposite of a dry run.
16
+ if: github.event_name == 'push'
17
+ runs-on: ubuntu-latest
18
+ environment: pypi # must match the Environment name set on the PyPI trusted publisher
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with: { python-version: "3.12" }
23
+ # Byte-for-byte the dry run's build chain below. A real release that resolved `build` and
24
+ # `hatchling` fresh from PyPI would not be the artifact the dry run proved.
25
+ - run: pip install --require-hashes -r requirements/release.lock
26
+ - run: python -m build --no-isolation
27
+ - run: twine check dist/*
28
+ - run: pip install "dist/$(ls dist | grep .whl)[dev]"
29
+ - run: pytest -m "not live and not performance"
30
+ - name: Publish to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
32
+ - name: Create GitHub release
33
+ uses: softprops/action-gh-release@v2
34
+ with:
35
+ generate_release_notes: true
36
+ files: dist/*
37
+
38
+ publish-testpypi:
39
+ # Manual only, via Actions -> Release -> Run workflow. Packaging and Release Standards §6
40
+ # requires a successful TestPyPI publish ahead of a package's first real release; 0.1.0 is
41
+ # this package's first published version, so run this once before tagging v0.1.0. Later
42
+ # releases may skip it, or use it again as a dry run.
43
+ if: github.event_name == 'workflow_dispatch'
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - uses: actions/setup-python@v5
48
+ with: { python-version: "3.12" }
49
+ - run: pip install --require-hashes -r requirements/release.lock
50
+ - run: python -m build --no-isolation
51
+ - run: twine check dist/*
52
+ - run: pip install "dist/$(ls dist | grep .whl)[dev]"
53
+ - run: pytest -m "not live and not performance"
54
+ - name: Publish to TestPyPI
55
+ uses: pypa/gh-action-pypi-publish@release/v1
56
+ with:
57
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,104 @@
1
+ # ---- Python ----
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # ---- Packaging / build backends ----
25
+ pip-wheel-metadata/
26
+ share/python-wheels/
27
+
28
+ # ---- Testing / coverage ----
29
+ .pytest_cache/
30
+ .cache
31
+ .coverage
32
+ .coverage.*
33
+ coverage.xml
34
+ *.cover
35
+ *.py,cover
36
+ htmlcov/
37
+ nosetests.xml
38
+ .hypothesis/
39
+
40
+ # ---- Type checking / linting ----
41
+ .mypy_cache/
42
+ .dmypy.json
43
+ dmypy.json
44
+ .ruff_cache/
45
+ .pytype/
46
+
47
+ # ---- Virtual environments ----
48
+ .venv/
49
+ venv/
50
+ ENV/
51
+ env/
52
+ env.bak/
53
+ venv.bak/
54
+ .python-version
55
+
56
+ # ---- Distribution / dependency locking ----
57
+ # requirements/*.lock is deliberately NOT ignored. The lock files are committed
58
+ # inputs: CI and release jobs install from them with --require-hashes (Packaging
59
+ # and Release Standards §4, Security Standards §11), so an ignored lock file
60
+ # means a checkout that cannot build.
61
+
62
+ # ---- Editors / OS ----
63
+ .vscode/
64
+ .idea/
65
+ *.swp
66
+ *.swo
67
+ .DS_Store
68
+ Thumbs.db
69
+
70
+ # ---- Suite runtime state (this component's local data) ----
71
+ # The application writes to XDG paths at runtime (~/.config, ~/.local/share,
72
+ # ~/.local/state per Master Architecture §1.2), never inside the repository.
73
+ # These entries only guard against a developer pointing XDG_* at the repo
74
+ # during local testing.
75
+ .local/
76
+ .config/
77
+ *.sqlite3
78
+ *.sqlite3-journal
79
+ *.sqlite3-wal
80
+ *.sqlite3-shm
81
+ /data/
82
+ /logs/
83
+ /backups/
84
+ /artifacts/
85
+ /exports/
86
+
87
+ # ---- Secrets ----
88
+ # Per Security Standards §8: secrets are never committed. Config files may
89
+ # only name where a secret comes from (*_env / *_file), never the value.
90
+ .env
91
+ .env.*
92
+ *.key
93
+ *.pem
94
+ secrets.toml
95
+
96
+ # ---- Node-free JS assets (MirrorWall consumers may still use a local tool) ----
97
+ node_modules/
98
+
99
+ # ---- Build artifacts from docs generation ----
100
+ docs/api/openapi-v1.json.tmp
101
+
102
+ # ---- import-linter cache ----
103
+ # `lint-imports` writes this on every gate run; it is a cache, never an input.
104
+ .import_linter_cache/
@@ -0,0 +1,42 @@
1
+ [importlinter]
2
+ root_package = commissioner
3
+ include_external_packages = True
4
+
5
+ [importlinter:contract:no-application-imports]
6
+ name = Commissioner must not import applications
7
+ type = forbidden
8
+ source_modules = commissioner
9
+ forbidden_modules =
10
+ freeweight
11
+ loadcoach
12
+ ideapress
13
+ promptcadence
14
+
15
+ [importlinter:contract:no-sibling-packages]
16
+ name = Commissioner must not import sibling capability packages
17
+ type = forbidden
18
+ source_modules = commissioner
19
+ forbidden_modules =
20
+ modelrack
21
+ sweatmeter
22
+ weightsdb
23
+ mirrorwall
24
+ cutctx
25
+ toolyard
26
+ loadledger
27
+
28
+ [importlinter:contract:only-the-sql-module-imports-sqlalchemy]
29
+ # ADR-0050 §4: `sqlalchemy` is an optional extra so the pure-value core stays installable with
30
+ # nothing but baseaicore and setspec. `alembic` has no exemption at all — the host owns every
31
+ # migration (ADR-0050 decision 5), so nothing in this package imports it, `commissioner.sql`
32
+ # included; it is a `[dev]` dependency for the miniature host only. With
33
+ # `include_external_packages = True`, import-linter collapses every external import to the
34
+ # top-level package, so `commissioner.sql -> sqlalchemy.**` would match nothing and is not written.
35
+ name = Only commissioner.sql may import SQLAlchemy, and nothing may import Alembic
36
+ type = forbidden
37
+ source_modules = commissioner
38
+ forbidden_modules =
39
+ sqlalchemy
40
+ alembic
41
+ ignore_imports =
42
+ commissioner.sql -> sqlalchemy
@@ -0,0 +1,22 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.6.9
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/pre-commit-hooks
9
+ rev: v4.6.0
10
+ hooks:
11
+ - id: trailing-whitespace
12
+ - id: end-of-file-fixer
13
+ - id: check-toml
14
+ - id: check-json
15
+ - id: check-added-large-files
16
+ - id: check-merge-conflict
17
+ - id: mixed-line-ending
18
+ args: [--fix=lf]
19
+ - repo: https://github.com/gitleaks/gitleaks
20
+ rev: v8.18.4
21
+ hooks:
22
+ - id: gitleaks
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to `commissioner` are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
+ [Semantic Versioning](https://semver.org/), pre-1.0 per
6
+ packaging and release standards §3.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] — 2026-09-03
11
+
12
+ ### Added
13
+ - Repository scaffold: toolchain copied from `py/LoadLedger` (hatchling, ruff, mypy strict,
14
+ import-linter, pytest with `pytest-randomly`, hash-pinned `requirements/` locks, CI and release
15
+ workflows), with the `setspec` dependency and its import-linter allowance added (this package,
16
+ like MirrorWall, is permitted to import SetSpec — master architecture §2).
17
+ - Phase 1, the pure core: `EgressTarget`, `EgressRequest`, `Verdict`, `EgressDecision` with
18
+ `to_payload`/`from_payload` against `setspec.governance.v1`'s `governance.egress_decision` 1.0;
19
+ the `EgressPolicy` protocol and `OrderedClassificationPolicy`, the whole shipped policy in four
20
+ rows (ADR-0054 rule 2); `CommissionerError`/`StoreFailure`. No I/O, no SQL, no logging, no
21
+ environment reads.
22
+ - Phase 2, the durable half: `commissioner.ledger` and `commissioner.sql` under the new
23
+ `commissioner[sql]` extra (ADR-0050, LoadLedger's mounting pattern copied by hand — the roadmap's
24
+ revisit trigger is a third mountable package).
25
+ - `EgressLedger` protocol and `InMemoryEgressLedger` — a process-local, thread-safe ledger with
26
+ no store of its own.
27
+ - `mount_egress_tables(metadata, *, prefix="egress_") -> EgressTables` adds one table,
28
+ `{prefix}decisions`, to a host's own `MetaData`: `decision_id` (primary key), `run_id`,
29
+ `verdict`, `target_name` and `decided_at` as indexed, filterable columns, and
30
+ `decision_json` — the decision's own `governance.egress_decision` payload, in canonical form —
31
+ as the byte-stable record. `verdict` is stored as plain text, never `sa.Enum`, so the same
32
+ column type renders on both dialects.
33
+ - `SqlEgressLedger(session_factory, *, table_prefix="egress_")` — `record`/`decisions` over a
34
+ host-owned session factory; no engine, no URL, no migration. Refuses a dialect other than
35
+ SQLite or PostgreSQL with `UnsupportedDialect`, and a `decision_id` this ledger has already
36
+ recorded with `StoreFailure`, rather than silently absorbing either.
37
+ - `UnsupportedDialect` (`EGRESS_UNSUPPORTED_DIALECT`), added to the error hierarchy alongside
38
+ `StoreFailure`.
39
+ - The append-only surface is asserted structurally, not merely documented: neither
40
+ `commissioner.ledger` nor `commissioner.sql` exposes an update or delete path, over both the
41
+ protocol and both implementations.
42
+ - `.importlinter`'s `no-sql-in-phase-1` contract replaced by
43
+ `only-the-sql-module-imports-sqlalchemy`: `sqlalchemy` is now permitted in `commissioner.sql`
44
+ alone, and nothing in the package imports `alembic` at runtime (the host owns every migration).
45
+ - The `[sql]` extra: `sqlalchemy>=2,<3`. The pure core still resolves to `baseaicore` and `setspec`
46
+ alone.
@@ -0,0 +1,53 @@
1
+ # Contributing to Commissioner
2
+
3
+ This repository is one component of the Local AI Suite. Before changing anything, read
4
+ `docs/packages/commissioner/spec.md` and the current
5
+ phase in `development-plan.md` — both are in this repository's `docs/` folder, copied from the suite's
6
+ central documentation set so this repository can be worked on independently.
7
+
8
+ ## Development setup
9
+
10
+ ```bash
11
+ python -m venv .venv
12
+ source .venv/bin/activate
13
+ pip install -e ".[dev]"
14
+ pre-commit install
15
+ ```
16
+
17
+ ## Required reading, in order
18
+
19
+ 1. This component's spec — purpose, scope, non-goals, contracts.
20
+ 2. `development-plan.md` in the same folder — the phase you are implementing, its acceptance criteria and its tests.
21
+ 3. [ADR-0054](docs/packages/commissioner/../../adr/0054-commissioner-records-egress-it-does-not-enforce-it.md) — the boundary that keeps this package small: the payload, the ordered comparison, and an append-only ledger, nothing else.
22
+
23
+ ## Rules that apply to every change here
24
+
25
+ * Follow the architecture's dependency direction.
26
+ This repository's `.importlinter` enforces it in CI; do not weaken that file to make an import work.
27
+ * No business logic in a route handler or CLI command body — both call one service method and render
28
+ .
29
+ * An unavailable measurement is `Unsupported`, never zero, never `None` used as a substitute
30
+ .
31
+ * Prompts are versioned JSON records, not Python string literals.
32
+ * Every phase's acceptance criteria in `development-plan.md` must be demonstrable, not merely
33
+ test-covered — the plan states what to run and what a person should see.
34
+ * **No enforcement.** `evaluate` never raises for a deny, and nothing here makes an HTTP call or
35
+ halts a caller. If a change starts to look like enforcement, it belongs in the caller.
36
+
37
+ ## Before opening a pull request
38
+
39
+ ```bash
40
+ ruff format --check .
41
+ ruff check .
42
+ mypy src tests
43
+ lint-imports
44
+ pytest -m "not live and not performance"
45
+ ```
46
+
47
+ All of the above run in CI (`.github/workflows/ci.yml`); a red CI run blocks merge.
48
+
49
+ ## Commit style
50
+
51
+ Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`, `build:`,
52
+ `ci:`), with `!` or a `BREAKING CHANGE:` footer for breaking changes. Update `CHANGELOG.md` under
53
+ `## [Unreleased]` for any user-visible change.