Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,10 @@ Grounded in SKOS `skos:altLabel`/`skos:prefLabel` (Miles & Bechhofer,
2009). Wired into `backend/app/keyman_ingestion.py`'s affiliation loop
and the offline synthetic-batch script's paced re-implementation of it
(the batch script's own copy was also missing `role_title` persistence
entirely -- fixed alongside this).
entirely -- fixed alongside this). Global Ask discloses corroborated
raw→canonical pairs on cited-post evidence after ABAC and names opening
Event Lineage as the next action; pending and uncorroborated aliases
stay hidden ([ADR 0107](docs/adr/0107-verified-organization-label-evidence.md)).

Also fixed while running this against synthetic embedded-image fixtures:
`image_content.py`'s `_parse_description` required an exact single-pass
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.d/2.21.0-verified-organization-label-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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.
- When a corroborated label matched, Ask names opening a cited post to
read Event Lineage as the next action (ADR 0107 / ADR 0008).
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.21.0] - 2026-08-20

### Added

- Global Ask now discloses search-corroborated organization labels on
cited-post evidence (for example `DC → Demo Corp`) and names opening
a cited post to read Event Lineage as the next action. Pending and
uncorroborated aliases stay hidden. No TEPP theta is invented
(ADR 0107 / ADR 0008 / ADR 0106).

## [2.19.0] - 2026-08-20

### Added
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,10 @@ current and moves focus to the Keyman heading once Keyman rows have
settled (ADR 0100). The report-member auto-land chain to related nodes
and Ask is not used for GNB origins. A home-list open does not gain that
focus. Do not invent a theta.

## Verified organization labels on Global Ask (v2.21.0)

Ask a corroborated synthetic alias such as `DC`. Cited-post evidence
names `DC → Demo Corp` and tells the buyer to open that cited post to
read Event Lineage (ADR 0107). Pending and uncorroborated aliases stay
hidden. Do not invent a theta.
100 changes: 100 additions & 0 deletions backend/app/global_ask_retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 entity.entity_name = resolution.resolved_organization_name
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
Comment on lines +100 to +121

Copy link
Copy Markdown

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, but verified_organization_label_facts then discloses that alias for ANY ABAC-visible post mentioning the linked corporate_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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

)
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Disclosure query recomputes terms independently of nomination

verified_organization_label_facts re-derives query terms via global_ask_query_terms(question, maximum_terms=8) (global_ask_retrieval.py), the same default used by semantic_candidate_post_ids's global_ask_query_terms(question). Because both cap at 8 terms and use the same stopword/token rules, the disclosed labels stay consistent with what nomination matched. Disclosure runs strictly on the ABAC-filtered visible_ids, and pending/uncorroborated aliases are excluded by the verify_corroborated filter — matching ADR 0107's 'disclosure cannot precede ABAC' constraint. No divergence found.

