Repository navigation
fix: enforce TEPP request and search evidence boundaries #323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
886d7e6
1a27efe
579bae3
3b57e1c
061e621
eb1fc2a
6c2ef69
4e92ef2
4243b26
553cf49
cc203bd
eddd995
caf86d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # 2.12.7 — Preserve Korean search corroboration boundaries | ||
|
|
||
| ## Fixed | ||
|
|
||
| - Accept a Korean organization token followed by a grammatical particle in a | ||
| natural Searxng snippet while still rejecting the token inside a longer | ||
| unrelated Hangul word. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,17 +56,37 @@ | |
| "group", | ||
| "holdings", | ||
| "limited", | ||
| # Fixture descriptors are search vocabulary, not organization identity. | ||
| "fictitious", | ||
| "nonexistent", | ||
| "placeholder", | ||
| "sample", | ||
| "example", | ||
| "demo", | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: New stopwords make demo/example-named orgs permanently uncorroborated Adding Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| "foundation", | ||
| "the", | ||
| "and", | ||
| } | ||
| ) | ||
| # Korean postpositions attach directly to nouns. They are allowed only when | ||
| # the suffix itself ends at a non-word boundary; a longer Hangul word must not | ||
| # turn a substring into corroborating evidence. | ||
| _HANGUL_PARTICLE = r"(?:으로|에서|에게|한테|까지|부터|처럼|보다|마다|조차|마저|밖에|이랑|랑|은|는|이|가|을|를|에|와|과|로|의|도|만|뿐)" | ||
|
|
||
| STATUS_PENDING = "verify_pending" | ||
| STATUS_CORROBORATED = "verify_corroborated" | ||
| STATUS_UNCORROBORATED = "verify_uncorroborated" | ||
|
|
||
|
|
||
| def _contains_org_token(token: str, haystack: str) -> bool: | ||
| """Match one organization token without breaking Korean particle syntax.""" | ||
| if re.search(r"[가-힣]", token): | ||
| pattern = rf"(?<!\w){re.escape(token)}(?:{_HANGUL_PARTICLE})?(?!\w)" | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
|
||
| else: | ||
| pattern = rf"(?<!\w){re.escape(token)}(?!\w)" | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
|
||
| return re.search(pattern, haystack) is not None | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Korean particle list is a curated subset
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class RelationVerificationResult: | ||
| """One claim's verification outcome. | ||
|
|
@@ -88,7 +108,9 @@ class RelationVerificationClient(Protocol): | |
|
|
||
| available: bool | ||
|
|
||
| def verify(self, organization_name: str, relationship_label: str) -> RelationVerificationResult: | ||
| def verify( | ||
| self, organization_name: str, relationship_label: str | ||
| ) -> RelationVerificationResult: | ||
| """Search for corroborating evidence of ``organization_name`` | ||
| having the relationship ``relationship_label`` describes. | ||
|
|
||
|
|
@@ -107,7 +129,9 @@ class NullRelationVerificationClient: | |
|
|
||
| available = False | ||
|
|
||
| def verify(self, organization_name: str, relationship_label: str) -> RelationVerificationResult: # pragma: no cover | ||
| def verify( | ||
| self, organization_name: str, relationship_label: str | ||
| ) -> RelationVerificationResult: # pragma: no cover | ||
| """Verify whether the relationship has supporting external evidence.""" | ||
| raise RuntimeError( | ||
| "NullRelationVerificationClient has no search channel; check .available first" | ||
|
|
@@ -134,11 +158,15 @@ class SearxngRelationVerificationClient: | |
| def __init__(self, base_url: str, *, timeout: float = 15.0) -> None: | ||
| parsed = urlparse(base_url) | ||
| if parsed.scheme not in {"http", "https"}: | ||
| raise ValueError(f"unsupported Searxng base URL scheme: {parsed.scheme or 'missing'}") | ||
| raise ValueError( | ||
| f"unsupported Searxng base URL scheme: {parsed.scheme or 'missing'}" | ||
| ) | ||
| self._base_url = base_url.rstrip("/") | ||
| self._timeout = timeout | ||
|
|
||
| def verify(self, organization_name: str, relationship_label: str) -> RelationVerificationResult: | ||
| def verify( | ||
| self, organization_name: str, relationship_label: str | ||
| ) -> RelationVerificationResult: | ||
| """Verify whether the relationship has supporting external evidence.""" | ||
| query = f"{organization_name} {relationship_label}" | ||
| body = get_json( | ||
|
|
@@ -147,33 +175,46 @@ def verify(self, organization_name: str, relationship_label: str) -> RelationVer | |
| ) | ||
| results = body.get("results") | ||
| if not isinstance(results, list): | ||
| return RelationVerificationResult(status_code=STATUS_UNCORROBORATED, evidence_url=None) | ||
| return RelationVerificationResult( | ||
| status_code=STATUS_UNCORROBORATED, evidence_url=None | ||
| ) | ||
| for result in results: | ||
| if not isinstance(result, dict): | ||
| continue | ||
| evidence_url = corroborating_evidence_url(organization_name, result) | ||
| if evidence_url is not None: | ||
| return RelationVerificationResult(status_code=STATUS_CORROBORATED, evidence_url=evidence_url) | ||
| return RelationVerificationResult(status_code=STATUS_UNCORROBORATED, evidence_url=None) | ||
| return RelationVerificationResult( | ||
| status_code=STATUS_CORROBORATED, evidence_url=evidence_url | ||
| ) | ||
| return RelationVerificationResult( | ||
| status_code=STATUS_UNCORROBORATED, evidence_url=None | ||
| ) | ||
|
|
||
|
|
||
| def corroborating_evidence_url(organization_name: str, result: dict[str, Any]) -> str | None: | ||
| def corroborating_evidence_url( | ||
| organization_name: str, result: dict[str, Any] | ||
| ) -> str | None: | ||
| """Return ``result['url']`` when it is a real-world footprint of ``organization_name``. | ||
|
|
||
| Search engines echo the query in result titles, so "any hit" is not | ||
| corroboration. A single distinctive token is not enough either -- an | ||
| invented name can still contain an ordinary dictionary word (e.g. | ||
| "Fictitious", "Nonexistent") that coincidentally appears on an | ||
| unrelated page, so a genuine multi-token name requires a majority of | ||
| its tokens to co-occur in the same result; a one-token name has no | ||
| majority to require and falls back to that single token. The host | ||
| must also not itself be a search page. Missing or empty URLs are not | ||
| evidence. | ||
| unrelated page, so every distinctive token in a multi-token name must | ||
| co-occur in the same result. A one-token name falls back to that single | ||
| token. The host must also not itself be a search page. Missing or empty | ||
| URLs are not evidence. | ||
| """ | ||
| url = result.get("url") | ||
| if not isinstance(url, str) or not url.strip(): | ||
| return None | ||
| host = urlparse(url).netloc.lower() | ||
| try: | ||
| parsed_url = urlparse(url) | ||
| host = (parsed_url.hostname or "").lower() | ||
| except ValueError: | ||
| return None | ||
| if parsed_url.scheme not in {"http", "https"}: | ||
| return None | ||
| if not host or any(marker in host for marker in _SEARCH_HOST_MARKERS): | ||
| return None | ||
| tokens = [ | ||
|
|
@@ -184,9 +225,9 @@ def corroborating_evidence_url(organization_name: str, result: dict[str, Any]) - | |
| if not tokens: | ||
| return None | ||
| haystack = f"{host} {result.get('content') or ''}".lower() | ||
| # Every distinctive token must occur in the same result. Matching one | ||
| # token lets generic pages about words such as "fictitious" corroborate a | ||
| # made-up multi-word organization. | ||
| if all(token in haystack for token in tokens): | ||
| # Substring matches turn ``Alpha`` into a false hit for ``alphabetical``. | ||
| # Word boundaries keep host labels, punctuation, and Hangul names usable | ||
| # without accepting a token embedded inside an unrelated word. | ||
| if all(_contains_org_token(token, haystack) for token in tokens): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Host matching now requires word boundaries
Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: Corroboration now requires all tokens, not a majority The evidence check switched from majority co-occurrence to requiring every distinctive token in a multi-token name. Documented and intentional, but a genuine result that omits one token of a long name will now read as uncorroborated. Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return url | ||
| return None | ||
Uh oh!
There was an error while loading. Please reload this page.