Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5387acb
feat(ask): nominate persisted semantic evidence
Aug 25, 2026
6d4b0e3
docs(gaps): record current protected queue
Aug 25, 2026
6852439
docs(gap): reconcile semantic evidence queue
Aug 25, 2026
5468c04
docs: assign unique semantic nomination ADR number
Aug 25, 2026
fa24548
feat(ask): verify typed public semantic claims (#682)
seonghobae Aug 26, 2026
9d1c5a1
test(ask): apply semantic search indexes in integration
Aug 26, 2026
f4cb992
fix(ask): show empty completed verification
Aug 26, 2026
3d743ff
fix(ask): pass claim client factory directly
Aug 26, 2026
91caeb0
style(ask): remove unused claim status import
Aug 26, 2026
ac521a2
fix(ask): isolate unexpected verification adapter failures
Aug 26, 2026
147f208
perf(ask): release pool before embedding calls
Aug 26, 2026
b7570c5
docs(adr): assign unique semantic decision ids
Aug 26, 2026
5aff98d
fix(semantic): isolate migration ids and late-bind verifier
Aug 26, 2026
99a0322
fix(schema): reserve unique public-verification migration id
Aug 26, 2026
eecbf88
fix(db): recover interrupted semantic indexes
Aug 26, 2026
213d661
fix(ask): classify embedding transport failures
Aug 26, 2026
672fdbf
fix(ask): defer optional verification client
Aug 26, 2026
748944a
fix(ask): preserve public verification boundaries
Aug 26, 2026
9460283
Merge remote-tracking branch 'origin/main' into restack/pr-672
seonghobae Aug 26, 2026
3f76c18
fix(ask): connect semantic evidence safely
Aug 26, 2026
5d2626b
fix(ask): wire typed public claims
Aug 26, 2026
b464e88
docs: reconcile stacked evidence rows
Aug 26, 2026
b4169c0
docs: reconcile stacked audit records
Aug 26, 2026
62b2dba
Merge branch 'feat/global-ask-semantic-public-current' of https://git…
Aug 26, 2026
f78f036
docs(ask): state typed claim selection contract
Aug 26, 2026
a3e87a8
test: follow global Ask embedding contract
Aug 26, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project are documented here. Format follows

### Added

- Global Ask now nominates bounded post IDs from indexed persisted project,
role, person, organization, team, and Knowledge Graph evidence before its
existing embedding channel, then repeats eligibility and authorization
before reading any source (ADR 0233; issue #272 internal-search slice).
- Persist explicit paragraph, list, table, MathML formula, and caller-parsed
conversation-turn semantic-unit kinds without inferring absent boundaries.
- Event Lineage now persists each reconstructed connection's independent
Expand Down
6 changes: 5 additions & 1 deletion backend/app/global_ask_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,11 @@ async def process_global_ask_job(
embedding_client=embedding_factory(),
semantic_query_client=semantic_query_factory(),
verify_external=bool(row["verify_external_requested"]),
claim_verification_client=claim_verification_factory(),
claim_verification_client=(
claim_verification_factory()
if bool(row["verify_external_requested"])
else None
),
knowledge_cutoff=row["knowledge_cutoff"],
),
timeout=JOB_DEADLINE_SECONDS,
Expand Down
129 changes: 129 additions & 0 deletions backend/app/global_ask_semantic_candidates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Nominate Global Ask posts from persisted semantic and KG evidence."""

from __future__ import annotations

from datetime import date

import asyncpg

from .post_eligibility import SOURCE_POST_ELIGIBILITY_SQL


async def semantic_candidate_post_ids(
conn: asyncpg.Connection,
question: str,
*,
maximum_candidates: int,
authorized_corporate_entity_ids: list[str],
authorized_process_unit_ids: list[str],
date_from: date | None,
date_to: date | None,
) -> list[str]:
Comment thread
seonghobae marked this conversation as resolved.
"""Return bounded post IDs whose persisted semantic evidence matches.

Nomination grants no access and returns no evidence text. The caller must
apply the ordinary source-post RBAC/ABAC, eligibility, and time boundary
before reading any nominated row.
"""

if maximum_candidates <= 0 or not question.strip():
Comment thread
seonghobae marked this conversation as resolved.
return []
rows = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
f"""
with search_query as (
select websearch_to_tsquery('simple', $1) as value
), 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
cross join search_query
where to_tsvector(
'simple',
coalesce(mention.project_key, '') || ' ' ||
coalesce(mention.project_name, '') || ' ' ||
coalesce(mention.evidence_text, '') || ' ' ||
coalesce(mention.ontology_iri, '')
) @@ search_query.value
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
cross join search_query
where to_tsvector(
'simple',
coalesce(role.actor_name, '') || ' ' ||
coalesce(role.responsibility, '') || ' ' ||
coalesce(role.affiliated_organization_name, '')
) @@ search_query.value
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
cross join search_query
where to_tsvector(
'simple', coalesce(person.person_name, '') || ' ' ||
coalesce(person.last_known_job_title, '')
) @@ search_query.value
union all
select mention.post_id, post.created_at
from post_person_mention mention
join source_post post on post.post_id = mention.post_id
cross join search_query
where to_tsvector('simple', coalesce(mention.mention_context, ''))
@@ search_query.value
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
cross join search_query
where to_tsvector('simple', entity.entity_name) @@ search_query.value
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
cross join search_query
where to_tsvector(
'simple',
coalesce(team.team_name, '') || ' ' ||
coalesce(team.affiliated_organization_name, '')
) @@ search_query.value
union all
select evidence.evidence_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
cross join search_query
where to_tsvector(
'simple',
replace(coalesce(edge.edge_type_code, '') || ' ' ||
coalesce(edge.source_node_type_code, '') || ' ' ||
coalesce(edge.target_node_type_code, ''), '_', ' ')
) @@ search_query.value
)
select candidate.post_id::text as post_id
from candidate_post candidate
join source_post post on post.post_id = candidate.post_id
where (post.visibility_code = 'public'
or (post.corporate_entity_id::text = any($3::text[])
and (cardinality($4::text[]) = 0
or post.process_unit_id::text = any($4::text[]))))
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
and ($5::date is null or (coalesce(post.event_occurred_at, post.created_at) at time zone 'Asia/Seoul')::date >= $5)
and ($6::date is null or (coalesce(post.event_occurred_at, post.created_at) at time zone 'Asia/Seoul')::date <= $6)
Comment thread
seonghobae marked this conversation as resolved.
group by candidate.post_id
order by max(candidate.created_at) desc, candidate.post_id desc
limit $2
Comment thread
seonghobae marked this conversation as resolved.
""",
question,
maximum_candidates,
authorized_corporate_entity_ids,
authorized_process_unit_ids,
date_from,
date_to,
)
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
return [str(row["post_id"]) for row in rows]
4 changes: 2 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ async def lifespan(app: FastAPI):
)
)
app.state.post_content_worker = content_worker
# Late-bound lambda so tests that monkeypatch _post_chat_client reach
# the worker too (the name resolves in module globals at call time).
# Late-bound lambdas keep worker factories aligned with runtime/test
# configuration changes (the names resolve in globals at call time).
# Only this worker gets the long answer timeout; the per-post chat
# endpoint keeps the client's interactive default.
global_ask_worker = asyncio.create_task(
Expand Down
Loading
Loading