Skip to content

refactor: span-level advisor, recogniser base, new recognisers, MCP entrypoint - #32

Merged
mitchelllisle merged 21 commits into
mainfrom
fix/review-fixes
Jul 25, 2026
Merged

refactor: span-level advisor, recogniser base, new recognisers, MCP entrypoint#32
mitchelllisle merged 21 commits into
mainfrom
fix/review-fixes

Conversation

@mitchelllisle

Copy link
Copy Markdown
Owner

What changed

  • Rename judgeadvisor — module moved to src/priveil/advisor/; assessor and span-advisor (formerly refiner) preserved with corrected logic (prefix strip, threshold 0.9, fail-open sort).
  • Recogniser base classsrc/priveil/recognisers/base.py standardises the interface; all AU recognisers updated.
  • New generic recognisers — credit card, datetime, email, location, person, phone added under src/priveil/recognisers/.
  • MCP entrypointsrc/priveil/mcp/__main__.py and src/priveil/__main__.py added for direct module execution.
  • Version bump 0.4.0pyproject.toml and uv.lock updated.
  • Docker / env cleanup — Dockerfile, docker-compose.yml, docker-compose.local.yml, .env.example aligned to new layout.
  • Tests and deps updated — conftest, integration, and unit tests reflect rename and new recognisers.

Why

Addresses review feedback: judge terminology was confusing; generic recognisers were missing; MCP server lacked a runnable entrypoint.

Copilot AI and others added 10 commits July 2, 2026 08:13
…efault, docker port

- sorted(certain + kept, key=e.start) — preserve span ordering across certain/kept merge
- catch IndexError — empty choices list no longer bypasses fail-open
- strip provider prefix in build_judge_model when base_url set (was already done in _judge)
- judge_score_threshold 0.99 → 0.85 — 0.99 sent nearly everything to judge, defeating latency goal
- docker-compose: vllm base_url uses container port 8000 not host-mapped 8001; drop openai: prefix
- module docstring documents false-negative capability trade-off explicitly
Presidio's recognisers emit 0.85 as their default confidence floor.
With threshold=0.85, nearly every detection is classified as 'certain'
and the judge is never called. 0.7 ensures default-confidence spans
(PERSON, LOCATION, DATE_TIME) go through the judge for verification.
Logic: score >= threshold → certain (skip judge), score < threshold → uncertain (send to judge).
0.85 and 0.7 had it backwards — they made PERSON at Presidio's 0.85 floor 'certain',
so the judge was never called. 0.9 sends NER detections (0.85) to the judge while
keeping 1.0-confidence pattern matches (AU_TFN, EMAIL) as certain.
…ities; correct threshold to 0.9

- _judge() was stripping on ':' treating Ollama's name:tag as provider:model (qwen2.5:3b → 3b).
  Custom endpoint receives model name as-is; stripping belongs in assessor path only.
- Fail-open return now sorted by e.start, consistent with success path.
- judge_score_threshold default 0.7 → 0.9: higher threshold = more entities sent to judge.
  0.9 makes Presidio's typical 0.85 NER detections uncertain; 1.0 pattern matches stay certain.
- app.py, mcp/server.py: change startup condition from 'judge_model or judge_base_url'
  to 'judge_model' — build_assessor_agent requires judge_model; base_url alone would
  raise ValueError at startup
- tests/conftest.py: _PassThroughRefiner uses object.__init__ instead of pass, avoiding
  violated base class invariants (_client/_settings unset)
- build_refiner: document OpenAI-compatible-only constraint; non-OpenAI provider prefixes
  (anthropic:, google:) are not supported and will fail-open
- docker-compose.yml: pin vllm image to v0.9.2 instead of :latest
- .env.example: clarify refiner/assessor provider split, correct threshold default to 0.9,
  swap example model to openai:gpt-4o-mini
Copilot AI review requested due to automatic review settings July 5, 2026 02:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the “judge/refiner” layer into an “advisor” architecture, replaces the Presidio+spaCy detection pipeline with an internal recogniser stack (regex + optional GLiNER2), and adds runnable entrypoints for both the FastAPI service and MCP server. It also updates tests, Docker/dev tooling, and bumps the package version to 0.4.0 to reflect the architectural shift.

