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.
Files changed (119) hide show
  1. cutctx-0.1.0/.editorconfig +23 -0
  2. cutctx-0.1.0/.github/workflows/ci.yml +136 -0
  3. cutctx-0.1.0/.github/workflows/release.yml +57 -0
  4. cutctx-0.1.0/.gitignore +104 -0
  5. cutctx-0.1.0/.importlinter +89 -0
  6. cutctx-0.1.0/.pre-commit-config.yaml +22 -0
  7. cutctx-0.1.0/CHANGELOG.md +105 -0
  8. cutctx-0.1.0/CONTRIBUTING.md +78 -0
  9. cutctx-0.1.0/LICENSE +201 -0
  10. cutctx-0.1.0/PKG-INFO +167 -0
  11. cutctx-0.1.0/README.md +138 -0
  12. cutctx-0.1.0/SECURITY.md +56 -0
  13. cutctx-0.1.0/acceptance/plan_and_apply.py +186 -0
  14. cutctx-0.1.0/docs/packages/cutctx/development-plan.md +101 -0
  15. cutctx-0.1.0/docs/packages/cutctx/spec.md +292 -0
  16. cutctx-0.1.0/pyproject.toml +105 -0
  17. cutctx-0.1.0/requirements/README.md +63 -0
  18. cutctx-0.1.0/requirements/ci.lock +604 -0
  19. cutctx-0.1.0/requirements/release.in +6 -0
  20. cutctx-0.1.0/requirements/release.lock +493 -0
  21. cutctx-0.1.0/src/cutctx/__about__.py +1 -0
  22. cutctx-0.1.0/src/cutctx/__init__.py +105 -0
  23. cutctx-0.1.0/src/cutctx/_invariants.py +529 -0
  24. cutctx-0.1.0/src/cutctx/errors.py +89 -0
  25. cutctx-0.1.0/src/cutctx/estimator.py +78 -0
  26. cutctx-0.1.0/src/cutctx/executor.py +238 -0
  27. cutctx-0.1.0/src/cutctx/policies/__init__.py +32 -0
  28. cutctx-0.1.0/src/cutctx/policies/chain.py +401 -0
  29. cutctx-0.1.0/src/cutctx/policies/drop_oldest.py +98 -0
  30. cutctx-0.1.0/src/cutctx/policies/masking.py +242 -0
  31. cutctx-0.1.0/src/cutctx/policies/summarizing.py +302 -0
  32. cutctx-0.1.0/src/cutctx/py.typed +0 -0
  33. cutctx-0.1.0/src/cutctx/types.py +734 -0
  34. cutctx-0.1.0/tests/conftest.py +86 -0
  35. cutctx-0.1.0/tests/goldens/chain-1.0.0-displaced_result-240-1.json +1 -0
  36. cutctx-0.1.0/tests/goldens/chain-1.0.0-empty-0-4.json +1 -0
  37. cutctx-0.1.0/tests/goldens/chain-1.0.0-entirely_pinned-30-0.json +1 -0
  38. cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-100-0.json +1 -0
  39. cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-200-2.json +1 -0
  40. cutctx-0.1.0/tests/goldens/chain-1.0.0-long_chat-400-0.json +1 -0
  41. cutctx-0.1.0/tests/goldens/chain-1.0.0-multi_call_exchange-100-1.json +1 -0
  42. cutctx-0.1.0/tests/goldens/chain-1.0.0-multi_call_exchange-900-2.json +1 -0
  43. cutctx-0.1.0/tests/goldens/chain-1.0.0-orphaned_result-45-1.json +1 -0
  44. cutctx-0.1.0/tests/goldens/chain-1.0.0-simple_chat-200-2.json +1 -0
  45. cutctx-0.1.0/tests/goldens/chain-1.0.0-simple_chat-60-1.json +1 -0
  46. cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-1400-0.json +1 -0
  47. cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-200-0.json +1 -0
  48. cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-400-2.json +1 -0
  49. cutctx-0.1.0/tests/goldens/chain-1.0.0-tool_heavy-700-0.json +1 -0
  50. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-displaced_result-240-1.json +1 -0
  51. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-empty-0-4.json +1 -0
  52. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-entirely_pinned-30-0.json +1 -0
  53. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-multi_call_exchange-100-1.json +1 -0
  54. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-multi_call_exchange-900-2.json +1 -0
  55. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-orphaned_result-45-1.json +1 -0
  56. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-simple_chat-200-2.json +1 -0
  57. cutctx-0.1.0/tests/goldens/drop_oldest-1.0.0-simple_chat-60-1.json +1 -0
  58. cutctx-0.1.0/tests/goldens/masking-1.0.0-displaced_result-240-1.json +1 -0
  59. cutctx-0.1.0/tests/goldens/masking-1.0.0-empty-0-4.json +1 -0
  60. cutctx-0.1.0/tests/goldens/masking-1.0.0-entirely_pinned-30-0.json +1 -0
  61. cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-100-0.json +1 -0
  62. cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-200-2.json +1 -0
  63. cutctx-0.1.0/tests/goldens/masking-1.0.0-long_chat-400-0.json +1 -0
  64. cutctx-0.1.0/tests/goldens/masking-1.0.0-multi_call_exchange-100-1.json +1 -0
  65. cutctx-0.1.0/tests/goldens/masking-1.0.0-multi_call_exchange-900-2.json +1 -0
  66. cutctx-0.1.0/tests/goldens/masking-1.0.0-orphaned_result-45-1.json +1 -0
  67. cutctx-0.1.0/tests/goldens/masking-1.0.0-simple_chat-200-2.json +1 -0
  68. cutctx-0.1.0/tests/goldens/masking-1.0.0-simple_chat-60-1.json +1 -0
  69. cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-1400-0.json +1 -0
  70. cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-200-0.json +1 -0
  71. cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-400-2.json +1 -0
  72. cutctx-0.1.0/tests/goldens/masking-1.0.0-tool_heavy-700-0.json +1 -0
  73. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-displaced_result-240-1.json +1 -0
  74. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-empty-0-4.json +1 -0
  75. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-entirely_pinned-30-0.json +1 -0
  76. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-multi_call_exchange-100-1.json +1 -0
  77. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-multi_call_exchange-900-2.json +1 -0
  78. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-orphaned_result-45-1.json +1 -0
  79. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-simple_chat-200-2.json +1 -0
  80. cutctx-0.1.0/tests/goldens/report-chain-1.0.0-simple_chat-60-1.json +1 -0
  81. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-displaced_result-240-1.json +1 -0
  82. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-empty-0-4.json +1 -0
  83. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-entirely_pinned-30-0.json +1 -0
  84. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-multi_call_exchange-100-1.json +1 -0
  85. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-multi_call_exchange-900-2.json +1 -0
  86. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-orphaned_result-45-1.json +1 -0
  87. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-simple_chat-200-2.json +1 -0
  88. cutctx-0.1.0/tests/goldens/report-drop_oldest-1.0.0-simple_chat-60-1.json +1 -0
  89. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-displaced_result-240-1.json +1 -0
  90. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-empty-0-4.json +1 -0
  91. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-entirely_pinned-30-0.json +1 -0
  92. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-100-0.json +1 -0
  93. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-200-2.json +1 -0
  94. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-long_chat-400-0.json +1 -0
  95. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-multi_call_exchange-100-1.json +1 -0
  96. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-multi_call_exchange-900-2.json +1 -0
  97. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-orphaned_result-45-1.json +1 -0
  98. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-simple_chat-200-2.json +1 -0
  99. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-simple_chat-60-1.json +1 -0
  100. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-1400-0.json +1 -0
  101. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-200-0.json +1 -0
  102. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-400-2.json +1 -0
  103. cutctx-0.1.0/tests/goldens/summarizing-1.0.0-tool_heavy-700-0.json +1 -0
  104. cutctx-0.1.0/tests/oracles.py +122 -0
  105. cutctx-0.1.0/tests/performance/test_targets.py +91 -0
  106. cutctx-0.1.0/tests/property/test_apply_property.py +208 -0
  107. cutctx-0.1.0/tests/property/test_invariants_property.py +467 -0
  108. cutctx-0.1.0/tests/property/test_policies_property.py +435 -0
  109. cutctx-0.1.0/tests/strategies.py +341 -0
  110. cutctx-0.1.0/tests/unit/test_chain.py +375 -0
  111. cutctx-0.1.0/tests/unit/test_determinism_goldens.py +300 -0
  112. cutctx-0.1.0/tests/unit/test_drop_oldest.py +143 -0
  113. cutctx-0.1.0/tests/unit/test_estimator.py +48 -0
  114. cutctx-0.1.0/tests/unit/test_executor.py +236 -0
  115. cutctx-0.1.0/tests/unit/test_invariants.py +663 -0
  116. cutctx-0.1.0/tests/unit/test_masking.py +277 -0
  117. cutctx-0.1.0/tests/unit/test_packaging.py +285 -0
  118. cutctx-0.1.0/tests/unit/test_summarizing.py +293 -0
  119. 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/
@@ -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.