Repository navigation
test(properties): cover sensitivity enforcement (#755 item 6) - #889
Merged
Merged
Conversation
Adds the four properties #755 item 6 asks for, plus a guard-the-guard, over `context.sensitivity.apply_sensitivity_filter`: * the floor is INCLUSIVE -- an item whose sensitivity equals the floor is enforced, not passed; * everything below the floor survives untouched and in input order; * the text of an at-or-above item reaches model-visible output under NEITHER action; * redact keeps the slot, masks the payload, and clears `artifact_ref`. The issue warns specifically against "an inverted floor assumption", so the ladder is restated in the test rather than imported from `_SENSITIVITY_ORDER`. Importing it would make the properties agree with the implementation by construction: reorder the ladder in the source and the tests would reorder with it and still pass. The second property is the counter-property. Without it, an implementation returning `[]` for every input would satisfy "nothing disallowed survives" perfectly. Mutation-checked, each mutation read back before trusting the result: >= floor -> > floor (the inverted floor) 4 failed drop the else that keeps permitted items 2 failed redact appends the original, not the mask 2 failed MaskRedactionHook stops clearing artifact_ref 1 failed stop counting drops 1 failed mask leaks item.text into the placeholder 1 failed permitted items reordered (insert(0, item)) 2 failed The artifact_ref mutation initially PASSED. `ContextItem.artifact_ref` defaults to None, so `assert result.artifact_ref is None` was vacuous until the strategy was taught to generate refs. `find()` now pins that the corpus still reaches that state, so the assertion cannot go vacuous again silently. Every mutation was re-run after that strategy change. CI cost: tests/test_properties.py 7.11s -> 7.59s; the five new tests run in 0.92s alone. Honest limitation, recorded rather than glossed: the existing example suite (tests/test_sensitivity.py, tests/test_sensitivity_fixtures.py) catches all seven mutations too, usually more loudly. `sensitivity.py` was already at 100% line coverage and stays there. These properties add input-space breadth and state the contract as a universally quantified claim; they are not demonstrated to catch a defect the examples miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhJp1Rnr6mF4vdSLkj14jU
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhJp1Rnr6mF4vdSLkj14jU
Contributor
Benchmark delta (vs
|
| size | recall@k (head Δ vs base) | MRR (head Δ vs base) | p99 (ms) |
|---|---|---|---|
| 50 | ✅ 0.5649 (+0.0000) | ✅ 0.4978 (+0.0000) | ✅ 0.439 (base 0.650) |
| 83 | ✅ 0.3825 (+0.0000) | ✅ 0.3242 (+0.0000) | ✅ 0.390 (base 1.205) |
| 1000 | ✅ 0.1475 (+0.0000) | ✅ 0.1456 (+0.0000) | ✅ 19.644 (base 47.855) |
Per-backend × per-size matrix
| backend | size | recall@k (Δ) | MRR (Δ) | p99 (ms) |
|---|---|---|---|---|
| bm25 | 100 | ✅ 0.3825 (+0.0000) | ✅ 0.3399 (+0.0000) | ✅ 3.528 (base 8.399) |
| bm25 | 500 | ✅ 0.2250 (+0.0000) | ✅ 0.2165 (+0.0000) | ✅ 16.300 (base 41.865) |
| bm25 | 1000 | ✅ 0.1575 (+0.0000) | ✅ 0.1525 (+0.0000) | ✅ 46.464 (base 112.387) |
| embedding_hashing | 100 | ✅ 0.5175 (+0.0000) | ✅ 0.4360 (+0.0000) | ✅ 3.970 (base 7.218) |
| embedding_hashing | 500 | ✅ 0.2700 (+0.0000) | ✅ 0.2674 (+0.0000) | ✅ 21.878 (base 43.171) |
| embedding_hashing | 1000 | ✅ 0.2000 (+0.0000) | ✅ 0.1931 (+0.0000) | ✅ 52.524 (base 102.979) |
| embedding_st | 100 | skipped (skipped: missing sentence-transformers) | — | — |
| embedding_st | 500 | skipped (skipped: missing sentence-transformers) | — | — |
| embedding_st | 1000 | skipped (skipped: missing sentence-transformers) | — | — |
| fuzzy | 100 | skipped (skipped: missing rapidfuzz) | — | — |
| fuzzy | 500 | skipped (skipped: missing rapidfuzz) | — | — |
| fuzzy | 1000 | skipped (skipped: missing rapidfuzz) | — | — |
| tfidf | 100 | ✅ 0.3825 (+0.0000) | ✅ 0.3220 (+0.0000) | ✅ 0.599 (base 1.193) |
| tfidf | 500 | ✅ 0.2325 (+0.0000) | ✅ 0.2314 (+0.0000) | ✅ 5.192 (base 10.803) |
| tfidf | 1000 | ✅ 0.1475 (+0.0000) | ✅ 0.1456 (+0.0000) | ✅ 18.595 (base 42.986) |
Context pipeline (per scenario)
| scenario | tokens | dropped | dedup |
|---|---|---|---|
| large_catalog | 1480 (base 1480, Δ+0) | 0 (base 0, Δ+0) | 0 (base 0, Δ+0) |
| long_conversation | 2500 (base 2500, Δ+0) | 0 (base 0, Δ+0) | 0 (base 0, Δ+0) |
| mixed_payload | 488 (base 488, Δ+0) | 0 (base 0, Δ+0) | 0 (base 0, Δ+0) |
| short_conversation | 487 (base 487, Δ+0) | 0 (base 0, Δ+0) | 0 (base 0, Δ+0) |
| stress_conversation | 6590 (base 6590, Δ+0) | 11 (base 11, Δ+0) | 4 (base 4, Δ+0) |
| tiny_payload | 256 (base 256, Δ+0) | 0 (base 0, Δ+0) | 0 (base 0, Δ+0) |
Numbers come from make benchmark / make benchmark-matrix.
Latency is hardware-dependent — treat the markers as a rough guide.
See benchmarks/scorecard.md for the full picture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds property coverage for item 6 of #755's phase-2 list — sensitivity enforcement — over
context.sensitivity.apply_sensitivity_filter. With #887 (items 1, 2, 7) this leaves item 6 done.Part of #755.
Changes
Four properties plus a guard-the-guard, appended to
tests/test_properties.py:redactkeeps the slot, masks the payload, and clearsartifact_ref(Prevent redacted items from exposing original content via artifact handles and drilldown #451);find()-based guard that the corpus still generatesartifact_refs at all.Two deliberate choices
The severity ladder is restated in the test rather than imported from
_SENSITIVITY_ORDER. Importing it would make every property agree with the implementation by construction — reorder the ladder in the source and the tests would reorder with it and still pass.The second property is the counter-property. Without it, an implementation that returned
[]for every input would satisfy "nothing disallowed survives" perfectly. Pinning the survivors is what makes the first property mean anything.The issue warns specifically against "an inverted floor assumption". The semantics are
if item_level >= floor_level:→ enforced, so the assertion on survivors isrank < floor, neverrank <= floor.Notes for reviewers
Mutation results — each mutation read back before trusting it
>= floor→> floor(the inverted floor)elsethat keeps permitted itemsMaskRedactionHookstops clearingartifact_refitem.textinto the placeholderinsert(0, item))The
artifact_refmutation initially PASSED.ContextItem.artifact_refdefaults toNone, soassert result.artifact_ref is Nonewas vacuous — it held whether or not the hook cleared anything. The strategy now generates refs, andfind()pins that it still reaches that state so the assertion cannot quietly go vacuous again. Every mutation was re-run after that strategy change, per the lesson from #887 where tightening one strategy silently un-caught a different mutation.Honest limitation — please weigh this
The existing example suite catches all seven mutations too, usually more loudly. Measured:
test_sensitivity*.pyartifact_refnot clearedsensitivity.pywas already at 100% line coverage and stays there. So these properties add input-space breadth (arbitrary ladder positions × actions × Unicode text × ref presence, against six fixed fixture files) and state the contract as a universally quantified claim — but I could not demonstrate a defect they catch that the examples miss. That is the honest marginal value, and if you judge it insufficient for the CI seconds, closing this is a reasonable call; the mutation table above is the useful artifact either way.CI budget (#755 requires this recorded)
tests/test_properties.py: 7.11s → 7.59s. The five new tests alone: 0.92s. No new dependency — Hypothesis arrived with #887.Two stale things noticed, not fixed here
scripts/check_module_size.py, raised by refactor: reconcile invariants and module-size policy #853); docs: state the module-size limit #853 actually enforces #888 corrected theMakefilecomment andworkflows.mdbut missed the template. Worth a one-line follow-up.make cicannot go green in this environment for reasons unrelated to this change — see below.Checklist
Tests added or updated for every new/changed public function — no public function changed; this is test-only
make cipasses locally — it does not, for two pre-existing environmental reasons, both reproduced on unmodifiedorigin/mainin a separate worktree:tests/test_mcp_serve_cli.py::test_serve_dry_run_writes_catalog_diagnostic_eventandtests/test_benchmark.py::test_matrix_cell_skip_reason_is_none_for_default_backendsboth fail onbc10596with no changes applied. The proxy blocksopenaipublic.blob.core.windows.net, so tiktoken falls back to the heuristic estimator.19146 / 2285 / 5630; only branch-partials move, 597 → 595). The floor failure is a consequence of those two skipped code paths, not of this change.Every other gate passes:
fmt,lint,type,drift-check,module-size-check,doc-snippets-check,readme-version-check,security-policy-check,version-metadata-check,schema-hosting-check— all OK. CI is the authority on the two environmental failures.CHANGELOG.mdupdated under## [Unreleased]Docstrings added for all new public APIs — n/a, no new public API
Public-API change? — none;
api/public_api.txtuntouchedEvery modified module stays within the enforced size limit —
make module-size-checkpasses (the gate is 500; see note above)Related issue linked — [Testing, phase 2] Complete property-based coverage of core invariants #755
Agent-facing docs updated if pipeline, API, or conventions changed — n/a, no pipeline/API/convention change
Reproducibility
Not applicable — this touches no routing, scoring, tokenisation or context-pipeline source. The diff is
tests/test_properties.pyandCHANGELOG.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01XhJp1Rnr6mF4vdSLkj14jU
Generated by Claude Code