Changes:

  • Replace Presidio AnalyzerEngine/spaCy with BaseRecogniser-driven detection (regex + optional GLiNER2) and introduce span-level SpanAdvisor.
  • Rename judgeadvisor throughout API/MCP surfaces (modes, settings, deps) and add advisor_applied metadata.
  • Add new generic recognisers (email/phone/credit-card/person/location/date_time), new module entrypoints, and update Docker/dev configuration accordingly.

Reviewed changes

Copilot reviewed 54 out of 58 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/unit/test_refiner.py New unit tests for SpanAdvisor behavior (routing, timeout fail-open, keep list filtering).
tests/unit/test_recognisers.py Adjust tests to new recogniser base validation hook (_validate).
tests/unit/test_pseudonymiser.py Update pseudonymiser tests to use operator configs derived from recognisers.
tests/unit/test_properties.py Switch property tests from ENTITY_CLASSIFICATION to recogniser-based completeness checks.
tests/unit/test_judge_model.py Rename tests to build_advisor_model and update env var expectations.
tests/unit/test_entities.py Update entity model tests for new verification field and recogniser-driven validation.
tests/unit/test_assessor.py Update assessor tests to advisor module path and new entity construction requirements.
tests/unit/test_analyser.py Update analyser tests to new Span/BaseRecogniser conversion and regex-only detection.
tests/integration/test_refine.py Integration coverage for advisor/fast modes across /detect and /pseudonymise.
tests/integration/test_pseudonymise.py Update integration test expectations for request/response mode metadata rename.
tests/integration/test_detect.py Update integration test expectations for request/response mode metadata rename.
tests/conftest.py Rewire fixtures to new analyser/pseudonymiser/advisor stack and provide test doubles.
src/priveil/settings.py Replace judge/spaCy settings with advisor + optional GLiNER2 config.
src/priveil/recognisers/registry.py Introduce recogniser registry returning ordered BaseRecogniser list + operator config builder.
src/priveil/recognisers/phone.py Add generic international phone regex recogniser.
src/priveil/recognisers/person.py Add GLiNER2-backed PERSON recogniser.
src/priveil/recognisers/location.py Add GLiNER2-backed LOCATION recogniser.
src/priveil/recognisers/email.py Add email regex recogniser.
src/priveil/recognisers/date_time.py Add GLiNER2-backed DATE_TIME recogniser with advisor verification routing.
src/priveil/recognisers/credit_card.py Add credit card regex recogniser with Luhn validation and mask operator defaults.
src/priveil/recognisers/base.py New shared recogniser base classes (Span, BaseRecogniser, RegexRecogniser, GLiNERRecogniser).
src/priveil/recognisers/au_tfn.py Port TFN recogniser to new regex base with checksum validation.
src/priveil/recognisers/au_phone.py Port AU phone recogniser to new regex base.
src/priveil/recognisers/au_medicare.py Port Medicare recogniser to new regex base with checksum validation.
src/priveil/recognisers/au_bsb.py Port BSB recogniser to new regex base and route verification via advisor.
src/priveil/recognisers/au_acn.py Port ACN recogniser to new regex base with checksum validation.
src/priveil/recognisers/au_abn.py Port ABN recogniser to new regex base with checksum validation.
src/priveil/mcp/tools.py Update MCP tools to use advisor flow and surface advisor_applied in responses.
src/priveil/mcp/server.py Update MCP server lifespan to load GLiNER2 optionally and wire advisor/assessor.
src/priveil/mcp/main.py Add runnable MCP entrypoint (python -m priveil.mcp).
src/priveil/mcp/init.py Update MCP package exports/import side effects to align with new entrypoint.
src/priveil/judge/refiner.py Remove legacy refiner implementation.
src/priveil/judge/prompts/refiner.md Remove legacy refiner prompt.
src/priveil/engine/pseudonymiser.py Make operator config injectable and derive defaults from recognisers.
src/priveil/engine/analyser.py Replace Presidio+spaCy analyser with concurrent recogniser execution + global deduplication.
src/priveil/domain/pseudonymisation.py Rename mode to advisor and add advisor_applied field to response data.
src/priveil/domain/entities.py Remove classification map and add verification field on Entity.
src/priveil/domain/detection.py Rename mode to advisor and add advisor_applied field to response data.
src/priveil/app.py Rewire app lifespan to initialise recogniser stack, pseudonymiser configs, and optional advisor.
src/priveil/api/routes/pseudonymise.py Replace refiner wiring with advisor wiring and surface advisor_applied.
src/priveil/api/routes/detect.py Replace refiner wiring with advisor wiring and surface advisor_applied.
src/priveil/api/routes/assess.py Update assess route to advisor assessor module and new env var wording.
src/priveil/api/models.py Update request/response meta mode Literals to advisor.
src/priveil/api/deps.py Replace refiner dependency with advisor dependency and update assess error message.
src/priveil/advisor/span_advisor.py New span-level advisor implementation with timeout + fail-open behavior.
src/priveil/advisor/prompts/span_advisor.md New advisor prompt for span keep/drop decisions.
src/priveil/advisor/prompts/assessor.md New assessor prompt aligned with advisor layer.
src/priveil/advisor/model.py New advisor model factory supporting provider strings and OpenAI-compatible endpoints.
src/priveil/advisor/assessor.py Update assessor prompt construction + breakdown logic and wire to advisor model factory.
src/priveil/advisor/init.py Add advisor package init.
src/priveil/main.py Add runnable API entrypoint (python -m priveil).
pyproject.toml Bump version to 0.4.0, add gliner optional extra, update scripts/dep set.
Dockerfile Restructure stages and add a “local” target with gliner + mcp extras.
docker-compose.yml Add vLLM service and wire API container to it via advisor settings.
docker-compose.local.yml Add local dev stack (Ollama + API + MCP over SSE).
.env.example Update env var documentation to advisor terminology and new tuning knobs.
.cursor/skills/conduit-py.md Update repository guidance to reflect new advisor/recogniser architecture and entrypoints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/priveil/app.py Outdated
Comment thread tests/conftest.py
Comment thread tests/integration/test_refine.py Outdated
Comment thread src/priveil/domain/detection.py
Comment thread src/priveil/domain/pseudonymisation.py
Comment thread .env.example Outdated
Comment thread src/priveil/settings.py Outdated
Comment thread tests/unit/test_refiner.py Outdated
Comment thread tests/unit/test_refiner.py
Copilot AI review requested due to automatic review settings July 5, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 58 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (2)

