-
Notifications
You must be signed in to change notification settings - Fork 1
feat: disclose verified organization labels on Global Ask (v2.21.0) #318
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 all commits
63ebb11
b50e51f
79b2bd3
f025eb4
87611ff
a64bdf0
8e67050
42d24fd
8ad157b
3ae5bd0
2329b69
69b6052
04e1664
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # 2.21.0 — Disclose corroborated organization labels on Global Ask | ||
|
|
||
| - Global Ask now shows search-corroborated raw→canonical organization | ||
| labels on cited-post evidence (for example `DC → Demo Corp`) after | ||
| ABAC-visible posts are selected. | ||
| - Pending and uncorroborated aliases stay excluded from nomination and | ||
| disclosure. | ||
| - Verified aliases now join the catalog through a stable entity id, so | ||
| same-named organizations cannot cross-match through display labels; existing | ||
| Compose volumes replay the required migrations. | ||
| - When a corroborated label matched, Ask names opening a cited post to | ||
| read Event Lineage as the next action (ADR 0107 / ADR 0008). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,102 @@ | |
| _TOKEN = re.compile(r"[^\W_]+(?:-[^\W_]+)*", re.UNICODE) | ||
| _EVIDENCE_POST_IDS = re.compile(r"\[evidence_post_id=([^]]+)\]") | ||
|
|
||
| VERIFIED_ORGANIZATION_LABEL_PREFIX = "verified organization label:" | ||
| VERIFIED_ORGANIZATION_LABEL_NEXT_ACTION = ( | ||
| "Corroborated organization labels are current. Open a cited post to read Event Lineage." | ||
| ) | ||
|
|
||
|
|
||
| def verified_organization_label_fact( | ||
| raw_organization_name: str, | ||
| resolved_organization_name: str, | ||
| ) -> str: | ||
| """Return one buyer-visible SKOS altLabel → prefLabel pair.""" | ||
|
|
||
| return ( | ||
| f"{VERIFIED_ORGANIZATION_LABEL_PREFIX} {raw_organization_name} → " | ||
| f"{resolved_organization_name}" | ||
| ) | ||
|
|
||
|
|
||
| async def verified_organization_label_facts( | ||
| conn: asyncpg.Connection, | ||
| question: str | None, | ||
| post_ids: list[str], | ||
| *, | ||
| maximum_terms: int = 8, | ||
| ) -> dict[str, tuple[str, ...]]: | ||
| """Disclose corroborated raw→canonical labels for already-visible posts. | ||
|
|
||
| Pending and uncorroborated aliases never appear. Nomination remains | ||
| identifier-only; this query runs after ABAC-visible post ids are known. | ||
| """ | ||
|
|
||
| if not post_ids: | ||
| return {} | ||
| terms = global_ask_query_terms(question, maximum_terms=maximum_terms) | ||
| if not terms: | ||
| return {} | ||
| rows = await conn.fetch( | ||
| """ | ||
| with query_terms as ( | ||
| select unnest($1::text[]) as term | ||
| ), nominated_post as ( | ||
| select unnest($2::uuid[]) as post_id | ||
| ), verified_organization as ( | ||
| select distinct | ||
| entity.corporate_entity_id, | ||
| resolution.raw_organization_name, | ||
| resolution.resolved_organization_name | ||
| from organization_name_resolution resolution | ||
| join corporate_entity entity | ||
| on resolution.resolved_corporate_entity_id = entity.corporate_entity_id | ||
| join query_terms term | ||
| on resolution.raw_organization_name ilike '%' || term.term || '%' | ||
| or resolution.resolved_organization_name ilike '%' || term.term || '%' | ||
| where resolution.verification_status_code = 'verify_corroborated' | ||
| ), matched_organization_label as ( | ||
| select distinct | ||
| mention.post_id, | ||
| organization.raw_organization_name, | ||
| organization.resolved_organization_name | ||
| from post_organization_mention mention | ||
| join verified_organization organization | ||
| on organization.corporate_entity_id = mention.corporate_entity_id | ||
| join nominated_post nominated | ||
| on nominated.post_id = mention.post_id | ||
| union | ||
| select distinct | ||
| mention.post_id, | ||
| organization.raw_organization_name, | ||
| organization.resolved_organization_name | ||
| from post_person_mention mention | ||
| join person_affiliation affiliation | ||
| on affiliation.person_id = mention.person_id | ||
| join verified_organization organization | ||
| on organization.corporate_entity_id = affiliation.affiliated_corporate_entity_id | ||
| join nominated_post nominated | ||
| on nominated.post_id = mention.post_id | ||
| ) | ||
| select post_id::text as post_id, | ||
| raw_organization_name, | ||
| resolved_organization_name | ||
| from matched_organization_label | ||
| order by post_id, raw_organization_name, resolved_organization_name | ||
| """, | ||
| list(terms), | ||
| list(post_ids), | ||
| ) | ||
| facts: dict[str, list[str]] = {} | ||
| for row in rows: | ||
| facts.setdefault(str(row["post_id"]), []).append( | ||
| verified_organization_label_fact( | ||
| row["raw_organization_name"], | ||
| row["resolved_organization_name"], | ||
| ) | ||
| ) | ||
| return {post_id: tuple(dict.fromkeys(values)) for post_id, values in facts.items()} | ||
|
Comment on lines
+64
to
+140
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: Disclosure query recomputes terms independently of nomination
Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| def global_ask_query_terms(question: str | None, *, maximum_terms: int = 8) -> tuple[str, ...]: | ||
| """Return bounded, de-duplicated lexical terms from a Global Ask query.""" | ||
|
|
@@ -90,7 +186,7 @@ async def semantic_candidate_post_ids( | |
| select distinct entity.corporate_entity_id | ||
| from organization_name_resolution resolution | ||
| join corporate_entity entity | ||
| on entity.entity_name = resolution.resolved_organization_name | ||
| on resolution.resolved_corporate_entity_id = entity.corporate_entity_id | ||
| join query_terms term | ||
| on resolution.raw_organization_name ilike '%' || term.term || '%' | ||
| or resolution.resolved_organization_name ilike '%' || term.term || '%' | ||
|
|
@@ -233,8 +329,12 @@ def _public_semantic_fact(fact: str) -> str: | |
|
|
||
|
|
||
| __all__ = [ | ||
| "VERIFIED_ORGANIZATION_LABEL_NEXT_ACTION", | ||
| "VERIFIED_ORGANIZATION_LABEL_PREFIX", | ||
| "global_ask_query_terms", | ||
| "graph_fact_evidence_post_ids", | ||
| "public_external_claim_facts", | ||
| "semantic_candidate_post_ids", | ||
| "verified_organization_label_fact", | ||
| "verified_organization_label_facts", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,7 +71,10 @@ | |
|
|
||
| from .corporate_entity_ingestion import get_or_create_corporate_entity | ||
| from .knowledge_graph import persist_edges_for_post | ||
| from .organization_name_resolution_ingestion import resolve_organization_name | ||
| from .organization_name_resolution_ingestion import ( | ||
| link_verified_organization_entity, | ||
| resolve_organization_name, | ||
| ) | ||
|
|
||
|
|
||
| async def _load_corporate_entity_candidates(conn: asyncpg.Connection) -> list[CorporateEntityCandidate]: | ||
|
|
@@ -288,6 +291,13 @@ async def ingest_post_keymen( | |
| corporate_entity_id, | ||
| mention.job_title, | ||
| ) | ||
| if corporate_entity_id is not None: | ||
| await link_verified_organization_entity( | ||
| conn, | ||
| organization_name, | ||
| post_body, | ||
| corporate_entity_id, | ||
| ) | ||
|
Comment on lines
+294
to
+300
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: link_verified_organization_entity is a no-op for non-corroborated resolutions
Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
+294
to
+300
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. 🔍 Verified-label nomination now requires the ingestion-time entity link The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| if resolved_name not in resolved_names: | ||
| resolved_names.append(resolved_name) | ||
| normalized_mentions.append( | ||
|
|
||
|
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: next_action override coexists with the verify_external skip status
(Refers to this code) Was this helpful? React with 👍 or 👎 to provide feedback. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,33 @@ def _context_sha256(context_text: str) -> str: | |
| return hashlib.sha256(context_text.encode("utf-8")).hexdigest() | ||
|
|
||
|
|
||
| async def link_verified_organization_entity( | ||
| conn: asyncpg.Connection, | ||
| raw_name: str, | ||
| context_text: str, | ||
| corporate_entity_id: str, | ||
| ) -> None: | ||
| """Attach a corroborated resolution to its stable catalog entity id. | ||
|
|
||
| Display names are not identity keys: two catalog entities may legitimately | ||
| share one name. Only the exact raw-name/context cache row is linked, and | ||
| only after external verification has corroborated the resolution. | ||
| """ | ||
| await conn.execute( | ||
| """ | ||
| update organization_name_resolution | ||
| set resolved_corporate_entity_id = $1, | ||
| resolved_at = now() | ||
| where raw_organization_name = $2 | ||
| and context_sha256 = $3 | ||
| and verification_status_code = 'verify_corroborated' | ||
| """, | ||
| corporate_entity_id, | ||
| raw_name, | ||
| _context_sha256(context_text), | ||
| ) | ||
|
Comment on lines
+26
to
+50
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: Resolution row committed before the transaction, linked inside it In keyman_ingestion.py, Was this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
|
|
||
| async def resolve_organization_name( | ||
| conn: asyncpg.Connection, | ||
| resolution_client: OrganizationNameResolutionClient, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Label disclosure is context-agnostic once a resolution is entity-linked
link_verified_organization_entity(organization_name_resolution_ingestion.py) links a resolution keyed by raw-name + context digest to a stable entity id, butverified_organization_label_factsthen discloses that alias for ANY ABAC-visible post mentioning the linkedcorporate_entity(or affiliating a mentioned person to it) whose raw/resolved label matches a query term — it does not re-check the post's own context digest. This is intentional per ADR 0107/0122 (disclosure joins by catalog identity, not per-post context), so a homonym-safe entity id gates it; noting it because it means a corroboration established in one post's context surfaces the alias across other posts about the same entity.Was this helpful? React with 👍 or 👎 to provide feedback.