Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
ce4663c
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
03a1167
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
d25eeda
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
0aaf992
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
3e5bfca
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
8966294
ci: repair analysis-run DAG cutoff context
seonghobae Aug 19, 2026
cad7327
feat: focus Keyman after GNB Event Lineage names it next (v2.19.0)
seonghobae Aug 19, 2026
8ed5015
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
3990fc9
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
f9b86fc
docs: assign Calendar focus ADR a unique number
seonghobae Aug 20, 2026
609b61d
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
a5831c0
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
73887c9
fix: preserve Event Lineage focus after related loads
seonghobae Aug 20, 2026
cbdcc6f
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
1eeac6b
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
b5caf7c
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
36ef783
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
29c9732
test: keep customer master router specific
seonghobae Aug 20, 2026
1c37126
test(red): reproduce stale post detail navigation
seonghobae Aug 20, 2026
b39fd35
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
48461d0
merge: restack onto feat/ask-agent-open-focus-event-lineage-v2160
seonghobae Aug 20, 2026
bd08c68
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
c910478
feat: name Weekly VOC and focus Event Lineage (v2.13.0)
seonghobae Aug 19, 2026
85db4e1
fix: reject impossible Weekly VOC dates
seonghobae Aug 20, 2026
8daa419
feat: open Calendar commitments onto Event Lineage (v2.14.0)
seonghobae Aug 19, 2026
156a507
fix: stop auto-drilled Keyman panel from stealing Event Lineage focus
seonghobae Aug 20, 2026
5267614
fix: keep Calendar scroll with Event Lineage
seonghobae Aug 20, 2026
dc00316
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
15c82c7
test: keep customer master router specific
seonghobae Aug 20, 2026
8fb0fea
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
7f36683
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
a731551
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
4d6494d
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
a7f3c12
merge: restack onto latest feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
d789c30
merge: restack onto latest feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
8bbc2ed
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
5e3e241
chore: reconcile remote Keyman restack with verified tree
seonghobae Aug 20, 2026
251efe1
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
4506f26
fix: align static SQL contract with safe composition
seonghobae Aug 20, 2026
b333958
chore: restack Keyman focus on current Event Lineage head
seonghobae Aug 20, 2026
7d881db
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
4ff6fa2
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
387427e
chore: restack Keyman focus on final Event Lineage
seonghobae Aug 20, 2026
afe8c2d
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
0229c4b
test: align embedded image accessibility expectation
seonghobae Aug 20, 2026
dc15a23
Merge commit '85013814fd65114c5a75d04297f04e342129b8d1' of https://gi…
seonghobae Aug 20, 2026
86c5f88
fix: avoid keyman ADR number collision
seonghobae Aug 20, 2026
95f19d0
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
eb4594d
docs: assign unique ADR number to Valkey events
seonghobae Aug 20, 2026
74c2234
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
e44f1a6
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
a005e7e
Merge current event-lineage base into GNB lineage
seonghobae Aug 20, 2026
60b44c4
merge: propagate current protected Event Lineage base
seonghobae Aug 20, 2026
75cdf43
merge: propagate current Event Lineage branch head
seonghobae Aug 20, 2026
26a6d9c
merge: propagate current semantic-source Event Lineage base
seonghobae Aug 20, 2026
4a76186
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
bbbe734
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
259b3b0
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
0beb110
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
8a6a1b6
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 21, 2026
c8b7445
fix: remove self-modifying repair artifacts
seonghobae Aug 21, 2026
d0ecc3d
feat: verify Global Ask semantic claims with public evidence (#276)
seonghobae Aug 21, 2026
d1b181f
fix: fail closed on malformed claim verification
seonghobae Aug 21, 2026
73621d1
chore: restack keyman focus on current parent
seonghobae Aug 21, 2026
1a8f926
chore: preserve exact keyman focus repair
seonghobae Aug 21, 2026
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
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@ Opening a Calendar commitment uses the same focus path (ADR 0094). Do not
invent a week, a theta, a cutoff body, or a CalDAV event.
Opening a Customer master related post uses the same focus path (ADR 0095).
Do not invent a week, a theta, a cutoff body, a CalDAV event, or a customer.
Opening an Ask Agent cited post uses the same focus path (ADR 0096).
Do not invent a cited post.
A linked Event Lineage node opened from that focused popup keeps the
originating flags (ADR 0097). That open then focuses Keyman as the named
next read (ADR 0100). Do not invent a week, a theta, a cutoff body,
a CalDAV event, a customer, or a cited post.
Opening an Ask Agent cited post uses the same focus path (ADR 0096). Do not
invent a cited post.
A linked Event Lineage node opened from that focused popup keeps the
originating flags (ADR 0097). Do not invent a cited post.
That open then focuses Keyman as the named next read (ADR 0100). Do not invent
a week, a theta, a cutoff body, a CalDAV event, a customer, or a cited post.


## Tests
Expand Down
11 changes: 5 additions & 6 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,13 @@ HTML. `src/api.ts` calls the FastAPI backend directly with the token
Keycloak issued; `src/App.tsx` renders a git-branch SVG of
`GET /api/lineage` (click a node to open that post; `post_admin` can
rebuild), the post list with a named Weekly VOC ISO-8601 week filter
(ADR 0092; opening that filtered post focuses Event Lineage, ADR 0093).
Calendar commitments use the same Event Lineage focus path (ADR 0094).
(ADR 0092; opening that filtered post focuses Event Lineage, ADR 0093),
Calendar commitments use the same Event Lineage focus path (ADR 0094),
Customer master related posts use the same Event Lineage focus path
(ADR 0095). Ask Agent cited posts use the same Event Lineage focus path
(ADR 0096). A linked Event Lineage node opened from a focused popup
keeps those flags (ADR 0097) and then focuses Keyman as the named next
read (ADR 0100).
The full detail popup includes Korean
(ADR 0096). A linked Event Lineage node opened from a focused popup keeps
those flags (ADR 0097) and then focuses Keyman as the named next read
(ADR 0100). The full detail popup includes Korean
summary/key-events/R&R, VOC evidence excerpts, an Event Lineage panel
(direct vs. indirect links; a link opens that post), the Keyman
affiliate tree (resolved ancestors plus unresolved org roots), Keyman +
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.d/2.20.0-global-ask-public-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 2.20.0 — Global Ask public semantic verification

- Global Ask may nominate source posts from persisted semantic, ontology, and Knowledge Graph evidence instead of requiring the buyer's term to appear in raw post text.
- Multilingual contains-search uses one indexable predicate per semantic field and migration 0054 adds matching `pg_trgm` GIN indexes; concatenated expression scans are regression-tested out.
- An explicit public-verification boundary uses SearXNG retrieval and contextual-orchestrator verification while keeping external URLs separate from internal post citations.
- Private source evidence, Keyman/person facts, TEPP measurement artifacts, and fast-mlsirm measurement data are ineligible for public-search egress.
- Public corroboration remains review evidence and never authority-promotes an inferred graph or ontology assertion.
7 changes: 7 additions & 0 deletions CHANGELOG.d/2.20.0-project-history-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 2.20.0 Project-history response contract

Project-history storage projections now emit the same strict evidence-bound
shape that the HTTP response validates. Project identity display names retain
their authoritative code/key, event source IDs and time basis are explicit,
and the endpoint no longer fails with a validation error for an authorized
timeline.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ All notable changes to this project are documented here. Format follows
and evaluation as the next read. After an authorized answer, Ask Agent
names cited posts as current before that open. Home-list opens do not add
that focus or copy. No TEPP theta is invented. No cited post is invented
(ADR 0096 / ADR 0039 / ADR 0016).
(ADR 0096 / ADR 0039 / ADR 0016).

## [2.15.0] - 2026-08-19

Expand Down
240 changes: 240 additions & 0 deletions backend/app/global_ask_retrieval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""Semantic and Knowledge-Graph candidate nomination for Global Ask.

Candidate nomination is deliberately non-authoritative. This module returns
post identifiers only. The caller must re-run the normal source-post visibility
predicate before exposing body text or evidence.
"""

from __future__ import annotations

import re
from typing import Any

import asyncpg

from lineageweave.claim_verification import ontology_lookup_codes_for_question

_STOP_WORDS = frozenset(
{
"which",
"what",
"where",
"when",
"who",
"why",
"how",
"the",
"this",
"that",
"posts",
"post",
"글",
"게시글",
"질문",
"관련",
"확인되는",
"핵심",
"사실",
"무엇",
"무엇인가요",
"인가요",
}
)
_TOKEN = re.compile(r"[^\W_]+(?:-[^\W_]+)*", re.UNICODE)
_EVIDENCE_POST_IDS = re.compile(r"\[evidence_post_id=([^]]+)\]")


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."""

if maximum_terms <= 0:
return ()
return tuple(
dict.fromkeys(
token.casefold()
for token in _TOKEN.findall(question or "")
if len(token) >= 2 and token.casefold() not in _STOP_WORDS
)
)[:maximum_terms]


async def semantic_candidate_post_ids(
conn: asyncpg.Connection,
question: str | None,
*,
maximum_candidates: int = 128,
) -> list[str]:
"""Nominate posts from persisted semantic and Knowledge-Graph evidence.

Project mentions, responsibility/affiliation evidence, Keyman names, and
organization/team catalogs use one ``ILIKE`` predicate per indexed column.
Search-corroborated raw/canonical organization-name pairs additionally
nominate direct organization mentions and affiliated people's posts; pending
or uncorroborated aliases do not. This preserves multilingual substring lookup
without wrapping indexed fields in an expression that forces a sequential
scan. Ontology lookup codes are applied only to graph lookup-code columns.
The function never returns source text and nomination never grants access.
"""

if maximum_candidates <= 0:
return []
terms = global_ask_query_terms(question)
ontology_codes = ontology_lookup_codes_for_question(question or "")
if not terms and not ontology_codes:
return []
rows = await conn.fetch(
"""
with query_terms as (
select unnest($1::text[]) as term
), verified_organization as (
select distinct entity.corporate_entity_id
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'
), candidate_post as (
select mention.post_id, post.created_at
from post_project_mention mention
join source_post post on post.post_id = mention.post_id
where exists (
select 1 from query_terms term
where mention.project_name ilike '%' || term.term || '%'
or mention.evidence_text ilike '%' || term.term || '%'
or mention.ontology_iri ilike '%' || term.term || '%'
)
union all
select role.post_id, post.created_at
from post_summary_role role
join source_post post on post.post_id = role.post_id
where exists (
select 1 from query_terms term
where role.actor_name ilike '%' || term.term || '%'
or role.responsibility ilike '%' || term.term || '%'
or role.affiliated_organization_name ilike '%' || term.term || '%'
)
union all
select mention.post_id, post.created_at
from post_person_mention mention
join cataloged_person person on person.person_id = mention.person_id
join source_post post on post.post_id = mention.post_id
where exists (
select 1 from query_terms term
where person.person_name ilike '%' || term.term || '%'
or person.last_known_job_title ilike '%' || term.term || '%'
or mention.mention_context ilike '%' || term.term || '%'
)
union all
select mention.post_id, post.created_at
from post_organization_mention mention
join corporate_entity entity
on entity.corporate_entity_id = mention.corporate_entity_id
join source_post post on post.post_id = mention.post_id
where exists (
select 1 from query_terms term
where entity.entity_name ilike '%' || term.term || '%'
)
union all
select mention.post_id, post.created_at
from post_organization_mention mention
join verified_organization organization
on organization.corporate_entity_id = mention.corporate_entity_id
join source_post post on post.post_id = mention.post_id
union all
select mention.post_id, post.created_at
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 source_post post on post.post_id = mention.post_id
union all
select mention.post_id, post.created_at
from post_team_mention mention
join cataloged_team team on team.team_id = mention.team_id
join source_post post on post.post_id = mention.post_id
where exists (
select 1 from query_terms term
where team.team_name ilike '%' || term.term || '%'
or team.affiliated_organization_name ilike '%' || term.term || '%'
)
union all
select evidence.evidence_post_id as post_id, post.created_at
from knowledge_graph_edge edge
join knowledge_graph_edge_evidence evidence
on evidence.knowledge_graph_edge_id = edge.knowledge_graph_edge_id
join source_post post on post.post_id = evidence.evidence_post_id
where edge.edge_type_code = any($2::text[])
or edge.source_node_type_code = any($2::text[])
or edge.target_node_type_code = any($2::text[])
)
select post_id::text as post_id
from candidate_post
group by post_id
order by max(created_at) desc, post_id desc
limit $3
""",
list(terms),
list(ontology_codes),
maximum_candidates,
)
return list(dict.fromkeys(str(row["post_id"]) for row in rows))


def graph_fact_evidence_post_ids(fact: str) -> frozenset[str]:
"""Extract all persisted evidence-post identifiers named by one graph fact."""

match = _EVIDENCE_POST_IDS.search(fact)
if match is None:
return frozenset()
return frozenset(
value.strip() for value in match.group(1).split(",") if value.strip()
)


def public_external_claim_facts(
row: Any,
semantic_facts: tuple[str, ...],
graph_facts: tuple[str, ...],
public_post_ids: frozenset[str],
) -> tuple[str, ...]:
"""Return externally searchable facts only for a public source post.

Source hints, people/Keyman facts, TEPP results, and fast-mlsirm reports are
absent by construction. A graph fact is eligible only if every persisted
evidence post named by that edge is public in the authorized result set.
"""

if row.get("visibility_code") != "public":
return ()
public_graph_facts = tuple(
fact
for fact in graph_facts
if (evidence_ids := graph_fact_evidence_post_ids(fact))
and evidence_ids.issubset(public_post_ids)
and "node_person" not in fact
)
Comment on lines +212 to +218

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Graph relations backed partly by private posts reach external web search

public_external_claim_facts treats a graph relation as public-egress eligible when every evidence id inside the fact string is public (evidence_ids.issubset(public_post_ids)). But _graph_facts_for_posts filters evidence with where evidence.evidence_post_id = any($1) before array_agg (post_chat_ingestion.py), so the fact only names evidence posts within the bounded visible set. A private evidence post on the same edge is dropped, so the check passes and, when verify_external is on, the relation is sent to SearXNG. ADR 0106 requires every persisted evidence post of the edge to be public; this only checks the visible subset.

Open in Devin Review

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

public_semantic_facts = tuple(
_public_semantic_fact(fact)
for fact in semantic_facts
if fact.startswith("project:")
and "node_person" not in fact
and not fact.startswith(("actor:", "Keyman mention:"))
)
return tuple(dict.fromkeys(public_semantic_facts + public_graph_facts))


def _public_semantic_fact(fact: str) -> str:
"""Keep only the assertion, never the internal project evidence excerpt."""

return fact.split(" | evidence:", 1)[0].strip()


__all__ = [
"global_ask_query_terms",
"graph_fact_evidence_post_ids",
"public_external_claim_facts",
"semantic_candidate_post_ids",
]
Loading