feat(threshold): raise default private_date post-filter to 0.85 - #46
feat(threshold): raise default private_date post-filter to 0.85#46lBroth wants to merge 1 commit into
Conversation
The GLiNER model is over-eager on bare calendar dates without identifying context — a system-prompt line like "Today's date is 2026-05-21" or a copyright footer like "© 2024 Acme Inc." gets tagged as `private_date` and erodes the token budget on every roundtrip without protecting anyone. Add `DEFAULT_CATEGORY_THRESHOLDS` with `private_date: 0.85` and merge it into the post-filter on a key-by-key basis — user-supplied `categoryThresholds` still win for the labels they spell out. Spans above 0.85 (DOB, birth-date, explicit personal-date PII) keep firing; the long tail of generic dates drops. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Closing this. The problem it targets is real, but a score threshold cannot express it — and the measured cost is a recall regression on a PII class. It does not affect its own target caseRe-ran the real model against gateway-shaped boilerplate. The dates this PR was written to suppress score near the ceiling, so an 0.85 cut does not reach them: On the multi-line system-prompt block from the original gateway report, all five date spans sit between 0.998 and 1.000. The threshold removes none of them. Across a 37-case sweep it drops 3 of 22 boilerplate date spans (13.6%) and 1 of 15 personal date spans (6.7%) — roughly 2× selectivity, and the personal one it removes is a birth date. Why: the score is not measuring what we needIt tracks span-boundary confidence, not PII relevance. Personal context does not move it: The Measured costRe-scored across OOD-7 + the internal bench, against corrected gold: 419 true date detections dropped to remove 453 false ones, and the class F1 goes down. Characterising the false positives it does remove shows they are not the target pathology either: Zero copyright footers, zero system-prompt date lines — that traffic is not in these corpora, and where it is, it scores ~1.00. Net on the benchmark this is approximately a no-op (+0.0009 macro on OOD-7 when combined with #45, −0.0055 on the regression cell), so it is not paying for the recall it costs. Separate defect found while reviewing
If any built-in per-category table lands later, precedence has to be by specificity with explicit caller config beating a built-in default: What would actually workA negative-context rule rather than a threshold: drop |
Summary
The GLiNER model is over-eager on bare calendar dates without identifying context — a system-prompt line like
Today's date is 2026-05-21or a footer like© 2024 Acme Inc.gets taggedprivate_dateand erodes the token budget on every roundtrip without protecting anyone.This adds
DEFAULT_CATEGORY_THRESHOLDSwithprivate_date: 0.85and merges it into the post-filter on a key-by-key basis — user-suppliedcategoryThresholdsstill win for the labels they spell out. Spans above 0.85 (DOB, birth-date, explicit personal-date PII) keep firing; the long tail of generic dates drops.Files
src/defaults.ts—DEFAULT_CATEGORY_THRESHOLDS = { private_date: 0.85 }src/nullpii.ts— merge built-in + user thresholds; user keys wintest/nullpii.test.ts— 2 integration tests: weak-date dropped / strong-date kept; user override still worksTest plan
npm test— 273 passing (was 271)npm run typecheck/lint/build— cleanpackages/evalon a date-heavy corpus to confirm no recall regression on DOB / birth-date / explicit dated PII (model emits these well above 0.9 in eval-loop history). Defer until bench run is convenient.Rationale
Choice of 0.85 is conservative — sits halfway between the global decode threshold (0.5) and the high-precision recognizer floor (0.95). If bench shows recall regression on legitimate dated PII the right knob is
private_date-specific not the global cut.🤖 Generated with Claude Code