zekan 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. zekan-0.1.0/.gitignore +56 -0
  2. zekan-0.1.0/COVERAGE_MAP.md +241 -0
  3. zekan-0.1.0/FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md +94 -0
  4. zekan-0.1.0/LICENSE +21 -0
  5. zekan-0.1.0/METHODOLOGY.md +344 -0
  6. zekan-0.1.0/PKG-INFO +297 -0
  7. zekan-0.1.0/PUBLISH_PREP_FINDINGS.md +441 -0
  8. zekan-0.1.0/README.md +258 -0
  9. zekan-0.1.0/examples/churn_instacart/README.md +6 -0
  10. zekan-0.1.0/examples/churn_instacart/fixture.csv +1501 -0
  11. zekan-0.1.0/examples/churn_instacart/zekan.yml +26 -0
  12. zekan-0.1.0/pyproject.toml +88 -0
  13. zekan-0.1.0/tests/__init__.py +1 -0
  14. zekan-0.1.0/tests/test_ablation_grouped.py +321 -0
  15. zekan-0.1.0/tests/test_ablation_parallel.py +209 -0
  16. zekan-0.1.0/tests/test_across_entity_null.py +372 -0
  17. zekan-0.1.0/tests/test_audit_integration.py +368 -0
  18. zekan-0.1.0/tests/test_benchmark.py +251 -0
  19. zekan-0.1.0/tests/test_bootstrap_ci.py +311 -0
  20. zekan-0.1.0/tests/test_ci_a_opt_in.py +240 -0
  21. zekan-0.1.0/tests/test_cli.py +1172 -0
  22. zekan-0.1.0/tests/test_config.py +81 -0
  23. zekan-0.1.0/tests/test_contract.py +268 -0
  24. zekan-0.1.0/tests/test_data_loading.py +284 -0
  25. zekan-0.1.0/tests/test_deployable_auc_labeling.py +161 -0
  26. zekan-0.1.0/tests/test_diff.py +635 -0
  27. zekan-0.1.0/tests/test_duplicate_probe.py +668 -0
  28. zekan-0.1.0/tests/test_entity_aggregate_probe.py +351 -0
  29. zekan-0.1.0/tests/test_entity_contamination_probe.py +503 -0
  30. zekan-0.1.0/tests/test_estimators.py +83 -0
  31. zekan-0.1.0/tests/test_explain.py +529 -0
  32. zekan-0.1.0/tests/test_f2a_parallel_null.py +348 -0
  33. zekan-0.1.0/tests/test_fast_mode.py +308 -0
  34. zekan-0.1.0/tests/test_fold_transparency.py +422 -0
  35. zekan-0.1.0/tests/test_forbidden_zero_cooccurrence_probe.py +358 -0
  36. zekan-0.1.0/tests/test_html_view.py +619 -0
  37. zekan-0.1.0/tests/test_init.py +431 -0
  38. zekan-0.1.0/tests/test_json_export.py +414 -0
  39. zekan-0.1.0/tests/test_leakage_verdict.py +236 -0
  40. zekan-0.1.0/tests/test_markers.py +152 -0
  41. zekan-0.1.0/tests/test_metrics.py +645 -0
  42. zekan-0.1.0/tests/test_module_invocation.py +38 -0
  43. zekan-0.1.0/tests/test_naive_auc_fold_seed.py +180 -0
  44. zekan-0.1.0/tests/test_near_bijection_probe.py +232 -0
  45. zekan-0.1.0/tests/test_near_bijection_view.py +383 -0
  46. zekan-0.1.0/tests/test_power.py +54 -0
  47. zekan-0.1.0/tests/test_preflight.py +371 -0
  48. zekan-0.1.0/tests/test_provenance.py +519 -0
  49. zekan-0.1.0/tests/test_robustness_gauntlet.py +439 -0
  50. zekan-0.1.0/tests/test_schema.py +338 -0
  51. zekan-0.1.0/tests/test_seed_stability.py +281 -0
  52. zekan-0.1.0/tests/test_severity.py +283 -0
  53. zekan-0.1.0/tests/test_splitter_contract_probe.py +556 -0
  54. zekan-0.1.0/tests/test_splitters.py +372 -0
  55. zekan-0.1.0/tests/test_structural_coverage.py +550 -0
  56. zekan-0.1.0/tests/test_structural_probe_wiring.py +489 -0
  57. zekan-0.1.0/tests/test_terminal_row_presence_probe.py +442 -0
  58. zekan-0.1.0/tests/test_terminal_row_view.py +488 -0
  59. zekan-0.1.0/tests/test_text_view.py +721 -0
  60. zekan-0.1.0/tests/test_tier2_sequential_null.py +545 -0
  61. zekan-0.1.0/tests/test_tier_system.py +376 -0
  62. zekan-0.1.0/tests/test_undeclared_feature_probe.py +458 -0
  63. zekan-0.1.0/tests/test_undeclared_screen_view.py +389 -0
  64. zekan-0.1.0/tests/test_verdict.py +525 -0
  65. zekan-0.1.0/zekan/__init__.py +3 -0
  66. zekan-0.1.0/zekan/__main__.py +9 -0
  67. zekan-0.1.0/zekan/cli.py +1292 -0
  68. zekan-0.1.0/zekan/config/__init__.py +1 -0
  69. zekan-0.1.0/zekan/config/schema.py +135 -0
  70. zekan-0.1.0/zekan/config/validator.py +1 -0
  71. zekan-0.1.0/zekan/contract/__init__.py +1 -0
  72. zekan-0.1.0/zekan/contract/contract_checks.py +465 -0
  73. zekan-0.1.0/zekan/contract/prediction_contract.py +69 -0
  74. zekan-0.1.0/zekan/data_loading.py +156 -0
  75. zekan-0.1.0/zekan/decision/__init__.py +1 -0
  76. zekan-0.1.0/zekan/decision/cost_model.py +1 -0
  77. zekan-0.1.0/zekan/decision/explanations.py +1 -0
  78. zekan-0.1.0/zekan/decision/ranking.py +1 -0
  79. zekan-0.1.0/zekan/decision/trust_score.py +1 -0
  80. zekan-0.1.0/zekan/detectors/__init__.py +1 -0
  81. zekan-0.1.0/zekan/detectors/base.py +1 -0
  82. zekan-0.1.0/zekan/detectors/contract_detector.py +1 -0
  83. zekan-0.1.0/zekan/detectors/deepchecks_adapter.py +1 -0
  84. zekan-0.1.0/zekan/detectors/duplicate_probe.py +337 -0
  85. zekan-0.1.0/zekan/detectors/entity_aggregate_probe.py +169 -0
  86. zekan-0.1.0/zekan/detectors/entity_contamination_risk.py +214 -0
  87. zekan-0.1.0/zekan/detectors/forbidden_zero_cooccurrence_probe.py +157 -0
  88. zekan-0.1.0/zekan/detectors/near_bijection_probe.py +210 -0
  89. zekan-0.1.0/zekan/detectors/schema.py +693 -0
  90. zekan-0.1.0/zekan/detectors/splitter_contract_probe.py +183 -0
  91. zekan-0.1.0/zekan/detectors/static_ast.py +1 -0
  92. zekan-0.1.0/zekan/detectors/terminal_row_presence_probe.py +197 -0
  93. zekan-0.1.0/zekan/detectors/undeclared_feature_probe.py +458 -0
  94. zekan-0.1.0/zekan/init_wizard.py +81 -0
  95. zekan-0.1.0/zekan/reports/__init__.py +1 -0
  96. zekan-0.1.0/zekan/reports/diff.py +219 -0
  97. zekan-0.1.0/zekan/reports/explain.py +373 -0
  98. zekan-0.1.0/zekan/reports/html.py +1 -0
  99. zekan-0.1.0/zekan/reports/html_view.py +693 -0
  100. zekan-0.1.0/zekan/reports/json_export.py +135 -0
  101. zekan-0.1.0/zekan/reports/markdown.py +1 -0
  102. zekan-0.1.0/zekan/reports/markers.py +83 -0
  103. zekan-0.1.0/zekan/reports/messages.py +323 -0
  104. zekan-0.1.0/zekan/reports/preflight.py +115 -0
  105. zekan-0.1.0/zekan/reports/provenance.py +219 -0
  106. zekan-0.1.0/zekan/reports/text_view.py +711 -0
  107. zekan-0.1.0/zekan/security/__init__.py +1 -0
  108. zekan-0.1.0/zekan/security/secrets_scan.py +1 -0
  109. zekan-0.1.0/zekan/severity/__init__.py +1 -0
  110. zekan-0.1.0/zekan/severity/ablation.py +443 -0
  111. zekan-0.1.0/zekan/severity/audit.py +759 -0
  112. zekan-0.1.0/zekan/severity/bootstrap_ci.py +221 -0
  113. zekan-0.1.0/zekan/severity/engine.py +908 -0
  114. zekan-0.1.0/zekan/severity/estimators.py +62 -0
  115. zekan-0.1.0/zekan/severity/metrics.py +728 -0
  116. zekan-0.1.0/zekan/severity/null_baseline.py +1339 -0
  117. zekan-0.1.0/zekan/severity/power.py +42 -0
  118. zekan-0.1.0/zekan/severity/splitters.py +287 -0
  119. zekan-0.1.0/zekan/severity/verdict.py +1350 -0