src/priveil/app.py:88

  • Warmup still references app.state.refiner, but the refiner was removed/renamed to advisor. As written, this will raise AttributeError at startup and also misses warming the advisor for this text.
    await app.state.analyser.analyse(DetectionRequest(text="Warmup: Jane Smith, TFN 123 456 782", mode="fast"))
    if app.state.refiner is not None:
        with suppress(Exception):
            warmup = await app.state.analyser.analyse(DetectionRequest(text="Warmup Jane Smith", mode="fast"))
            await app.state.refiner.refine("Warmup Jane Smith", warmup.entities)

tests/unit/test_recognisers.py:143

  • This cross-cutting section still refers to Presidio's validate_result semantics, but the recogniser API is now _validate() and the engine no longer uses Presidio. Renaming the test/docstring and fixing the assertion message will keep the intent clear.

Comment thread tests/unit/test_refiner.py Outdated
Comment thread src/priveil/app.py Outdated
Comment thread tests/conftest.py
Comment thread tests/integration/test_refine.py Outdated
Comment thread src/priveil/recognisers/base.py Outdated
Comment thread src/priveil/settings.py Outdated
Comment thread .env.example Outdated
- Fix critical app.py hasattr→is None bug (engines were never init in prod)
- Delete stale app.state.refiner warmup block (refiner removed)
- test_refine.py: rename refined_client→advised_client, fix docstring, test names
- test_assess.py: PRIVEIL_JUDGE_MODEL→PRIVEIL_ADVISOR_MODEL, drop PERSON assertion
- test_pseudonymise.py: operator_override_redact uses email (no GLiNER2 in tests)
- test_tools.py: PERSON→email/TFN tests, fix assess match string, rename tests
- test_refiner.py: rename misleading test name
- detection/pseudonymisation.py: fix mode description (judge→advisor env var)
- recognisers/base.py: Three→Four public classes in docstring
- .env.example: fix mode='judge' refs, fix wrong raw-OpenAI-client claim
Copilot AI review requested due to automatic review settings July 5, 2026 03:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 61 changed files in this pull request and generated 25 comments.

