chancel 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 (123) hide show
  1. chancel-0.1.0/.devcontainer/devcontainer.json +12 -0
  2. chancel-0.1.0/.env.example +7 -0
  3. chancel-0.1.0/.github/CODEOWNERS +3 -0
  4. chancel-0.1.0/.github/ISSUE_TEMPLATE/adapter-request.yml +32 -0
  5. chancel-0.1.0/.github/ISSUE_TEMPLATE/bug.yml +38 -0
  6. chancel-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +12 -0
  7. chancel-0.1.0/.github/dependabot.yml +10 -0
  8. chancel-0.1.0/.github/workflows/ci.yml +196 -0
  9. chancel-0.1.0/.github/workflows/codeql.yml +29 -0
  10. chancel-0.1.0/.github/workflows/docs.yml +44 -0
  11. chancel-0.1.0/.github/workflows/nightly-live.yml +55 -0
  12. chancel-0.1.0/.github/workflows/release.yml +115 -0
  13. chancel-0.1.0/.gitignore +25 -0
  14. chancel-0.1.0/.pre-commit-config.yaml +28 -0
  15. chancel-0.1.0/CHANGELOG.md +41 -0
  16. chancel-0.1.0/CONTRIBUTING.md +8 -0
  17. chancel-0.1.0/LICENSE +21 -0
  18. chancel-0.1.0/PKG-INFO +155 -0
  19. chancel-0.1.0/PRPs/ai_docs/claude-agent-sdk.md +239 -0
  20. chancel-0.1.0/PRPs/ai_docs/fastembed.md +210 -0
  21. chancel-0.1.0/PRPs/ai_docs/provider-apis.md +295 -0
  22. chancel-0.1.0/PRPs/ai_docs/pypi-trusted-publishing.md +188 -0
  23. chancel-0.1.0/PRPs/ai_docs/qdrant-client.md +296 -0
  24. chancel-0.1.0/PRPs/ai_docs/qdrant-multitenancy.md +183 -0
  25. chancel-0.1.0/PRPs/prp-12-scope-isolation.md +76 -0
  26. chancel-0.1.0/README.md +121 -0
  27. chancel-0.1.0/SECURITY.md +11 -0
  28. chancel-0.1.0/data/README.md +3 -0
  29. chancel-0.1.0/decisions.md +102 -0
  30. chancel-0.1.0/docs/adding-a-provider.md +121 -0
  31. chancel-0.1.0/docs/adding-a-store.md +69 -0
  32. chancel-0.1.0/docs/adr/0001-name-and-enforcement-boundary.md +53 -0
  33. chancel-0.1.0/docs/adr/0002-equivalent-mutants-in-policy.md +109 -0
  34. chancel-0.1.0/docs/adr/0003-collection-per-space-departs-from-vendor-default.md +54 -0
  35. chancel-0.1.0/docs/adr/0004-audit-hash-chain.md +37 -0
  36. chancel-0.1.0/docs/architecture.md +147 -0
  37. chancel-0.1.0/docs/index.md +45 -0
  38. chancel-0.1.0/docs/threat-model.md +85 -0
  39. chancel-0.1.0/docs/why.md +82 -0
  40. chancel-0.1.0/justfile +27 -0
  41. chancel-0.1.0/mkdocs.yml +52 -0
  42. chancel-0.1.0/pyproject.toml +97 -0
  43. chancel-0.1.0/scripts/generate_corpus.py +22 -0
  44. chancel-0.1.0/src/chancel/__init__.py +46 -0
  45. chancel-0.1.0/src/chancel/agent.py +165 -0
  46. chancel-0.1.0/src/chancel/audit.py +177 -0
  47. chancel-0.1.0/src/chancel/cli.py +361 -0
  48. chancel-0.1.0/src/chancel/corpus.py +698 -0
  49. chancel-0.1.0/src/chancel/demo.py +471 -0
  50. chancel-0.1.0/src/chancel/embedders/__init__.py +3 -0
  51. chancel-0.1.0/src/chancel/embedders/base.py +26 -0
  52. chancel-0.1.0/src/chancel/embedders/fastembed_local.py +55 -0
  53. chancel-0.1.0/src/chancel/embedders/hash_stub.py +76 -0
  54. chancel-0.1.0/src/chancel/embedders/openai_compat.py +63 -0
  55. chancel-0.1.0/src/chancel/exceptions.py +47 -0
  56. chancel-0.1.0/src/chancel/ingest.py +47 -0
  57. chancel-0.1.0/src/chancel/memory.py +90 -0
  58. chancel-0.1.0/src/chancel/model.py +117 -0
  59. chancel-0.1.0/src/chancel/policy.py +141 -0
  60. chancel-0.1.0/src/chancel/providers/__init__.py +10 -0
  61. chancel-0.1.0/src/chancel/providers/anthropic.py +119 -0
  62. chancel-0.1.0/src/chancel/providers/base.py +113 -0
  63. chancel-0.1.0/src/chancel/providers/echo.py +61 -0
  64. chancel-0.1.0/src/chancel/providers/hostile_echo.py +277 -0
  65. chancel-0.1.0/src/chancel/providers/openai_compat.py +132 -0
  66. chancel-0.1.0/src/chancel/py.typed +0 -0
  67. chancel-0.1.0/src/chancel/registry.py +95 -0
  68. chancel-0.1.0/src/chancel/retriever.py +95 -0
  69. chancel-0.1.0/src/chancel/stores/__init__.py +9 -0
  70. chancel-0.1.0/src/chancel/stores/_common.py +55 -0
  71. chancel-0.1.0/src/chancel/stores/base.py +171 -0
  72. chancel-0.1.0/src/chancel/stores/filtered.py +97 -0
  73. chancel-0.1.0/src/chancel/stores/inmemory.py +99 -0
  74. chancel-0.1.0/src/chancel/stores/isolated.py +84 -0
  75. chancel-0.1.0/src/chancel/stores/qdrant.py +169 -0
  76. chancel-0.1.0/src/chancel/stores/shared.py +93 -0
  77. chancel-0.1.0/tests/__init__.py +0 -0
  78. chancel-0.1.0/tests/cassettes/.gitkeep +0 -0
  79. chancel-0.1.0/tests/cassettes/anthropic/final_text_response.json +15 -0
  80. chancel-0.1.0/tests/cassettes/anthropic/tool_use_response.json +17 -0
  81. chancel-0.1.0/tests/cassettes/openai/final_response.json +16 -0
  82. chancel-0.1.0/tests/cassettes/openai/malformed_arguments_response.json +25 -0
  83. chancel-0.1.0/tests/cassettes/openai/tool_calls_response.json +25 -0
  84. chancel-0.1.0/tests/conformance/.gitkeep +0 -0
  85. chancel-0.1.0/tests/conformance/_contracts.py +309 -0
  86. chancel-0.1.0/tests/conformance/registry_conformance.py +58 -0
  87. chancel-0.1.0/tests/conformance/test_chatmodel_conformance.py +169 -0
  88. chancel-0.1.0/tests/conformance/test_embedder_conformance.py +123 -0
  89. chancel-0.1.0/tests/conformance/test_mode_conformance.py +138 -0
  90. chancel-0.1.0/tests/conformance/test_new_adapter_is_drop_in.py +99 -0
  91. chancel-0.1.0/tests/conformance/test_store_conformance.py +85 -0
  92. chancel-0.1.0/tests/conftest.py +15 -0
  93. chancel-0.1.0/tests/e2e/.gitkeep +0 -0
  94. chancel-0.1.0/tests/e2e/test_demo_matrix.py +74 -0
  95. chancel-0.1.0/tests/integration/.gitkeep +0 -0
  96. chancel-0.1.0/tests/integration/test_qdrant_store.py +115 -0
  97. chancel-0.1.0/tests/leaks/.gitkeep +0 -0
  98. chancel-0.1.0/tests/leaks/_probes.py +165 -0
  99. chancel-0.1.0/tests/leaks/test_finding1_canary_leak.py +110 -0
  100. chancel-0.1.0/tests/leaks/test_finding2_sparse_idf.py +145 -0
  101. chancel-0.1.0/tests/leaks/test_finding3_unrepresentable_call.py +112 -0
  102. chancel-0.1.0/tests/leaks/test_finding4_deletion_verifiability.py +80 -0
  103. chancel-0.1.0/tests/leaks/test_finding5_hostile_matrix.py +156 -0
  104. chancel-0.1.0/tests/leaks/test_finding6_gate_fails_closed.py +120 -0
  105. chancel-0.1.0/tests/leaks/test_finding7_promotion_gate.py +94 -0
  106. chancel-0.1.0/tests/mutmut_allowlist.txt +11 -0
  107. chancel-0.1.0/tests/unit/.gitkeep +0 -0
  108. chancel-0.1.0/tests/unit/test_agent_loop.py +157 -0
  109. chancel-0.1.0/tests/unit/test_audit.py +340 -0
  110. chancel-0.1.0/tests/unit/test_base_install.py +36 -0
  111. chancel-0.1.0/tests/unit/test_cli.py +161 -0
  112. chancel-0.1.0/tests/unit/test_embedder_fastembed_local.py +35 -0
  113. chancel-0.1.0/tests/unit/test_embedder_openai_compat.py +54 -0
  114. chancel-0.1.0/tests/unit/test_hostile_echo.py +333 -0
  115. chancel-0.1.0/tests/unit/test_inmemory_store.py +111 -0
  116. chancel-0.1.0/tests/unit/test_layouts.py +85 -0
  117. chancel-0.1.0/tests/unit/test_model.py +134 -0
  118. chancel-0.1.0/tests/unit/test_policy.py +467 -0
  119. chancel-0.1.0/tests/unit/test_provider_anthropic.py +149 -0
  120. chancel-0.1.0/tests/unit/test_provider_openai.py +139 -0
  121. chancel-0.1.0/tests/unit/test_registry_providers.py +69 -0
  122. chancel-0.1.0/tests/unit/test_retriever.py +87 -0
  123. chancel-0.1.0/uv.lock +2656 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "image": "mcr.microsoft.com/devcontainers/python:3.12",