zekan-0.1.0/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+ .installed.cfg
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.whl
20
+
21
+ # Virtual environments
22
+ venv/
23
+ .venv/
24
+ env/
25
+ .env/
26
+ ENV/
27
+
28
+ # Zekan cache
29
+ .zekan/
30
+
31
+ # Testing
32
+ .pytest_cache/
33
+ .coverage
34
+ htmlcov/
35
+ .tox/
36
+
37
+ # Type checking
38
+ .mypy_cache/
39
+ .dmypy.json
40
+ dmypy.json
41
+
42
+ # Ruff
43
+ .ruff_cache/
44
+
45
+ # IDE
46
+ .vscode/
47
+ .idea/
48
+ *.swp
49
+ *.swo
50
+
51
+ # OS
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # Local scratch artifacts
56
+ scratch/
@@ -0,0 +1,241 @@
1
+ # Coverage Map
2
+
3
+ What Zekan checks, what it does not, and where the boundaries of what it
4
+ does check are known to sit. This is not a list of apologies — it is the
5
+ thing a tool that measures trust has no business hiding. Every claim below
6
+ is sourced to committed code or a committed calibration/validation
7
+ document.
8
+
9
+ ---
10
+
11
+ ## 1. What runs in a real audit
12
+
13
+ Every `zekan audit` invocation runs two independent layers:
14
+
15
+ ### The A/B/C severity engine
16
+
17
+ Measures `fixable_leakage` from columns you declared in
18
+ `forbidden_after_prediction`, with a permutation-null detection gate. See
19
+ [`METHODOLOGY.md`](METHODOLOGY.md) for the full definition. This is the
20
+ only layer that can change the verdict (`PASS`/`NOTE`/`WARN`/`FAIL`/
21
+ `UNCONFIRMED_HIGH_DAMAGE`).
22
+
23
+ ### Nine structural probes (annotate-only — never change the verdict)
24
+
25
+ Registered in `zekan/severity/audit.py::_build_probe_registry`, run on
26
+ every audit alongside the engine:
27
+
28
+ | probe | catches | annotate-only |
29
+ |---|---|---|
30
+ | `probe_forbidden_entity_level_aggregate` | a forbidden column that is constant *within* each entity — a value the model could learn as an entity ID in disguise | yes |
31
+ | `probe_raw_duplicates` | exact-duplicate rows anywhere in the dataset | yes |
32
+ | `probe_cross_fold_duplicates` | a test-fold row that is byte-identical to a training-fold row | yes |
33
+ | `probe_undeclared_feature_screen` (Upgrade 1) | non-forbidden features with a high standalone (univariate, temporal-fold) association with the target — a ranked candidate list, not a verdict | yes |
34
+ | `probe_near_bijection` (Upgrade H) | a non-forbidden column whose values determine the target almost exactly (Theil's U ≥ 0.99) — a near-perfect value-to-label partition, the shape of an ID leaking the answer | yes |
35
+ | `probe_terminal_row_presence` | a non-forbidden column populated *only* on an entity's last observed row — the shape of a post-outcome field (settlement amount, disposition code) nobody declared forbidden | yes |
36
+ | `probe_entity_contamination_risk` | whether the same entity recurs across more than one `prediction_time` period — the structural condition under which a *random* (non-grouped) split would leak that entity into both train and test | yes (`confirmed=False`, advisory) |
37
+ | `probe_splitter_contract_violation` | an internal self-check: verifies Zekan's own random-grouped splitter never placed the same entity in both train and test of a fold. If it fires, the bug is in Zekan, not your data | yes (internal integrity check, not a data finding) |
38
+ | `probe_forbidden_zero_cooccurrence` | a *declared forbidden* column with zero non-missing rows where the target is positive — the declared leak is structurally incapable of having contributed to the measured `fixable_leakage`, whatever number came back. See [`FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md`](FORBIDDEN_ZERO_COOCCURRENCE_CHECK.md) | yes |
39
+
40
+ None of these nine can turn a clean `fl` into a WARN/FAIL. Six are
41
+ target-free by design (`probe_forbidden_entity_level_aggregate`,
42
+ `probe_raw_duplicates`, `probe_cross_fold_duplicates`,
43
+ `probe_terminal_row_presence`, `probe_entity_contamination_risk`,
44
+ `probe_splitter_contract_violation`); three read the target because that is
45
+ exactly what they measure — `probe_undeclared_feature_screen`'s univariate
46
+ association and `probe_near_bijection`'s value-to-label determinism are
47
+ both, by definition, feature-vs-target relationships;
48
+ `probe_forbidden_zero_cooccurrence` reads the target's own value (positive
49
+ vs. negative row) rather than any feature-vs-target statistical
50
+ relationship — a deductive non-missing-count fact, not a model score.
51
+ `probe_splitter_contract_violation`
52
+ reads neither the target nor forbidden-column content — it checks only
53
+ entity_id positions across the random-grouped fold indices Zekan itself
54
+ built. Every finding from these probes appears under
55
+ `structural_annotations` in the JSON export and in a dedicated section of
56
+ both human-readable formatters, never folded silently into the headline
57
+ verdict.
58
+
59
+ **A real, near-universal consequence worth stating plainly**:
60
+ `probe_entity_contamination_risk` will fire `WARN` on almost every dataset
61
+ Zekan is designed for — its trigger condition (an entity recurring across
62
+ more than one period) is the same "recurring over time" structure this
63
+ README's own fit criteria require. Measured directly against the Freddie
64
+ Mac Frame C data: 1,902 of 1,905 entities (99.8%) trigger it. This is not
65
+ noise or miscalibration; it is the advisory correctly describing the
66
+ structural risk of ever running a *non-grouped* split on this shape of
67
+ data, restated on every audit because the risk is present on every audit
68
+ of this shape of data.
69
+
70
+ **`structural_coverage_complete`** (a top-level JSON field, and a caveat
71
+ line in both formatters when false) tracks whether every probe that was
72
+ *supposed* to run actually did — a probe that crashed internally, or
73
+ that needed temporal folds the data couldn't supply, sets this false and
74
+ lists why, rather than letting a clean-looking PASS pass over an
75
+ unchecked category silently.
76
+
77
+ ---
78
+
79
+ ## 2. A masking defect this document caused to be found, and its fix
80
+
81
+ Writing §1 above (first published `f3a7ffe`) surfaced a real defect:
82
+ `probe_entity_contamination_risk` and `probe_splitter_contract_violation`
83
+ were complete, individually-tested modules, **not** registered in
84
+ `_build_probe_registry()` — no real `zekan audit` run had ever called
85
+ either. `structural_coverage_complete` stayed `True` throughout, because
86
+ that field's computation only ever considers registered probes; an
87
+ unregistered probe is invisible to it by construction, not merely
88
+ untested. Full record, including why this is a worse defect than the
89
+ structural-coverage-caveat fix (§1's `structural_coverage_complete`
90
+ mechanism itself), how it was found (writing this document, not a test),
91
+ and why it does not change any conclusion in the Freddie Mac validation
92
+ study: `DATASET2_ADDENDUM_15_UNREGISTERED_STRUCTURAL_PROBES.md`.
93
+
94
+ **Fixed.** Both probes are now registered (§1's table). Wiring
95
+ `probe_splitter_contract_violation` required more than adding a registry
96
+ line: its own signature (`folds` first, ahead of `df`/`contract`) had
97
+ drifted from the registry's calling convention, and — found only by
98
+ measuring, not by reading the signature alone — the probe validates the
99
+ *random-grouped* splitter's contract specifically (no entity ever appears
100
+ in both train and test), a property the *temporal* splitter deliberately
101
+ does **not** uphold (`temporal_expanding_folds`'s own docstring: entity
102
+ recurrence across train/test "is allowed... this is not leakage"). Giving
103
+ it the temporal folds `_run_structural_probes` already had available
104
+ would have made it report `internal_fail` — "every evaluation result from
105
+ this run is unreliable" — on essentially every real audit of the
106
+ recurring-entity data Zekan is built for. The fix threads the
107
+ random-grouped folds through as a distinct, additive capability
108
+ (`SeverityResult.random_folds`, `_ProbeSpec.needs_random_folds`) rather
109
+ than reusing the temporal ones. Addendum 15 §2 and §4 have the full
110
+ before/after reasoning and the direct measurement
111
+ (`entity_overlap_count`: 100/100 entities under temporal folds, 0/0 under
112
+ random-grouped, on the same panel).
113
+
114
+ **Regression guard added**: a test walks `zekan/detectors/` dynamically
115
+ (`pkgutil`, not a hand-maintained module list — a hand-maintained list is
116
+ how this defect happened) and fails if any `probe_*` function exists
117
+ without a matching entry in `_build_probe_registry()`
118
+ (`tests/test_structural_probe_wiring.py::test_every_implemented_probe_module_is_registered`).
119
+
120
+ ## 3. Declared in the schema, never implemented
121
+
122
+ `zekan/detectors/schema.py`'s `IssueType` enum reserves a few categories
123
+ that carry a full coverage-classification entry but no implementing probe
124
+ anywhere in the codebase: `ENTITY_CONTAMINATION` (the *confirmed*,
125
+ cross-fold-observed variant — distinct from the risk-advisory probe in
126
+ §2), `WRONG_SPLIT_STRATEGY`, `SUSPECTED_UNDECLARED_LEAK` (Upgrade 1's
127
+ retired lower-confidence tier — killed on calibration grounds, see
128
+ `UPGRADE1_CALIBRATION.md`), and `CODE_STRUCTURAL_LEAK`, which the schema
129
+ itself labels `SourceLayer.OUT_OF_SCOPE`. These are planned scope
130
+ markers, not silent gaps — the enum's own docstring calls it "the
131
+ scope-declaration forcing function," and `OUT_OF_SCOPE` is a real,
132
+ readable value a JSON consumer can check for, not an omission.
133
+
134
+ ---
135
+
136
+ ## 4. Known boundaries of what the live probes actually catch
137
+
138
+ Findings below come from the Freddie Mac second-dataset validation study
139
+ (`zekan/benchmark/results/DATASET2_FREDDIEMAC_VALIDATION_STUDY.md`) —
140
+ every threshold Zekan ships was calibrated on one dataset
141
+ (Diabetes-130); this study is the record of what did and did not
142
+ transfer to an independently structured second one.
143
+
144
+ ### The terminal-row check's miss set: event-stamp vs. persistent-state fields
145
+
146
+ `probe_terminal_row_presence` catches columns populated **exclusively**
147
+ on an entity's last row (`terminal_row_fraction == 1.0` exactly — a
148
+ deductive, not inductive, criterion by design,
149
+ `TERMINAL_ROW_PRESENCE_PREREGISTRATION.md` §9.3). Validated against a
150
+ hand-authored, independent ground-truth list of 24 known post-outcome
151
+ Freddie Mac columns: **16 of 24 hit exactly**, **0 false positives**
152
+ (study doc §6). The 8 misses are not random — every one is
153
+ outcome-adjacent for a reason *other than* row position:
154
+
155
+ | miss | why it's missed |
156
+ |---|---|
157
+ | `MODIFICATION FLAG`, `PAYMENT DEFERRAL FLAG`, `INTEREST RATE STEP INDICATOR`, `CURRENT PERIOD MODIFICATION COSTS` | **persistent state, not an event stamp** — these recur across many rows of an affected entity's life once set (e.g. ~35 rows/loan for the modification-cost field), not a single terminal record |
158
+ | `BORROWER ASSISTANCE PLAN`, `DELINQUENCY DUE TO DISASTER` | current-period distress flags genuinely forward-associated with the target (93%/91% concentration) — real signal, not a row-position artifact |
159
+ | `MORTGAGE INSURANCE CANCELLATION INDICATOR` | dense on 1,893 of 1,905 entities — the opposite of terminal-only |
160
+ | `BANKRUPTCY CRAMDOWN COSTS` | the check's own named stress anchor: `terminal_row_fraction = 0.535`, a genuinely mixed pattern (a bankruptcy event that sometimes, not always, coincides with termination) — plausibly a real partial leak that no current structural check catches, recorded as an accepted limit, not discovered later as a surprise |
161
+
162
+ **The general shape of the miss set: a column whose leak-relevant state is
163
+ written once and persists across many subsequent rows (a flag, a status,
164
+ a running cost total) looks nothing like a terminal-only field to a
165
+ row-position check.** This is a real, principled boundary of the
166
+ detection mechanism, not a bug — the check is deductive precisely because
167
+ it only claims what row position alone can prove.
168
+
169
+ ### One row per entity: the terminal-row check correctly recognizes it has nothing to check
170
+
171
+ On a frame where every entity contributes exactly one row (no lifecycle
172
+ for "terminal" to mean anything relative to), `probe_terminal_row_presence`
173
+ returns no findings — this frame shape was measured to make the check
174
+ fire on 40 of 40 candidates with zero discrimination before this guard
175
+ existed (`DATASET2_ADDENDUM_12_FRAME_T_FALSIFIED_INJECTED_ANCHOR.md` §3);
176
+ it is now recognized as **inapplicable**, not silently vacuous, and does
177
+ not count against `structural_coverage_complete`
178
+ (`TERMINAL_ROW_PRESENCE_PREREGISTRATION.md` §10). Zekan's core severity
179
+ engine likewise needs recurring per-entity observations over time — see
180
+ the README's "Does Zekan fit my project?" section — so this boundary
181
+ applies to the same class of one-row-per-item data the engine itself
182
+ already declines to fit.
183
+
184
+ ### The screenability gate is anti-correlated with leak risk
185
+
186
+ `probe_undeclared_feature_screen` (Upgrade 1) requires a minimum
187
+ non-missing count and minority-class count before it will score a
188
+ candidate feature at all (`_MIN_NONMISSING_COUNT`,
189
+ `_MIN_MINORITY_CLASS_COUNT`). Measured on the Freddie Mac frame: this
190
+ gate excluded 16 of 63 candidate columns, and **15 of those 16 were
191
+ post-outcome/post-liquidation columns**
192
+ (`DATASET2_ADDENDUM_06_FRAME_D_PROBE_SEARCH.md` Finding 1) — the gate's
193
+ own sparsity floor excludes precisely the highest-risk class of column,
194
+ *because* outcome-derived fields are sparse for the same reason the
195
+ outcome itself is rare. The terminal-row check (above) closes part of
196
+ this specific gap for one leak shape (event-stamp fields); the gate's
197
+ general anti-correlation with risk remains, un-addressed, for any
198
+ candidate feature the terminal-row check's own narrower criterion
199
+ doesn't happen to also catch.
200
+
201
+ ### Upgrade H's `U >= 0.99` remains untested on a second dataset
202
+
203
+ The near-bijection check has never fired on any real Freddie Mac feature,
204
+ across two independent runs
205
+ (`DATASET2_ADDENDUM_06_FRAME_D_PROBE_SEARCH.md`,
206
+ `DATASET2_ADDENDUM_07_FRAME_D_DROPPED.md`). This is recorded as
207
+ **untested, not passed** — the one pre-identified real candidate for a
208
+ near-bijective leak (DDLPI, a field the source data guide's own
209
+ definition says delinquency status is computed from) was ruled out on
210
+ data-quality grounds before it could reach the check: of the 27,524 rows
211
+ where the target actually fires, DDLPI is populated on only 47 (0.17%)
212
+ — too sparse for any screen to evaluate, not a clean negative result
213
+ (study doc §4a). The `0.99` threshold's separation margin
214
+ (`UPGRADE_H_CALIBRATION.md`) has only ever been measured against
215
+ Diabetes-130.
216
+
217
+ ---
218
+
219
+ ## 5. What Zekan does not attempt, by design (v1 scope)
220
+
221
+ Stated in the audit's own "Scope note" (printed on every run) and in the
222
+ README's fit criteria:
223
+
224
+ - Combined or multi-feature leakage — the undeclared-feature screen is
225
+ strictly univariate.
226
+ - Notebooks, user split code, or external train/test artifacts — Zekan
227
+ evaluates under the contract you declare, not your actual pipeline code.
228
+ - One-row-per-item data with no repeated entity structure over time — the
229
+ core engine needs recurring per-entity observations to build temporal
230
+ folds at all.
231
+ - Regression targets and multi-class targets — v1 is yes/no prediction
232
+ only.
233
+
234
+ ---
235
+
236
+ ## 6. See also
237
+
238
+ [`METHODOLOGY.md`](METHODOLOGY.md) — what `fixable_leakage` measures, why
239
+ detection and severity are separate questions, and why the calibrated
240
+ floors (`warn_floor`/`fail_floor`) were shown not to transfer as absolute
241
+ values to a second dataset.
@@ -0,0 +1,94 @@
1
+ # Forbidden Zero-Cooccurrence Check
2
+
3
+ Dated 2026-08-20.
4
+
5
+ ## Motivation
6
+
7
+ Frame P (`zekan/benchmark/results/DATASET2_ADDENDUM_10_FRAME_P_SEVERITY_DILUTION.md`)
8
+ declared 25 forbidden columns and Zekan returned `fixable_leakage=0.0085` --
9
+ a number that was **correct but meaningless**. That addendum's table A2
10
+ measured that 16 of those 25 columns (the arithmetic loss/disposition
11
+ family: `ACTUAL LOSS` and its components, `ZERO BALANCE CODE/EFFECTIVE
12
+ DATE/REMOVAL UPB`, `DELINQUENT ACCRUED INTEREST`, `DUE DATE OF LAST PAID
13
+ INSTALLMENT (DDLPI)`, `CUMULATIVE MODIFICATION COSTS`, `UNDERWRITING DEFECT
14
+ AND MAJOR SERVICING DEFECT SETTLEMENT DATE`) were **0.000000 populated on
15
+ every one of the frame's 4,567 positive rows**. A column that is never
16
+ non-missing on a positive row carries no information distinguishing
17
+ positives from negatives -- no leak declared through it could possibly have
18
+ contributed to the measured B-C gap on that frame, no matter what the
19
+ arithmetic landed on. Zekan already had every number needed to notice this
20
+ (non-missing counts, target values) and said nothing. This check closes
21
+ that gap.
22
+
23
+ ## Criterion
24
+
25
+ No pre-registration measurement was needed before implementing this check
26
+ -- the claim is deductive, not statistical:
27
+
28
+ For each column in `forbidden_after_prediction`, count non-missing rows
29
+ where `target == 1` (the positive class; this project's targets are
30
+ 0/1-encoded binary columns throughout, the same convention
31
+ `near_bijection_probe.py` already relies on). If that count is **zero**,
32
+ the declared leak cannot contribute to `B - C` on this frame: the column
33
+ carries no information distinguishing positives, so any severity number
34
+ computed with it among the forbidden set is structurally pinned toward
35
+ whatever the *other* forbidden columns alone would have produced.
36
+
37
+ Counting only -- no model, no folds, no target relationship beyond
38
+ non-missing presence at the row level.
39
+
40
+ ## Behavior
41
+
42
+ - **Annotate-only.** Never changes `policy_decision.verdict`, same as every
43
+ other structural probe in `zekan/severity/audit.py::_build_probe_registry`.
44
+ - `confirmed=True`, scoped to the structural **fact** (zero co-occurrence)
45
+ -- never to severity. It says nothing about whether *other* declared
46
+ forbidden columns on the same frame carried a real, measurable leak.
47
+ - Message states meaning first: `"'{col}' is never populated on any row
48
+ where {target} is positive, so declaring it forbidden cannot change the
49
+ measured severity on this frame"`.
50
+ - Action line points at the contract or the target's own definition --
51
+ deliberately nothing about imputation, since imputing a value that was
52
+ never observed on a positive row would manufacture information that does
53
+ not exist, not recover it.
54
+ - Reports the count and the positive-row total as evidence
55
+ (`n_non_missing_on_positive_rows` / `n_positive_rows`).
56
+ - When **every** declared forbidden column meets the criterion, that is a
57
+ distinct, stronger statement: the entire declared forbidden set is
58
+ structurally incapable of contributing to `fixable_leakage` on this
59
+ frame, so the audit could not have found anything through it, whatever
60
+ number came back. Every per-column record carries `all_forbidden_zero`
61
+ in its evidence, and its `what` text states the stronger claim directly
62
+ rather than leaving the reader to infer it from N separate identical-
63
+ looking records.
64
+
65
+ See `zekan/detectors/forbidden_zero_cooccurrence_probe.py` for the
66
+ implementation and `zekan/detectors/schema.py`'s `FORBIDDEN_ZERO_COOCCURRENCE`
67
+ registry row / `ForbiddenZeroCooccurrenceDetail` for the typed evidence.
68
+
69
+ ## Interaction with the structural coverage caveat (020a46a)
70
+
71
+ `zekan/severity/audit.py::_compute_structural_coverage` (introduced in
72
+ `020a46a`) marks a category as a coverage **gap** in exactly two cases: a
73
+ probe was silently skipped because its declared capability precondition
74
+ wasn't met (`needs_folds`/`needs_random_folds` and the fold set wasn't
75
+ available), or a probe raised and was isolated as `PROBE_FAILED`. Neither
76
+ condition applies here: `probe_forbidden_zero_cooccurrence` declares no
77
+ capability flags (same registration shape as
78
+ `probe_forbidden_entity_level_aggregate`) and runs unconditionally to
79
+ completion on every audit.
80
+
81
+ **Decision: this is NOT a coverage gap.** The check ran and returned a
82
+ record -- that record being "the declared forbidden set is structurally
83
+ incapable of measuring anything here" is the *informative* case
84
+ (`_compute_structural_coverage`'s own docstring: "a probe that ran and
85
+ returned real IssueRecords -- zero, one, or every candidate flagged --
86
+ counts as covered, full stop... never an informativeness check"), the same
87
+ bucket `probe_terminal_row_presence` already occupies when it fires on
88
+ every single candidate on a degenerate cross-sectional frame
89
+ (`DATASET2_ADDENDUM_12_FRAME_T_FALSIFIED_INJECTED_ANCHOR.md` section 3;
90
+ `tests/test_structural_coverage.py::test_vacuous_but_completed_probe_is_not_a_gap`).
91
+ The finding itself -- loud, in `structural_annotations`, `status="fail"`,
92
+ `confirmed=True` -- is the warning. `structural_coverage_complete` stays
93
+ `True` when this is the only thing that fired. Verified directly:
94
+ `tests/test_forbidden_zero_cooccurrence_probe.py::test_all_zero_is_not_a_coverage_gap`.
zekan-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zekan contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.