Open in Devin Review

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."""
Expand Down Expand Up @@ -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",
]
23 changes: 20 additions & 3 deletions backend/app/main.py

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: next_action override coexists with the verify_external skip status

_ask_next_action (main.py) overrides the verification-derived next action with the verified-label message whenever any cited post carries a verified_organization_label fact, regardless of external_verification_status. This means even with verify_external=False (status SKIPPED), a corroborated label match will surface the Event-Lineage next action rather than 'Enable public verification...'. This appears intentional per ADR 0107 ('Public-verification next actions remain for answers that have no such label'), but worth confirming that suppressing the verification-enable hint when a label is present is the desired precedence.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
cited_post_summaries,
render_global_ask_context,
)
from backend.app.global_ask_retrieval import VERIFIED_ORGANIZATION_LABEL_NEXT_ACTION
from lineageweave.post_content_normalization import normalize_post_body
from lineageweave.post_evaluation import (
ContextualOrchestratorPostEvaluationClient,
Expand Down Expand Up @@ -2577,6 +2578,21 @@ def _verification_next_action(status_code: str) -> str:
}.get(status_code, "Inspect the authorized cited posts and their evidence.")


def _ask_next_action(
status_code: str,
cited_evidence: list[dict[str, object]],
) -> str:
"""Name Event Lineage when a corroborated organization label nominated the cite."""

if any(
isinstance(fact, dict) and fact.get("kind") == "verified_organization_label"
for item in cited_evidence
for fact in item.get("facts", [])
):
return VERIFIED_ORGANIZATION_LABEL_NEXT_ACTION
return _verification_next_action(status_code)


async def _verify_public_claims(
question: str,
sources: list[ChatSourceDocument],
Expand Down Expand Up @@ -2795,7 +2811,7 @@ async def ask_agent(
"timeline": [],
"external_verification_status": verification_status,
"external_claims": [claim.to_payload() for claim in external_claims],
"next_action": _verification_next_action(verification_status),
"next_action": _ask_next_action(verification_status, []),
}
try:
answer = await asyncio.to_thread(
Expand All @@ -2816,6 +2832,7 @@ async def ask_agent(
cited_ids,
verify_external=request.verify_external,
)
evidence = cited_post_evidence(sources, cited_ids)
async with pool.acquire() as conn:
await persist_global_ask_turn(
conn,
Expand All @@ -2835,12 +2852,12 @@ async def ask_agent(
"answer_text": answer.answer_text,
"cited_post_ids": cited_ids,
"cited_posts": cited_post_summaries(sources, cited_ids),
"cited_post_evidence": cited_post_evidence(sources, cited_ids),
"cited_post_evidence": evidence,
"source_post_ids": [source.post_id for source in sources],
"timeline": global_ask_timeline(sources),
"external_verification_status": verification_status,
"external_claims": [claim.to_payload() for claim in external_claims],
"next_action": _verification_next_action(verification_status),
"next_action": _ask_next_action(verification_status, evidence),
}


Expand Down
4 changes: 4 additions & 0 deletions backend/app/post_chat_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
global_ask_query_terms,
public_external_claim_facts,
semantic_candidate_post_ids,
verified_organization_label_facts,
)
from lineageweave.claim_verification import GlobalAskSourceDocument
from lineageweave.ontology import ontology_annotations
Expand Down Expand Up @@ -706,6 +707,7 @@ async def gather_global_chat_sources(
visible_ids = [str(row["post_id"]) for row in visible_rows]
anchor_is_visible = lineage_anchor_id in visible_ids
semantic_facts = await _semantic_facts_for_posts(conn, visible_ids)
label_facts = await verified_organization_label_facts(conn, question, visible_ids)
graph_facts = (await _graph_facts_for_posts(conn, visible_ids))[:16]
public_post_ids = frozenset(
str(row["post_id"])
Expand Down Expand Up @@ -740,7 +742,9 @@ async def gather_global_chat_sources(
graph_facts=graph_facts if index == 0 else (),
evidence_facts=_source_hint_facts(row)
+ semantic_facts.get(post_id, ())
+ label_facts.get(post_id, ())
Comment on lines 743 to +746

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Label facts kept out of external claims but fed to the LLM prompt

label_facts are concatenated into evidence_facts at post_chat_ingestion.py, which means they appear both in the buyer-facing cited_post_evidence and in the LLM source block (evidence_facts is rendered into the prompt). Critically, external_claim_facts is computed separately from semantic_facts only (post_chat_ingestion.py), and the label prefix 'verified organization label:' does not start with 'project:', so label facts are correctly excluded from public_external_claim_facts. This matches ADR 0107's requirement that the label is internal SKOS evidence, not a public-search claim. Verified as correct, not a bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

+ lineage_fact,

occurred_at=_timestamp_text(row),
timeline_kind=(
"lineage_neighbor"
Expand Down
20 changes: 20 additions & 0 deletions backend/tests/test_global_ask_public_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,23 @@ def verify(self, claim: Any) -> ClaimVerificationResult:
assert len(claims) == 1
assert claims[0].status_code == CLAIM_SUPPORTED
assert verified == ["project: Apollo"]


def test_ask_next_action_names_event_lineage_for_verified_organization_labels() -> None:
evidence = [
{
"post_id": "11111111-1111-1111-1111-111111111111",
"facts": [
{
"kind": "verified_organization_label",
"text": "verified organization label: DC → Demo Corp",
}
],
}
]
assert main._ask_next_action(VERIFICATION_SKIPPED, evidence) == (
"Corroborated organization labels are current. Open a cited post to read Event Lineage."
)
assert main._ask_next_action(VERIFICATION_SKIPPED, []) == (
"Enable public verification to check eligible public claims."
)
11 changes: 7 additions & 4 deletions docs/adr/0008-organization-abbreviation-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ projection. A raw abbreviation, local-language name, or translated name that
has actually appeared in a post context can therefore nominate the same posts
as its canonical corporate-entity name. The projection joins the corroborated
`resolved_organization_name` back to `corporate_entity`; pending and
uncorroborated rows remain invisible. It does not generate translations or
infer aliases at query time. This preserves the source-observed label and the
SKOS preferred/alternative-label distinction while applying the document-level
context required by multilingual entity linking (De Cao et al., 2022).
uncorroborated rows remain invisible. After ABAC-visible posts are selected,
Global Ask discloses the matched raw→canonical pair on cited-post evidence and
names opening Event Lineage as the next action (ADR 0107). It does not generate
translations or infer aliases at query time. This preserves the source-observed
label and the SKOS preferred/alternative-label distinction while applying the
document-level context required by multilingual entity linking (De Cao et al.,
2022).

Only a search-corroborated resolution is ever substituted in for
downstream entity matching (`resolve_corporate_entity`) -- an
Expand Down
68 changes: 68 additions & 0 deletions docs/adr/0107-verified-organization-label-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# ADR 0107 — Disclose corroborated organization labels on Global Ask

- Status: Accepted
- Date: 2026-08-20
- Owners: LineageWeave Buyer surface / Organization identity
- Depends on: [0008](0008-organization-abbreviation-resolution.md), [0106](0106-global-ask-public-claim-verification.md)

## Context

Global Ask may nominate authorized posts from search-corroborated
raw/canonical organization-name pairs (ADR 0008). Nomination returns
post identifiers only. Without a buyer-visible evidence fact, a query
for a synthetic alias such as `DC` can surface a Demo Corp post while
leaving the matched SKOS altLabel → prefLabel pair hidden. The buyer
then cannot tell why that post was nominated or what to open next.

Pending and uncorroborated aliases must stay invisible. A guessed
translation or an unverified LLM expansion is not evidence.

## Decision

After the ordinary source-visibility/ABAC predicate has selected
visible posts, Global Ask SHALL load corroborated
`organization_name_resolution` rows whose raw or resolved label matches
the bounded query terms and whose canonical name joins a
`corporate_entity` already mentioned on, or affiliated to a person on,
those visible posts.

Each match is disclosed as cited-post evidence of kind
`verified_organization_label` with the raw label and canonical label
kept separate (`DC → Demo Corp`). The fact is internal SKOS evidence. It
is not a public-search claim, is not placed in `external_claim_facts`,
and does not authority-promote a Knowledge Graph edge.

Pending (`verify_pending`) and uncorroborated (`verify_uncorroborated`)
rows remain excluded from both nomination and disclosure. The query
uses one indexable `ILIKE` predicate per label column.

When at least one cited post carries a verified organization label, the
response next action SHALL name opening that cited post to read Event
Lineage. Public-verification next actions remain for answers that have
no such label.

## Buyer next action

- corroborated label match → open a cited post to read Event Lineage
- no corroborated label match → keep the ADR 0106 public-verification
next action

## Consequences

- The buyer can inspect the exact raw→canonical pair that nominated the
post without inventing a translation at query time.
- Disclosure cannot precede ABAC: only already-visible post identifiers
are joined.
- Synthetic fixtures only; Demo Corp / DC and Aurora Grid Power / AGP
are the documented examples.

## References

De Cao, N., Wu, L., Popat, K., Artetxe, M., Goyal, N., Plekhanov, M.,
Zettlemoyer, L., & Riedel, S. (2022). Multilingual autoregressive
entity linking. *Transactions of the Association for Computational
Linguistics, 10*, 274–290. https://doi.org/10.1162/tacl_a_00460

Miles, A., & Bechhofer, S. (Eds.). (2009). *SKOS simple knowledge
organization system reference*. World Wide Web Consortium.
https://www.w3.org/TR/skos-reference/
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "2.19.0",
"version": "2.21.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Loading
Loading