Comment thread src/priveil/recognisers/base.py Outdated
Comment thread src/priveil/recognisers/phone.py Outdated
Comment thread src/priveil/recognisers/phone.py Outdated
Comment thread src/priveil/recognisers/email.py Outdated
Comment thread src/priveil/recognisers/email.py Outdated
Comment thread src/priveil/recognisers/au_acn.py Outdated
Comment thread src/priveil/recognisers/au_medicare.py Outdated
Comment thread src/priveil/recognisers/au_medicare.py Outdated
Comment thread src/priveil/recognisers/au_tfn.py Outdated
Comment thread src/priveil/recognisers/au_tfn.py Outdated
- verification: ClassVar → ClassVar[Literal['trust','advisor']] in all 12 recognisers
- Add Literal to typing imports in each recogniser
- Fix context word matching: substring 'cw in window' → re.search with \b
  boundaries to prevent false positives (e.g. 'tel' matching 'hotel')
…endpoints

- mode='judge' → mode='advisor' throughout
- PRIVEIL_JUDGE_* → PRIVEIL_ADVISOR_* in config table
- /anonymise → /pseudonymise endpoint
- Add Detection Stack section (GLiNER2 + regex layers)
- Remove spaCy references; update MCP install instructions
- Update project structure (judge/ → advisor/, full recognisers/ tree)
- JSON examples show new meta/data response envelope
- Configuration table: full advisor settings + GLiNER2 model setting
- Remove AU_ACCOUNT_NUMBER (not in current recogniser registry)
Copilot AI review requested due to automatic review settings July 5, 2026 08:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 58 out of 62 changed files in this pull request and generated 6 comments.

