cutctx 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.
- cutctx-0.1.0/.editorconfig +23 -0
- cutctx-0.1.0/.github/workflows/ci.yml +136 -0
- cutctx-0.1.0/.github/workflows/release.yml +57 -0
- cutctx-0.1.0/.gitignore +104 -0
- cutctx-0.1.0/.importlinter +89 -0
- cutctx-0.1.0/.pre-commit-config.yaml +22 -0
- cutctx-0.1.0/CHANGELOG.md +105 -0
- cutctx-0.1.0/CONTRIBUTING.md +78 -0
- cutctx-0.1.0/LICENSE +201 -0
- cutctx-0.1.0/PKG-INFO +167 -0
- cutctx-0.1.0/README.md +138 -0
- cutctx-0.1.0/SECURITY.md +56 -0
- cutctx-0.1.0/acceptance/plan_and_apply.py +186 -0
- cutctx-0.1.0/docs/packages/cutctx/development-plan.md +101 -0
- cutctx-0.1.0/docs/packages/cutctx/spec.md +292 -0
- cutctx-0.1.0/pyproject.toml +105 -0
- cutctx-0.1.0/requirements/README.md +63 -0
- cutctx-0.1.0/requirements/ci.lock +604 -0
- cutctx-0.1.0/requirements/release.in +6 -0
- cutctx-0.1.0/requirements/release.lock +493 -0
- cutctx-0.1.0/src/cutctx/__about__.py +1 -0
- cutctx-0.1.0/src/cutctx/__init__.py +105 -0
- cutctx-0.1.0/src/cutctx/_invariants.py +529 -0
- cutctx-0.1.0/src/cutctx/errors.py +89 -0
- cutctx-0.1.0/src/cutctx/estimator.py +78 -0
- cutctx-0.1.0/src/cutctx/executor.py +238 -0
- cutctx-0.1.0/src/cutctx/policies/__init__.py +32 -0
- cutctx-0.1.0/src/cutctx/policies/chain.py +401 -0
- cutctx-0.1.0/src/cutctx/policies/drop_oldest.py +98 -0
- cutctx-0.1.0/src/cutctx/policies/masking.py +242 -0
- cutctx-0.1.0/src/cutctx/policies/summarizing.py +302 -0
- cutctx-0.1.0/src/cutctx/py.typed +0 -0
- cutctx-0.1.0/src/cutctx/types.py +734 -0
- cutctx-0.1.0/tests/conftest.py +86 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-100-0.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-1400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-200-0.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-400-2.json +1 -0
- cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-700-0.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-100-0.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-1400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-200-0.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-400-2.json +1 -0
- cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-700-0.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/report-chain-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-displaced_result-240-1.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-empty-0-4.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-entirely_pinned-30-0.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-100-0.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-multi_call_exchange-100-1.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-multi_call_exchange-900-2.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-orphaned_result-45-1.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-simple_chat-200-2.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-simple_chat-60-1.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-1400-0.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-200-0.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-400-2.json +1 -0
- cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-700-0.json +1 -0
- cutctx-0.1.0/tests/oracles.py +122 -0
- cutctx-0.1.0/tests/performance/test_targets.py +91 -0
- cutctx-0.1.0/tests/property/test_apply_property.py +208 -0
- cutctx-0.1.0/tests/property/test_invariants_property.py +467 -0
- cutctx-0.1.0/tests/property/test_policies_property.py +435 -0
- cutctx-0.1.0/tests/strategies.py +341 -0
- cutctx-0.1.0/tests/unit/test_chain.py +375 -0
- cutctx-0.1.0/tests/unit/test_determinism_goldens.py +300 -0
- cutctx-0.1.0/tests/unit/test_drop_oldest.py +143 -0
- cutctx-0.1.0/tests/unit/test_estimator.py +48 -0
- cutctx-0.1.0/tests/unit/test_executor.py +236 -0
- cutctx-0.1.0/tests/unit/test_invariants.py +663 -0
- cutctx-0.1.0/tests/unit/test_masking.py +277 -0
- cutctx-0.1.0/tests/unit/test_packaging.py +285 -0
- cutctx-0.1.0/tests/unit/test_summarizing.py +293 -0
- cutctx-0.1.0/tests/unit/test_types.py +276 -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,136 @@
|
|
|
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
|
+
coverage:
|
|
73
|
+
needs: [tests]
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- uses: actions/setup-python@v5
|
|
78
|
+
with: { python-version: "3.12" }
|
|
79
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
80
|
+
- run: pip install . --no-deps
|
|
81
|
+
- run: pytest -m "not live and not performance" --cov --cov-report=term-missing --cov-fail-under=95
|
|
82
|
+
|
|
83
|
+
contracts:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: actions/setup-python@v5
|
|
88
|
+
with: { python-version: "3.12" }
|
|
89
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
90
|
+
- run: pip install . --no-deps
|
|
91
|
+
- run: pytest -m contract
|
|
92
|
+
|
|
93
|
+
security:
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v4
|
|
97
|
+
# gitleaks scans *history*, and `actions/checkout` fetches a single commit by default.
|
|
98
|
+
# For a push it is handed `<first-pushed>^..<last-pushed>`, so the parent of the first
|
|
99
|
+
# pushed commit has to be in the object store; in a depth-1 clone it is not, and git
|
|
100
|
+
# answers "unknown revision", which the action reports as exit code 1. It fails the same
|
|
101
|
+
# way whether or not a secret exists, so a green run would not have meant anything either.
|
|
102
|
+
with: { fetch-depth: 0 }
|
|
103
|
+
- uses: actions/setup-python@v5
|
|
104
|
+
with: { python-version: "3.12" }
|
|
105
|
+
- run: pip install pip-audit
|
|
106
|
+
# Audit the locked sets, not the job's own environment: a bare `pip-audit` here would
|
|
107
|
+
# inspect an environment containing only pip-audit itself (Security Standards §11).
|
|
108
|
+
- run: pip-audit --require-hashes -r requirements/ci.lock
|
|
109
|
+
- run: pip-audit --require-hashes -r requirements/release.lock
|
|
110
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
111
|
+
env:
|
|
112
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
113
|
+
|
|
114
|
+
build:
|
|
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/release.lock
|
|
121
|
+
- run: python -m build --no-isolation
|
|
122
|
+
- run: twine check dist/*
|
|
123
|
+
- uses: actions/upload-artifact@v4
|
|
124
|
+
with: { name: dist, path: dist/ }
|
|
125
|
+
|
|
126
|
+
install-check:
|
|
127
|
+
needs: [build]
|
|
128
|
+
runs-on: ubuntu-latest
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/checkout@v4
|
|
131
|
+
- uses: actions/setup-python@v5
|
|
132
|
+
with: { python-version: "3.12" }
|
|
133
|
+
- uses: actions/download-artifact@v4
|
|
134
|
+
with: { name: dist, path: dist/ }
|
|
135
|
+
- run: pip install dist/*.whl
|
|
136
|
+
- run: python -c "import cutctx"
|
|
@@ -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/
|
cutctx-0.1.0/.gitignore
ADDED
|
@@ -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,89 @@
|
|
|
1
|
+
[importlinter]
|
|
2
|
+
root_package = cutctx
|
|
3
|
+
include_external_packages = True
|
|
4
|
+
|
|
5
|
+
[importlinter:contract:no-application-imports]
|
|
6
|
+
name = CutCtx must not import applications
|
|
7
|
+
type = forbidden
|
|
8
|
+
source_modules = cutctx
|
|
9
|
+
forbidden_modules =
|
|
10
|
+
freeweight
|
|
11
|
+
loadcoach
|
|
12
|
+
ideapress
|
|
13
|
+
promptcadence
|
|
14
|
+
|
|
15
|
+
[importlinter:contract:no-sibling-packages]
|
|
16
|
+
name = CutCtx must not import sibling capability packages
|
|
17
|
+
type = forbidden
|
|
18
|
+
source_modules = cutctx
|
|
19
|
+
forbidden_modules =
|
|
20
|
+
setspec
|
|
21
|
+
modelrack
|
|
22
|
+
sweatmeter
|
|
23
|
+
weightsdb
|
|
24
|
+
mirrorwall
|
|
25
|
+
toolyard
|
|
26
|
+
loadledger
|
|
27
|
+
commissioner
|
|
28
|
+
spotcheck
|
|
29
|
+
|
|
30
|
+
[importlinter:contract:no-second-path-to-a-model]
|
|
31
|
+
# ADR-0052 decision 2 and gold standards §2: purity is proven, not claimed. The package holds no
|
|
32
|
+
# HTTP client and no provider, so summarization *cannot* be performed here even by accident — it
|
|
33
|
+
# crosses the boundary as a SummarizationRequest. `no-sibling-packages` above already bars
|
|
34
|
+
# `modelrack`; this contract bars the do-it-yourself alternative.
|
|
35
|
+
#
|
|
36
|
+
# allow_indirect_imports: these contracts describe what *this package's own modules* import.
|
|
37
|
+
# `baseaicore` is stdlib-only, so no chain reaches these today, but a chain through a dependency
|
|
38
|
+
# would be that dependency's budget to answer for, not a CutCtx import to fix.
|
|
39
|
+
name = CutCtx must hold no HTTP client and no socket
|
|
40
|
+
allow_indirect_imports = True
|
|
41
|
+
type = forbidden
|
|
42
|
+
source_modules = cutctx
|
|
43
|
+
forbidden_modules =
|
|
44
|
+
httpx
|
|
45
|
+
requests
|
|
46
|
+
aiohttp
|
|
47
|
+
urllib3
|
|
48
|
+
urllib
|
|
49
|
+
http
|
|
50
|
+
socket
|
|
51
|
+
ssl
|
|
52
|
+
asyncio
|
|
53
|
+
|
|
54
|
+
[importlinter:contract:no-persistence]
|
|
55
|
+
# ADR-0052 decision 2: no database, no filesystem. CutCtx never persists anything (spec §10); the
|
|
56
|
+
# caller stores plans and reports. The stdlib halves matter as much as the third-party ones: a
|
|
57
|
+
# `pathlib` import here would be the first step of a cache nobody asked for.
|
|
58
|
+
name = CutCtx must touch no database and no filesystem
|
|
59
|
+
allow_indirect_imports = True
|
|
60
|
+
type = forbidden
|
|
61
|
+
source_modules = cutctx
|
|
62
|
+
forbidden_modules =
|
|
63
|
+
sqlalchemy
|
|
64
|
+
alembic
|
|
65
|
+
sqlite3
|
|
66
|
+
os
|
|
67
|
+
io
|
|
68
|
+
pathlib
|
|
69
|
+
shutil
|
|
70
|
+
tempfile
|
|
71
|
+
glob
|
|
72
|
+
subprocess
|
|
73
|
+
logging
|
|
74
|
+
|
|
75
|
+
[importlinter:contract:no-clock-and-no-randomness]
|
|
76
|
+
# The one package in the suite that needs no clock. A plan carries no time, because a plan must be
|
|
77
|
+
# byte-identical on re-derivation (spec §11 contract 4) and appears in an audit record. If a
|
|
78
|
+
# `CompactionReport` wants a timestamp, the caller stamps it. Randomness is barred for the same
|
|
79
|
+
# reason: it is the other way a re-derived plan stops matching the one that was recorded.
|
|
80
|
+
name = CutCtx must read no clock and draw no randomness
|
|
81
|
+
allow_indirect_imports = True
|
|
82
|
+
type = forbidden
|
|
83
|
+
source_modules = cutctx
|
|
84
|
+
forbidden_modules =
|
|
85
|
+
datetime
|
|
86
|
+
time
|
|
87
|
+
random
|
|
88
|
+
secrets
|
|
89
|
+
uuid
|
|
@@ -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,105 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) (pre-1.0: `0.x`).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-09-03
|
|
10
|
+
|
|
11
|
+
The first release. Phases 1 and 2 of the
|
|
12
|
+
[development plan](docs/packages/cutctx/development-plan.md) — the vocabulary, the invariants, and
|
|
13
|
+
the four shipped policies.
|
|
14
|
+
|
|
15
|
+
### Added — Phase 2, the policy set
|
|
16
|
+
|
|
17
|
+
* **`ObservationMaskingPolicy`** — replaces old `TOOL` bodies with a labelled stub carrying the
|
|
18
|
+
original's **sha256 and token estimate, never an excerpt** (spec §14): the original may hold a
|
|
19
|
+
secret, and a "first 200 characters" preview is a leak with an extra step. The turn stays in the
|
|
20
|
+
view at its own position, which is why masking orphans nothing and is legal under contract 3.
|
|
21
|
+
Reasoning turns are left byte-identical — that is the whole value of the policy, and a property
|
|
22
|
+
asserts it rather than hoping for it.
|
|
23
|
+
* **`SummarizingPolicy`** — folds the oldest contiguous unpinned span into one planned summary turn
|
|
24
|
+
and one `SummarizationRequest`. It **plans a summarization and never performs one** (ADR-0052):
|
|
25
|
+
no model, no HTTP client, no prompt text, and a `prompt_id` naming a versioned record.
|
|
26
|
+
* **`PolicyChain`** and **`default_chain`** — masking → summarizing → drop-oldest, stopping as soon
|
|
27
|
+
as the budget fits.
|
|
28
|
+
* **`DEFAULT_PLACEHOLDER`** and **`GROUP_ID_PREFIX`**, so a consumer reading a plan can recognise a
|
|
29
|
+
shipped policy's output.
|
|
30
|
+
* **Cross-matrix determinism goldens** for all four policies, over the Phase-1 fixtures at every
|
|
31
|
+
Phase-1 budget (dev-plan AC2) and over a second fixture set large enough to actually produce a
|
|
32
|
+
stub and a summary group — because the Phase-1 transcripts are reached by keeping and dropping
|
|
33
|
+
alone, and a golden set that locked in only those would lock in nothing new.
|
|
34
|
+
* **A property suite over every shipped policy**, against oracles restated from the spec in a
|
|
35
|
+
different shape from the implementation (`maskable_ids` counts backwards where the policy slices;
|
|
36
|
+
`exchange_rule_holds` keys on the correlation id with no notion of position).
|
|
37
|
+
* **`acceptance/plan_and_apply.py`** — spec §20 criterion 2 as a runnable check rather than a
|
|
38
|
+
demonstration, exiting non-zero when a claim fails.
|
|
39
|
+
|
|
40
|
+
### Decided — Phase 2
|
|
41
|
+
|
|
42
|
+
* **Contract 3 said the wrong thing, and the document was amended.** Read literally, "masked
|
|
43
|
+
together" forbade the `ObservationMaskingPolicy` §7 ships. Phase 1 enforced the reading that the
|
|
44
|
+
prohibition is on *separation* — and what separates is *removal* — and left the decision to this
|
|
45
|
+
row. Having built the policy, the reading is confirmed: within one exchange, either every member
|
|
46
|
+
is retained (`KEEP`/`MASK`, mixed freely) or every member is removed by the same action. The spec
|
|
47
|
+
now states the enforced rule.
|
|
48
|
+
* **A chain composes over a projection, not over an applied view.** Applying requires summaries and
|
|
49
|
+
this package produces none, so the chain builds the view each plan *would* produce — a masked
|
|
50
|
+
turn's replacement is in the plan, a summary turn's id is derived and its estimate is
|
|
51
|
+
`target_tokens` — and lifts the next policy's decisions back onto the real transcript. **One
|
|
52
|
+
plan is built, once, through `_invariants.build_plan`.** A later policy that removes a summary
|
|
53
|
+
turn removes everything folded into it, and a half-removed exchange is escalated to the removing
|
|
54
|
+
action before the plan is built rather than being rejected after.
|
|
55
|
+
* **Ordering is transcript position at every step**, never a `dict` or `set` iteration order. The
|
|
56
|
+
development plan named "nondeterminism via dict ordering in group assembly" as this phase's
|
|
57
|
+
likely failure mode, and it is the kind that reproduces only under an unlucky `pytest-randomly`
|
|
58
|
+
seed unless the ordering is structural.
|
|
59
|
+
* **A summary group's id is a digest of its span's turn ids** — never a counter and never a uuid.
|
|
60
|
+
A counter would renumber when a chain composed the policies differently, and the id is in the
|
|
61
|
+
plan's bytes *and* in the summary turn's derived id.
|
|
62
|
+
* **Masking stops as soon as the budget fits**, oldest result first, the way `DropOldestPolicy`
|
|
63
|
+
does; `keep_recent_results` is a floor that does not move for the budget. Masking every eligible
|
|
64
|
+
result would cost the model observations for nothing.
|
|
65
|
+
* **A result smaller than its own stub is left alone.** A stub costs about thirty tokens, so
|
|
66
|
+
masking a two-token result would *increase* the estimate, and a transcript of many tiny results
|
|
67
|
+
would grow under a policy whose purpose is shrinking.
|
|
68
|
+
* **A planned summary's token figure is `target_tokens`** — an estimate of a text that does not
|
|
69
|
+
exist yet — and the executor does not re-estimate the text it is handed. That is contract 5
|
|
70
|
+
working as intended; recomputing anything in the report is exactly what makes plan and report
|
|
71
|
+
able to disagree.
|
|
72
|
+
|
|
73
|
+
### Fixed — Phase 2
|
|
74
|
+
|
|
75
|
+
* **`SummarizingPolicy` folded a span even when the transcript already fitted the budget**, which
|
|
76
|
+
spent a model call and replaced turns the model could still have read. Found by the property
|
|
77
|
+
`test_a_transcript_that_already_fits_is_left_alone_by_every_shipped_policy`, which runs over
|
|
78
|
+
every shipped policy: the other three check the budget inside a loop over reductions, and this
|
|
79
|
+
one has no such loop, so it needed the check stated explicitly.
|
|
80
|
+
|
|
81
|
+
### Added — Phase 1
|
|
82
|
+
|
|
83
|
+
* **Transcript model** — `TranscriptTurn`, `Transcript`, `Role`, `Action`, `CompactionBudget`,
|
|
84
|
+
with `tool_call_id` as a *correlation* id so that a multi-call assistant turn and all of its
|
|
85
|
+
results form one tool exchange.
|
|
86
|
+
* **Plan vocabulary** — `TurnAction`, `TurnReplacement`, `SummarizationRequest`, `CompactionPlan`,
|
|
87
|
+
`CompactedTranscript`, `CompactionReport`, `CompactionPolicy`. `CompactionReport` is the body of
|
|
88
|
+
the suite's `context.compacted` event (spec §11 contract 7) and carries no timestamp.
|
|
89
|
+
* **`cutctx._invariants`** — spec §11 contracts 1–3 in one place, plus the token arithmetic and
|
|
90
|
+
the `budget_unmet` determination. `CompactionPlan` takes the transcript and budget it is a plan
|
|
91
|
+
*for* as `InitVar`s and validates against them at construction, so there is no path to an
|
|
92
|
+
unvalidated plan.
|
|
93
|
+
* **`BudgetUnsatisfiable`** naming both numbers, `SummaryMissing` naming the group,
|
|
94
|
+
`PlanTranscriptMismatch` naming the difference — all under `CompactionError`, all subclassing
|
|
95
|
+
`baseaicore.SuiteError`.
|
|
96
|
+
* **`CharRatioEstimator`** (`chars_per_token=4.0`) behind the `TokenEstimator` protocol, with the
|
|
97
|
+
ratio recorded on plans that used it and never presented as a count.
|
|
98
|
+
* **`CompactionExecutor.apply`** — masking, summary substitution, `summaries` fulfilment, and a
|
|
99
|
+
report whose figures are copied from the plan rather than recomputed.
|
|
100
|
+
* **`DropOldestPolicy`** — drops whole tool exchanges, oldest first, until the budget fits;
|
|
101
|
+
`budget_unmet` when it cannot, `BudgetUnsatisfiable` when nothing could.
|
|
102
|
+
* Property-based invariant suite (`hypothesis`), determinism goldens, an import-graph purity test,
|
|
103
|
+
and the spec §15 performance targets behind the `performance` marker.
|
|
104
|
+
|
|
105
|
+
[Unreleased]: https://github.com/JPKell/CutCtx/commits/main
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Contributing to CutCtx
|
|
2
|
+
|
|
3
|
+
This repository is one component of the Local AI Suite. Before changing anything, read
|
|
4
|
+
`docs/packages/cutctx/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, and especially §11, the public contracts
|
|
20
|
+
`cutctx._invariants` exists to enforce.
|
|
21
|
+
2. `development-plan.md` in the same folder — the phase you are implementing, its acceptance
|
|
22
|
+
criteria and its tests.
|
|
23
|
+
3. `src/cutctx/_invariants.py`'s module docstring — the rules, how contract 3 is read, and why
|
|
24
|
+
protection propagates through a tool exchange.
|
|
25
|
+
|
|
26
|
+
## Rules that apply to every change here
|
|
27
|
+
|
|
28
|
+
* Follow the architecture's dependency direction.
|
|
29
|
+
This repository's `.importlinter` enforces it in CI; do not weaken that file to make an import work.
|
|
30
|
+
* **Purity is the product.** No I/O of any kind: no network, no filesystem, no database, no
|
|
31
|
+
environment, no clock, no randomness, no logging. Five `import-linter` contracts and an
|
|
32
|
+
import-allowlist test enforce it; do not weaken either to make an import work.
|
|
33
|
+
* **Every policy routes through `cutctx._invariants`.** A `CompactionPlan` cannot be constructed
|
|
34
|
+
without the transcript and budget it is a plan *for*, and construction validates against them —
|
|
35
|
+
so there is no unvalidated plan. Build yours with `_invariants.build_plan`, which also does the
|
|
36
|
+
token arithmetic and the `budget_unmet` determination; a test asserts that every module under
|
|
37
|
+
`policies/` does.
|
|
38
|
+
* **An estimate is never presented as a count** (ADR-0016): token figures are estimates, and the
|
|
39
|
+
character-ratio default's ratio rides on the plan that used it.
|
|
40
|
+
* **Prompts are named, never carried** — `prompt_id` refers to a versioned record in the
|
|
41
|
+
*application's* prompt pack. No prompt text belongs in this package.
|
|
42
|
+
* **Plans are byte-identical on re-derivation.** Nothing may depend on dict insertion order, on a
|
|
43
|
+
clock, or on randomness. Changing a policy's behaviour bumps its `version` and its goldens.
|
|
44
|
+
* Every phase's acceptance criteria in `development-plan.md` must be demonstrable, not merely
|
|
45
|
+
test-covered — the plan states what to run and what a person should see.
|
|
46
|
+
|
|
47
|
+
## Before opening a pull request
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
ruff format --check .
|
|
51
|
+
ruff check .
|
|
52
|
+
mypy src tests
|
|
53
|
+
lint-imports
|
|
54
|
+
pytest -m "not live and not performance"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All of the above run in CI (`.github/workflows/ci.yml`); a red CI run blocks merge. Coverage floor
|
|
58
|
+
is **95 %** (a shared package, not an application).
|
|
59
|
+
|
|
60
|
+
### Replaying a failing property test
|
|
61
|
+
|
|
62
|
+
The invariant suite is property-based. `pytest-randomly` reseeds every test, so a failure is not
|
|
63
|
+
reproducible from the command line alone — but the hypothesis profile in `tests/conftest.py` sets
|
|
64
|
+
`print_blob=True`, so each failure prints a `@reproduce_failure(...)` decorator. Paste it onto the
|
|
65
|
+
failing test to replay that exact example; the `--randomly-seed=` line pytest prints reproduces the
|
|
66
|
+
*ordering*. A failure that only appears under one seed is a real bug, never a reason to pin one.
|
|
67
|
+
|
|
68
|
+
New properties belong beside the existing ones, and new generators in `tests/strategies.py`. Build
|
|
69
|
+
transcripts **by construction**, never by `filter`: heavy filtering gives flaky health-check
|
|
70
|
+
failures and useless shrinking. And state a property against `tests/oracles.py` — the untouchable
|
|
71
|
+
set and the tool exchanges restated from the spec — rather than against the implementation's own
|
|
72
|
+
helpers, or the property moves with the bug.
|
|
73
|
+
|
|
74
|
+
## Commit style
|
|
75
|
+
|
|
76
|
+
Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`, `build:`,
|
|
77
|
+
`ci:`), with `!` or a `BREAKING CHANGE:` footer for breaking changes. Update `CHANGELOG.md` under
|
|
78
|
+
`## [Unreleased]` for any user-visible change.
|