3
+ "features": {
4
+ "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}
5
+ },
6
+ "postCreateCommand": "uv sync --all-extras",
7
+ "customizations": {
8
+ "vscode": {
9
+ "extensions": ["charliermarsh.ruff"]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ # Provider selection. Defaults shown; all optional — the base install runs offline.
2
+ CHANCEL_PROVIDER=echo # echo | hostile_echo | anthropic | openai_compat
3
+ CHANCEL_MODEL= # e.g. claude-sonnet-5 or gpt-5-mini; resolved per provider
4
+ CHANCEL_BASE_URL= # OpenAI-compatible endpoint override (Groq/Together/OpenRouter/vLLM/Ollama)
5
+ # Keys are read only by their own adapter. Never committed; gitleaks runs pre-commit.
6
+ ANTHROPIC_API_KEY=
7
+ OPENAI_API_KEY=
@@ -0,0 +1,3 @@
1
+ # These two paths are load-bearing: the security gate and the proof it works.
2
+ /src/chancel/policy.py @frankTurtle
3
+ /tests/leaks/ @frankTurtle
@@ -0,0 +1,32 @@
1
+ name: Adapter request
2
+ description: Request support for a new provider or vector store
3
+ title: "[Adapter] "
4
+ labels: ["adapter-request"]
5
+ body:
6
+ - type: input
7
+ id: provider_name
8
+ attributes:
9
+ label: Provider or store name
10
+ description: "e.g., Groq, Together, Milvus, Pinecone"
11
+ placeholder: "Groq"
12
+ validations:
13
+ required: true
14
+ - type: input
15
+ id: docs_url
16
+ attributes:
17
+ label: Docs URL
18
+ description: Link to official documentation or API reference
19
+ placeholder: "https://..."
20
+ validations:
21
+ required: true
22
+ - type: textarea
23
+ id: why
24
+ attributes:
25
+ label: Why
26
+ description: Why would this adapter be valuable for chancel users?
27
+ validations:
28
+ required: true
29
+ - type: markdown
30
+ attributes:
31
+ value: |
32
+ **Note:** New adapters must pass `tests/conformance/` unmodified. Check the conformance suite before opening a request.
@@ -0,0 +1,38 @@
1
+ name: Bug report
2
+ description: Report a bug in chancel
3
+ title: "[Bug] "
4
+ labels: ["bug"]
5
+ body:
6
+ - type: textarea
7
+ id: what_happened
8
+ attributes:
9
+ label: What happened
10
+ description: Describe the unexpected behavior
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: expected
15
+ attributes:
16
+ label: Expected
17
+ description: What should have happened instead
18
+ validations:
19
+ required: true
20
+ - type: input
21
+ id: backend_mode
22
+ attributes:
23
+ label: Backend mode
24
+ description: "isolated, filtered, or shared"
25
+ placeholder: "isolated"
26
+ - type: input
27
+ id: provider_adapter
28
+ attributes:
29
+ label: Provider adapter
30
+ description: "e.g., anthropic, openai_compat, echo"
31
+ placeholder: "echo"
32
+ - type: checkboxes
33
+ id: confirmation
34
+ attributes:
35
+ label: Confirmation
36
+ options:
37
+ - label: I confirm no real client data appears in this report
38
+ required: true
@@ -0,0 +1,12 @@
1
+ ## What changed
2
+
3
+ ## Which invariant does this touch?
4
+ (name it or state none)
5
+
6
+ ## Which tests prove it?
7
+
8
+ ## Checklist
9
+
10
+ - [ ] /simplify was run on this diff
11
+ - [ ] No real data introduced (synthetic only)
12
+ - [ ] Conventional Commit title
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ - package-ecosystem: "github-actions"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "weekly"
@@ -0,0 +1,196 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main, develop]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ lint:
18
+ name: Lint
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: astral-sh/setup-uv@v6
23
+ with:
24
+ enable-cache: true
25
+ - name: Sync dependencies
26
+ run: uv sync
27
+ - name: Run ruff check
28
+ run: uv run ruff check src tests
29
+ - name: Run ruff format check
30
+ run: uv run ruff format --check src tests
31
+ - name: Validate PR title (Conventional Commits)
32
+ if: github.event_name == 'pull_request'
33
+ run: |
34
+ PR_TITLE="${{ github.event.pull_request.title }}"
35
+ if ! [[ "$PR_TITLE" =~ ^(feat|fix|docs|test|chore|refactor|perf|build|ci|style|revert)(\(.+\))?!?:\ .+ ]]; then
36
+ echo "PR title does not follow Conventional Commits format"
37
+ echo "Expected format: <type>(<scope>)?: <description>"
38
+ echo "Got: $PR_TITLE"
39
+ exit 1
40
+ fi
41
+
42
+ types:
43
+ name: Type Check
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v4
47
+ - uses: astral-sh/setup-uv@v6
48
+ with:
49
+ enable-cache: true
50
+ - name: Sync dependencies with extras
51
+ run: uv sync --extra qdrant --extra anthropic --extra openai
52
+ - name: Run mypy
53
+ run: uv run mypy src/
54
+
55
+ abstraction-guard:
56
+ name: Abstraction Guard
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: astral-sh/setup-uv@v6
61
+ with:
62
+ enable-cache: true
63
+ - name: Check provider imports
64
+ run: |
65
+ echo "Checking for provider-specific imports outside adapter boundaries..."
66
+ if grep -rniE 'anthropic|openai|groq|ollama' src/chancel --include='*.py' -l | grep -vE 'providers/|embedders/|registry.py'; then
67
+ echo "FAIL: Found provider-specific imports in non-adapter code"
68
+ exit 1
69
+ fi
70
+ echo "✓ No provider imports in core logic"
71
+ - name: Check vector store imports
72
+ run: |
73
+ echo "Checking for vector store imports outside stores/qdrant.py..."
74
+ if grep -rn 'import qdrant' src/chancel | grep -v stores/qdrant.py; then
75
+ echo "FAIL: Found qdrant imports outside stores/qdrant.py"
76
+ exit 1
77
+ fi
78
+ echo "✓ Qdrant imports properly isolated"
79
+
80
+ test:
81
+ name: Test (Python ${{ matrix.python-version }}, ${{ matrix.install }})
82
+ runs-on: ubuntu-latest
83
+ strategy:
84
+ matrix:
85
+ python-version: ['3.12', '3.13']
86
+ install: ['base', 'extras']
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ - uses: astral-sh/setup-uv@v6
90
+ with:
91
+ python-version: ${{ matrix.python-version }}
92
+ enable-cache: true
93
+ - name: Sync dependencies (base)
94
+ if: matrix.install == 'base'
95
+ run: uv sync
96
+ - name: Run offline tiers (base install, no extras)
97
+ if: matrix.install == 'base'
98
+ # No coverage gate here: with no optional adapters installed, the
99
+ # provider/qdrant/fastembed code paths are unexercised by design, so a
100
+ # whole-package threshold cannot be met. This tier only proves the
101
+ # offline suite is green from a bare install. The 90% gate lives on the
102
+ # extras tier below, which does exercise every path.
103
+ run: uv run pytest tests/unit tests/conformance -q
104
+ - name: Sync dependencies (extras)
105
+ if: matrix.install == 'extras'
106
+ run: uv sync --extra qdrant --extra anthropic --extra openai
107
+ - name: Run full test suite with coverage gate
108
+ if: matrix.install == 'extras'
109
+ run: uv run pytest tests/unit tests/integration tests/conformance tests/leaks tests/e2e --cov=chancel --cov-report=term-missing --cov-fail-under=90
110
+ - name: Verify policy.py has 100% coverage
111
+ if: matrix.install == 'extras'
112
+ # --cov-fail-under against the single module fails the step if the gate
113
+ # is ever below 100%, with no output parsing to drift.
114
+ run: uv run pytest tests/unit tests/integration tests/conformance tests/leaks tests/e2e --cov=chancel.policy --cov-fail-under=100 -q
115
+
116
+ leaks:
117
+ name: Leak Suite
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+ - uses: astral-sh/setup-uv@v6
122
+ with:
123
+ enable-cache: true
124
+ - name: Sync dependencies
125
+ run: uv sync --extra qdrant
126
+ - name: Run leak and e2e tests
127
+ run: uv run pytest tests/leaks tests/e2e -v
128
+ - name: Note - leaks run on fork PRs (no secrets)
129
+ run: echo "Leak suite runs with offline echo/hostile_echo backends; no API keys required"
130
+
131
+ mutate:
132
+ name: Mutation Testing
133
+ runs-on: ubuntu-latest
134
+ steps:
135
+ - uses: actions/checkout@v4
136
+ - uses: astral-sh/setup-uv@v6
137
+ with:
138
+ enable-cache: true
139
+ - name: Sync dependencies
140
+ run: uv sync
141
+ - name: Run mutmut
142
+ run: uv run mutmut run
143
+ - name: Check survivors against the equivalence allow-list
144
+ run: |
145
+ # The set of surviving mutants must equal tests/mutmut_allowlist.txt
146
+ # EXACTLY. A new survivor is a real coverage gap and fails the build;
147
+ # a stale waiver (an allow-listed mutant that no longer survives) also
148
+ # fails, so the allow-list can never silently rot. Each allowed id is
149
+ # justified in docs/adr/0002-equivalent-mutants-in-policy.md.
150
+ uv run mutmut results 2>/dev/null \
151
+ | grep 'survived' | sed 's/: survived//; s/^[[:space:]]*//' \
152
+ | sort > /tmp/survivors.txt
153
+ grep -vE '^[[:space:]]*(#|$)' tests/mutmut_allowlist.txt | sort > /tmp/allowed.txt
154
+ echo "--- surviving mutants ---"; cat /tmp/survivors.txt
155
+ echo "--- allow-listed (see ADR 0002) ---"; cat /tmp/allowed.txt
156
+ if ! diff -q /tmp/survivors.txt /tmp/allowed.txt >/dev/null; then
157
+ echo "FAIL: surviving mutants do not match the allow-list."
158
+ echo "New survivors (kill them or justify in ADR 0002):"
159
+ comm -23 /tmp/survivors.txt /tmp/allowed.txt || true
160
+ echo "Stale waivers (remove from tests/mutmut_allowlist.txt):"
161
+ comm -13 /tmp/survivors.txt /tmp/allowed.txt || true
162
+ exit 1
163
+ fi
164
+ echo "✓ Survivors match the documented equivalence allow-list exactly."
165
+
166
+ demo:
167
+ name: Demo Matrix
168
+ runs-on: ubuntu-latest
169
+ steps:
170
+ - uses: actions/checkout@v4
171
+ - uses: astral-sh/setup-uv@v6
172
+ with:
173
+ enable-cache: true
174
+ - name: Sync dependencies
175
+ run: uv sync
176
+ - name: Run demo (GitHub format)
177
+ run: uv run chancel demo --no-llm --format github >> $GITHUB_STEP_SUMMARY
178
+ - name: Run demo (text format)
179
+ run: uv run chancel demo --no-llm
180
+ - name: Verify demo exit code
181
+ run: uv run chancel demo --no-llm && echo "✓ Demo passed"
182
+
183
+ security:
184
+ name: Security Audit
185
+ runs-on: ubuntu-latest
186
+ steps:
187
+ - uses: actions/checkout@v4
188
+ - uses: astral-sh/setup-uv@v6
189
+ with:
190
+ enable-cache: true
191
+ - name: Sync dependencies
192
+ run: uv sync
193
+ - name: Run pip-audit
194
+ run: uv run pip-audit || true
195
+ # Allow to fail on transitive dev deps; real vulns in direct deps will block CI.
196
+ # Use --ignore-vuln to suppress known safe transitive vulnerabilities with justification.
@@ -0,0 +1,29 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
10
+
11
+ permissions:
12
+ security-events: write
13
+ contents: read
14
+ actions: read
15
+
16
+ jobs:
17
+ analyze:
18
+ name: Analyze
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - name: Initialize CodeQL
23
+ uses: github/codeql-action/init@v3
24
+ with:
25
+ languages: 'python'
26
+ - name: Autobuild
27
+ uses: github/codeql-action/autobuild@v3
28
+ - name: Perform CodeQL Analysis
29
+ uses: github/codeql-action/analyze@v3
@@ -0,0 +1,44 @@
1
+ name: Docs
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - 'docs/**'
7
+ - 'mkdocs.yml'
8
+ push:
9
+ branches: [main]
10
+ paths:
11
+ - 'docs/**'
12
+ - 'mkdocs.yml'
13
+
14
+ permissions:
15
+ contents: write
16
+
17
+ jobs:
18
+ build:
19
+ name: Build Docs
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v6
24
+ with:
25
+ enable-cache: true
26
+ - name: Sync dependencies with docs group
27
+ run: uv sync --group docs
28
+ - name: Build docs (strict mode)
29
+ run: uv run mkdocs build --strict
30
+
31
+ deploy:
32
+ name: Deploy to GitHub Pages
33
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: astral-sh/setup-uv@v6
39
+ with:
40
+ enable-cache: true
41
+ - name: Sync dependencies with docs group
42
+ run: uv sync --group docs
43
+ - name: Deploy docs to GitHub Pages
44
+ run: uv run mkdocs gh-deploy --force
@@ -0,0 +1,55 @@
1
+ name: Nightly Live Provider Testing
2
+
3
+ on:
4
+ schedule:
5
+ # Run nightly at 2 AM UTC
6
+ - cron: '0 2 * * *'
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ issues: write
12
+
13
+ jobs:
14
+ test-live:
15
+ name: Test Against Live Providers
16
+ # Only run on the main repo, not on forks
17
+ if: github.repository == 'Nobel-Co/chancel'
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v6
22
+ with:
23
+ enable-cache: true
24
+ - name: Sync dependencies with provider extras
25
+ run: uv sync --extra anthropic --extra openai
26
+ - name: Run demo against live providers (isolated backend)
27
+ env:
28
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
29
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
30
+ run: uv run chancel demo --provider anthropic --provider openai_compat --backend isolated
31
+ - name: Run demo against live providers (filtered backend)
32
+ env:
33
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
34
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
35
+ run: uv run chancel demo --provider anthropic --provider openai_compat --backend filtered
36
+ - name: Run demo against live providers (shared backend)
37
+ env:
38
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
39
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
40
+ run: uv run chancel demo --provider anthropic --provider openai_compat --backend shared
41
+ - name: Report failure
42
+ if: failure()
43
+ uses: actions/github-script@v7
44
+ with:
45
+ script: |
46
+ const now = new Date().toISOString().split('T')[0];
47
+ github.rest.issues.create({
48
+ owner: context.repo.owner,
49
+ repo: context.repo.repo,
50
+ title: `nightly-live failed on ${now}`,
51
+ body: `The nightly live provider testing workflow failed. Check the run: ${context.server_url}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
52
+ labels: ['bug', 'ci']
53
+ });
54
+ - name: Note - catches provider API changes
55
+ run: echo "This job catches breaking changes in provider tool-call shapes and API contracts"
@@ -0,0 +1,115 @@
1
+ # Release workflow for PyPI Trusted Publishing via OIDC
2
+ # See PRPs/ai_docs/pypi-trusted-publishing.md for configuration details
3
+ # PyPI pending publisher config: owner Nobel-Co, repo chancel, workflow release.yml, environment pypi
4
+
5
+ name: Release
6
+
7
+ on:
8
+ push:
9
+ tags:
10
+ - 'v*'
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ name: Build
18
+ runs-on: ubuntu-latest
19
+ outputs:
20
+ version: ${{ steps.version.outputs.version }}
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v6
24
+ with:
25
+ enable-cache: true
26
+ - name: Extract version from tag
27
+ id: version
28
+ run: |
29
+ TAG="${GITHUB_REF_NAME}"
30
+ VERSION="${TAG#v}"
31
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
32
+ echo "tag=$TAG" >> $GITHUB_OUTPUT
33
+ - name: Verify tag matches pyproject.toml version
34
+ run: |
35
+ PROJ_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
36
+ TAG_VERSION="${{ steps.version.outputs.version }}"
37
+ if [ "$PROJ_VERSION" != "$TAG_VERSION" ]; then
38
+ echo "FAIL: Tag version ($TAG_VERSION) does not match pyproject.toml version ($PROJ_VERSION)"
39
+ exit 1
40
+ fi
41
+ echo "✓ Version match: $PROJ_VERSION"
42
+ - name: Build distributions
43
+ run: uv build
44
+ - name: Verify dist/ contains wheel and sdist
45
+ run: |
46
+ if [ ! -f dist/chancel-*.whl ]; then
47
+ echo "FAIL: No wheel found in dist/"
48
+ exit 1
49
+ fi
50
+ if [ ! -f dist/chancel-*.tar.gz ]; then
51
+ echo "FAIL: No sdist found in dist/"
52
+ exit 1
53
+ fi
54
+ echo "✓ Built distributions:"
55
+ ls -lh dist/
56
+ - name: Generate demo matrix for release notes
57
+ run: uv run chancel demo --no-llm --format github > demo-matrix.md
58
+ - name: Upload distributions artifact
59
+ uses: actions/upload-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+ if-no-files-found: error
64
+ - name: Upload demo matrix artifact
65
+ uses: actions/upload-artifact@v4
66
+ with:
67
+ name: demo-matrix
68
+ path: demo-matrix.md
69
+ if-no-files-found: error
70
+
71
+ pypi-publish:
72
+ name: Publish to PyPI
73
+ needs: build
74
+ runs-on: ubuntu-latest
75
+ environment:
76
+ name: pypi
77
+ url: https://pypi.org/p/chancel
78
+ permissions:
79
+ id-token: write
80
+ steps:
81
+ - name: Download distributions
82
+ uses: actions/download-artifact@v4
83
+ with:
84
+ name: dist
85
+ path: dist/
86
+ - name: Publish to PyPI via Trusted Publishing (OIDC)
87
+ uses: pypa/gh-action-pypi-publish@release/v1
88
+ with:
89
+ packages-dir: dist/
90
+ verbose: true
91
+
92
+ github-release:
93
+ name: Create GitHub Release
94
+ needs: build
95
+ runs-on: ubuntu-latest
96
+ permissions:
97
+ contents: write
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ - name: Download distributions
101
+ uses: actions/download-artifact@v4
102
+ with:
103
+ name: dist
104
+ path: dist/
105
+ - name: Download demo matrix
106
+ uses: actions/download-artifact@v4
107
+ with:
108
+ name: demo-matrix
109
+ path: .
110
+ - name: Create GitHub Release
111
+ uses: softprops/action-gh-release@v2
112
+ with:
113
+ files: dist/*
114
+ body_path: demo-matrix.md
115
+ generate_release_notes: true
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .coverage
9
+ coverage.xml
10
+ htmlcov/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ mutants/
14
+ .mutmut-cache
15
+ site/
16
+ .env
17
+ .DS_Store
18
+ out/
19
+
20
+ # Synthetic corpora are generated, never committed; the generator lives in scripts/.
21
+ # Ignore the directory's contents (data/*) rather than the directory itself, so the
22
+ # tracked placeholder below survives -- git cannot re-include a file under an
23
+ # excluded directory.
24
+ data/*
25
+ !data/README.md
@@ -0,0 +1,28 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.16.3
4
+ hooks:
5
+ - id: ruff-check
6
+ - id: ruff-format
7
+ - repo: https://github.com/gitleaks/gitleaks
8
+ rev: v8.24.3
9
+ hooks:
10
+ - id: gitleaks
11
+ - repo: https://github.com/compilerla/conventional-pre-commit
12
+ rev: v4.0.0
13
+ hooks:
14
+ - id: conventional-pre-commit
15
+ stages: [commit-msg]
16
+ - repo: local
17
+ hooks:
18
+ - id: no-ai-attribution
19
+ name: forbid AI attribution trailers in commit message
20
+ language: pygrep
21
+ entry: '(?i)(co-authored-by:.*claude|generated with.*claude|anthropic\.com/claude)'
22
+ stages: [commit-msg]
23
+ - id: cassette-secret-scan
24
+ name: scan cassettes for key-shaped strings
25
+ language: pygrep
26
+ entry: '(sk-[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|Bearer [A-Za-z0-9_.-]{20,})'
27
+ files: ^tests/cassettes/
28
+ default_install_hook_types: [pre-commit, commit-msg]
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project adheres to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-08-19
10
+
11
+ First public release. A demonstration, not a product — see the README's
12
+ out-of-scope list.
13
+
14
+ ### Added
15
+ - `PolicyGate.authorize()` — the provider-neutral wall every retrieval passes
16
+ through; fails closed on unknown scope, malformed input, resolver failure,
17
+ timeout, empty allowlist, and any resolver attempt to widen authority beyond
18
+ the active scope. 100% statement coverage; mutation-tested to two documented
19
+ equivalent survivors.
20
+ - Domain model (`Scope`, `DocumentId`, `ActiveScope`, `RetrievalReceipt`) whose
21
+ types make a cross-space read unrepresentable under the isolated layout.
22
+ - Three store layouts behind one interface: `isolated` (the claim), `filtered`
23
+ (the vendor-default payload filter), and `shared` (prompt-only), with an
24
+ `inmemory` reference implementation and a `qdrant` local-mode adapter.
25
+ - Provider adapters `anthropic`, `openai_compat`, `echo`, and the offline
26
+ adversarial `hostile_echo`; embedders `hash_stub`, `fastembed_local`, and
27
+ `openai_compat`. Nothing above the adapter boundary names a provider.
28
+ - `Retriever` — the sole caller of a store's search, exposing no collection or
29
+ filter parameter — and a neutral `ScopedAgent` loop whose one retrieval tool
30
+ takes a query string only.
31
+ - Append-only audit log with a SHA-256 hash chain; `chancel verify-log` names
32
+ the first divergent line and prints the head hash as an external anchor.
33
+ - Three-tier memory with `promote_fact()`, which refuses any fact whose
34
+ provenance includes a space-scoped id.
35
+ - The leak suite: seven findings asserting the predicted color per backend, and
36
+ `chancel demo` rendering the backend × provider matrix with no API key, no
37
+ Docker, and no model download.
38
+ - CLI: `demo`, `ask`, `attack`, `verify-log`, `gen-corpus`.
39
+
40
+ [Unreleased]: https://github.com/Nobel-Co/chancel/compare/v0.1.0...HEAD
41
+ [0.1.0]: https://github.com/Nobel-Co/chancel/releases/tag/v0.1.0
@@ -0,0 +1,8 @@
1
+ # Contributing
2
+
3
+ - PRs only.
4
+ - Use Conventional Commits.
5
+ - No AI attribution trailers in commit messages.
6
+ - Run `just all` before opening a PR.
7
+ - **Any PR containing real data (client names, real matters, real filings) is rejected — synthetic generated data only.**
8
+ - New adapters must pass `tests/conformance/` without modifying it.