Comment thread src/priveil/app.py
Comment on lines +36 to +45
try:
from gliner2 import GLiNER2
logger.info("Loading GLiNER2 model '%s'…", settings.gliner2_model)
gliner_model = GLiNER2.from_pretrained(settings.gliner2_model)
logger.info("GLiNER2 model loaded.")
except ImportError:
logger.warning(
"gliner2 package not installed — NER recognisers (PERSON, LOCATION, DATE_TIME) "
"are disabled. Install with: uv sync --extra gliner"
)
Comment thread src/priveil/mcp/server.py
Comment on lines +55 to +59
try:
from gliner2 import GLiNER2
gliner_model = GLiNER2.from_pretrained(settings.gliner2_model)
logger.info("GLiNER2 model loaded for MCP server.")
except ImportError:
Comment thread tests/unit/test_recognisers.py Outdated
Comment on lines 149 to 153
@@ -151,7 +151,7 @@ def test_validate_result_returns_false_not_none_on_invalid(
Returning None would tell presidio 'no validation performed' and the
match would be kept at its original score — a critical correctness bug.
"""
Comment on lines +1 to +16
"""Generic (international) phone number recogniser."""

from __future__ import annotations

import re
from typing import ClassVar, Literal

from priveil.domain.entities import EntityType, Sensitivity
from priveil.recognisers.base import RegexRecogniser


class PhoneRecogniser(RegexRecogniser):
"""Detect North American and international phone numbers.

Complements AUPhoneRecogniser with broader international patterns.
"""
Comment thread src/priveil/recognisers/person.py Outdated
Comment thread src/priveil/recognisers/location.py Outdated
- docker-compose.yml test service: remove bare 'pytest' command override;
  Dockerfile CMD already uses 'uv run pytest' which resolves through the
  managed env — bare 'pytest' is not on $PATH
- docker-compose.local.yml api/mcp: add --no-dev to 'uv run' so it does not
  attempt to sync dev dependencies (pytest etc.) that are absent from the
  local image (no-dev + gliner + mcp extras only)
…ark suite

- tests/unit/test_detection_accuracy.py: 52 parametrized tests covering
  obvious detections (email, TFN, ABN, ACN, Medicare, AU phone, credit card,
  BSB), checksum/Luhn true-negatives, entity metadata assertions, and
  mixed-document scenarios. Documents the \b anchoring gap for +61 and (02)
  phone formats as known limitations.

- tests/unit/test_span_advisor_routing.py: 17 unit tests for
  SpanAdvisor.advise() — trust bypass, score-threshold bypass (0.9),
  LLM invocation with drop semantics, fail-open via mocked agent.run,
  and HTTP-level integration using detect_client / advised_client fixtures.

- benchmarks/: standalone suite requiring a live API (make serve).
  Auto-skips when unreachable. 7 data-driven scenarios in benchmarks/data/
  (single email, customer notification, AU loan application, financial
  report, bulk records, dense PII, advisor-ambiguous BSB). Benchmarks
  cover per-scenario latency, concurrent burst throughput (5/10/25
  requests), and fast-vs-advisor mode comparison.

- benchmarks/generate_report.py: post-processor that strips raw timing
  arrays, fetches /detect for each scenario, embeds results into
  results.json and index.html (so the dashboard works via file://).

- benchmarks/index.html: static dashboard with overview cards, latency
  table with inline bar charts, concurrent throughput, mode comparison,
  and a span viewer modal — click any scenario to see the text with
  detected entity spans colour-coded by type, hover tooltips, and an
  entity detail table.

- Makefile: bench target (make serve first, then make bench); test target
  unchanged. pyproject.toml: pytest-benchmark added to dev deps.
Copilot AI review requested due to automatic review settings July 6, 2026 07:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 77 changed files in this pull request and generated 4 comments.

Comment thread pyproject.toml
Comment on lines 42 to +46
"pytest>=9.1.1,<10.0",
"pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0,<8",
"httpx>=0.28.1",
"hypothesis>=6.155.7",
"pytest-benchmark>=5.1.0",
Comment on lines +25 to +27
patterns: ClassVar[list[re.Pattern[str]]] = [
re.compile(r"\b(?:\+?1[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}\b"),
]
Comment on lines +66 to +86
def detect(self, text: str) -> list[Span]:
spans: list[Span] = []
text_lower = text.lower()

for pattern in self.patterns:
for match in pattern.finditer(text):
matched_text = match.group()

if self._validate(matched_text) is False:
continue

score = self._BASE_SCORE

# Context boost: search a window of ±_CONTEXT_WINDOW chars around the match.
window_start = max(0, match.start() - self._CONTEXT_WINDOW)
window_end = min(len(text), match.end() + self._CONTEXT_WINDOW)
window = text_lower[window_start:window_end]

if any(re.search(r"\b" + re.escape(cw) + r"\b", window) for cw in self.context_words):
score = min(1.0, score + self._CONTEXT_BOOST)

Comment thread tests/unit/test_recognisers.py Outdated
Comment on lines 149 to 157
result = recogniser._validate(invalid_text) # type: ignore[union-attr]
assert result is False, (
f"{type(recogniser).__name__}.validate_result({invalid_text!r}) returned "
f"{result!r}; expected False so presidio invalidates the match"
mitchelllisle and others added 2 commits July 6, 2026 18:00
'routing' is too generic — matches network/system routing terminology
and pushes the score to 0.95, bypassing the advisor threshold (0.9).
The LLM is then never consulted, causing false positives like routing
codes in technical documentation being flagged as bank BSBs.

Removed 'routing' and added 'account', 'transfer', 'payment' as
replacement context words that are unambiguously banking-specific.
Score for non-banking contexts stays at 0.8, correctly routing to the
LLM advisor for contextual verification.
Copilot AI review requested due to automatic review settings July 14, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 77 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

tests/unit/test_recognisers.py:158

  • The assertion message still mentions validate_result and Presidio. Since tests now call _validate(), the message should match the method name and the new engine behaviour (discard span on False).

Comment thread tests/unit/test_recognisers.py Outdated
Comment on lines +38 to +39
# validate_result must return True (not None) so presidio keeps the match
assert self.recogniser.validate_result("123 456 782") is True
assert self.recogniser._validate("123 456 782") is True
Comment thread tests/unit/test_recognisers.py Outdated
Comment on lines 41 to 45
def test_invalid_returns_false_not_none(self) -> None:
# The spike bug: returning None here means presidio keeps the match at
# its original score. We must return False to invalidate.
result = self.recogniser.validate_result("123 456 789")
result = self.recogniser._validate("123 456 789")
assert result is False, f"Expected False, got {result!r} — validate_result must not return None on failure"
Comment thread tests/unit/test_recognisers.py Outdated
Comment on lines 149 to 153
@@ -151,7 +151,7 @@ def test_validate_result_returns_false_not_none_on_invalid(
Returning None would tell presidio 'no validation performed' and the
match would be kept at its original score — a critical correctness bug.
"""
Comment thread src/priveil/advisor/span_advisor.py Outdated
Comment on lines +8 to +10
When the LLM advisor is unavailable (PRIVEIL_ADVISOR_MODEL not set) the advisor
tier degrades conservatively — spans are kept rather than silently dropped.

Comment on lines +22 to +24
patterns: ClassVar[list[re.Pattern[str]]] = [
re.compile(r"\b[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}\b", re.IGNORECASE),
]
Comment on lines +25 to +27
patterns: ClassVar[list[re.Pattern[str]]] = [
re.compile(r"\b(?:\+?1[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}\b"),
]
Remove stray blank line that ruff's isort rule flagged as an
unsorted/unformatted import block, failing the Lint & type-check job.
Copilot AI review requested due to automatic review settings July 25, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 77 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/priveil/recognisers/email.py:24

  • The email regex only allows a single dot-separated TLD segment (e.g. "example.com"), so addresses like "[email protected]" will be partially matched as "[email protected]". In /pseudonymise this can leave the trailing ".au" in the output, leaking part of the address and producing an incorrect entity_map key.
    src/priveil/recognisers/phone.py:27
  • The phone regex is wrapped in word boundaries (\b). Because '+' is a non-word character, numbers that start with a country prefix like "+1 212 555 1212" will not match at the start boundary, despite the docstring claiming international support.

Addresses outstanding review feedback on #32 that was never actioned.

Correctness:
- email: domain allowed a single label, so [email protected] redacted to
  <EMAIL>.au. Allow one or more labels.
- phone: leading \b cannot match before '+', so '+1 212 555 0123' lost its
  '+' and '+61 2 9374 4000' was not detected at all. Replace with lookarounds
  and add an E.164 pattern. Match parens as a balanced pair so
  '(212) 555-0123' no longer yields the malformed span '212) 555-0123'.
- analyser: on an overlap score tie, prefer the longer span. AU_BSB matched
  '212-555' inside a phone number and won, redacting to 'XXX-XXX-0123'.
- person/location: declared verification='trust', so SpanAdvisor never
  submitted them and span_advisor.md's PERSON/LOCATION false-positive
  guidance was unreachable. Route them to the advisor tier.
- app/mcp: GLiNER2 load caught only ImportError, so an installed-but-
  unloadable model killed startup. Degrade to regex-only instead.

Docs/tests:
- Drop stale presidio/validate_result wording for the _validate contract.
- Correct span_advisor and phone docstrings.
- Add span-exactness, international, dedup and NER-routing regressions.
Copilot AI review requested due to automatic review settings July 25, 2026 10:53
@mitchelllisle
mitchelllisle merged commit f60765a into main Jul 25, 2026
5 checks passed
@mitchelllisle
mitchelllisle deleted the fix/review-fixes branch July 25, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 73 out of 77 changed files in this pull request and generated 2 comments.

Comment on lines +121 to +125
prev = result[-1]
if span.start < prev.end: # Overlapping
if span.score > prev.score:
result[-1] = span
# else: keep prev, discard span
advisor_spans.append(entity)

if not advisor_spans:
return AdvisorResult(entities=tuple(certain), advisor_applied=False)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants