Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7f29b97
docs: record customer master tree/filter scope gap
seonghobae Aug 21, 2026
f51b2bc
docs: decide customer master scope facets
seonghobae Aug 21, 2026
c2816f7
docs: anchor customer scope ADR to merged head
seonghobae Aug 21, 2026
765a12c
feat: expose customer master scope facets
seonghobae Aug 21, 2026
6a43dca
docs: record customer master scope implementation
seonghobae Aug 21, 2026
ad4403e
feat: expose observed customer hierarchy facets
seonghobae Aug 21, 2026
7974894
docs: record observed hierarchy facet
seonghobae Aug 21, 2026
c8b32c7
fix: keep observed customer navigation fail closed
seonghobae Aug 21, 2026
96c0ac3
fix: make post detail popup keyboard accessible
seonghobae Aug 21, 2026
a4ae004
docs: refresh exact-head product gap baseline
seonghobae Aug 21, 2026
5fb4c1f
fix: keep demo scope out of customer master queries
seonghobae Aug 21, 2026
6cdb782
docs: record demo-scope authorization regression
seonghobae Aug 21, 2026
884dc97
fix: recompute observed hierarchy after scope filtering
seonghobae Aug 21, 2026
dac963e
docs: record hierarchy facet filtering fix
seonghobae Aug 21, 2026
1eef321
fix: standardize visible keyboard focus
seonghobae Aug 21, 2026
a9e5e84
docs: record shared focus baseline
seonghobae Aug 21, 2026
aef31c3
merge: reconcile lineage-dag-regression base
seonghobae Aug 21, 2026
302ae73
docs: record active semantic parsing PR
seonghobae Aug 21, 2026
e2ebd15
fix: share post visibility policy SQL
seonghobae Aug 21, 2026
4a17dd3
docs: record shared visibility review repair
seonghobae Aug 21, 2026
cc33aaf
docs: record ontology explorer stabilization
seonghobae Aug 21, 2026
09b47cc
fix: keep modal focus inside visible controls
seonghobae Aug 21, 2026
fbffec4
docs: record modal focus review repair
seonghobae Aug 21, 2026
e6d94c2
docs: record current frontend verification
seonghobae Aug 21, 2026
497ef8b
docs: record full backend verification
seonghobae Aug 21, 2026
d38ef6f
fix: fully contain popup keyboard navigation
seonghobae Aug 21, 2026
a5aa0da
fix: contain focus trap from hidden descendants
seonghobae Aug 21, 2026
47932b9
docs: record popup focus repair
seonghobae Aug 21, 2026
696f8d4
docs: record nested hidden focus repair
seonghobae Aug 21, 2026
588dc91
fix: keep disclosure summaries keyboard reachable
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ All notable changes to this project are documented here. Format follows
- All OpenAI-compatible chat-completion consumers now validate the shared
response envelope before parsing it, preventing malformed provider bodies
from escaping as raw `KeyError` or response-shape details.
- The post-detail dialog focus trap now excludes descendants of `aria-hidden`
content as well as collapsed `details`, keeping keyboard focus inside the
visible modal controls.

## [2.12.6] - 2026-08-20

Expand Down
118 changes: 99 additions & 19 deletions backend/app/main.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@
persist_post_summary,
require_summary_source_body,
)
from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL
from backend.app.post_eligibility import (
SOURCE_POST_ELIGIBILITY_SQL,
SOURCE_POST_VISIBILITY_SQL,
)
from backend.app.demo_scope import (
fetch_demo_corporate_entity_ids,
has_real_source_context,
Expand Down Expand Up @@ -550,8 +553,7 @@ async def _post_filter_options(
left join common_lookup_value lookup
on lookup.lookup_category = 'post_visibility'
and lookup.lookup_code = post.visibility_code
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($1::text[]))
where {SOURCE_POST_VISIBILITY_SQL.format(alias='post', authorized_entity_ids='$1')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by display_order, code
"""
Expand All @@ -563,8 +565,7 @@ async def _post_filter_options(
left join common_lookup_value lookup
on lookup.lookup_category = 'voc_type'
and lookup.lookup_code = post.voc_type_code
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($1::text[]))
where {SOURCE_POST_VISIBILITY_SQL.format(alias='post', authorized_entity_ids='$1')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by display_order, code
"""
Expand Down Expand Up @@ -740,7 +741,7 @@ async def read_customer_master(
from source_post
where (nullif(btrim(source_customer_code), '') is not null
or nullif(btrim(source_customer_name), '') is not null)
and (visibility_code = 'public' or corporate_entity_id = any($1::uuid[]))
and {SOURCE_POST_VISIBILITY_SQL.format(alias='source_post', authorized_entity_ids='$1')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='source_post')}
), ranked as (
select scoped.*,
Expand Down Expand Up @@ -806,7 +807,7 @@ async def read_customer_master(
join user_account author on author.user_account_id = post.author_account_id
where post.source_author_code is not null
and btrim(post.source_author_code) <> ''
and (post.visibility_code = 'public' or post.corporate_entity_id = any($1::uuid[]))
and {SOURCE_POST_VISIBILITY_SQL.format(alias='post', authorized_entity_ids='$1')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
), ranked as (
select scoped.*,
Expand Down Expand Up @@ -924,16 +925,77 @@ async def read_customer_master(
""",
list(account.corporate_entity_ids),
)
entity_rows = await conn.fetch(
authorized_entity_rows = await conn.fetch(
"""
select corporate_entity_id, corporate_entity_code, entity_name,
entity_level_code, parent_entity_id
from corporate_entity
where corporate_entity_id = any($1::uuid[])
order by entity_name
select entity.corporate_entity_id, entity.corporate_entity_code,
entity.entity_name, entity.entity_level_code,
entity.parent_entity_id,
coalesce(
array_agg(distinct affiliation.affiliation_scope_code)
filter (where affiliation.affiliation_scope_code is not null),
array['scope_unclassified']::text[]
) as affiliation_scope_codes
from corporate_entity entity
left join account_affiliation affiliation
on affiliation.corporate_entity_id = entity.corporate_entity_id
and affiliation.user_account_id = $2
where entity.corporate_entity_id = any($1::uuid[])
group by entity.corporate_entity_id, entity.corporate_entity_code,
entity.entity_name, entity.entity_level_code,
entity.parent_entity_id
order by entity.entity_name
""",
list(account.corporate_entity_ids),
account.user_account_id,
)
# Resolved organization mentions enrich navigation only when the source
# post is already visible to this account. They never grant access to a
# private post and unresolved counterparty names remain hints below.
# Safe SQL: the visibility and eligibility predicates are closed schema text; ids are bound.
observed_entity_rows = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
f"""
select distinct entity.corporate_entity_id, entity.corporate_entity_code,
entity.entity_name, entity.entity_level_code,
entity.parent_entity_id
from post_organization_mention mention
join source_post post on post.post_id = mention.post_id
join corporate_entity entity
on entity.corporate_entity_id = mention.corporate_entity_id
where {SOURCE_POST_VISIBILITY_SQL.format(alias='post', authorized_entity_ids='$1')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by entity.entity_name
""",
list(account.corporate_entity_ids),
)

scope_facet_by_code = {
"scope_own_entity": "authorized_own",
"scope_granted_entity": "authorized_granted",
"scope_unclassified": "scope_unclassified",
}
entity_by_id: dict[str, dict[str, Any]] = {}

def add_entity(row: asyncpg.Record | dict[str, Any], facets: list[str]) -> None:
entity_id = str(row["corporate_entity_id"])
current = entity_by_id.get(entity_id)
if current is None:
current = dict(row)
current["scope_facets"] = set()
entity_by_id[entity_id] = current
current["scope_facets"].update(facets)

for row in authorized_entity_rows:
add_entity(
row,
[
scope_facet_by_code.get(scope_code, "scope_unclassified")
for scope_code in (row["affiliation_scope_codes"] or ["scope_unclassified"])
],
)
observed_entity_ids = {str(row["corporate_entity_id"]) for row in observed_entity_rows}
for row in observed_entity_rows:
add_entity(row, ["observed_organization"])
Comment thread
seonghobae marked this conversation as resolved.
entity_rows = sorted(entity_by_id.values(), key=lambda row: row["entity_name"])
Comment thread
seonghobae marked this conversation as resolved.
has_source_context = bool(source_customer_rows or source_author_rows)
if not has_source_context:
has_source_context = await has_real_source_context(
Expand All @@ -946,11 +1008,29 @@ async def read_customer_master(
for row in entity_rows
if str(row["corporate_entity_id"]) not in synthetic_only_entity_ids
]
entity_ids = [row["corporate_entity_id"] for row in entity_rows]
surviving_entity_by_id = {
str(row["corporate_entity_id"]): row for row in entity_rows
}
for entity_id in observed_entity_ids:
entity = surviving_entity_by_id.get(entity_id)
if entity is None:
continue
parent_id = entity["parent_entity_id"]
parent = surviving_entity_by_id.get(str(parent_id)) if parent_id is not None else None
if parent is None:
continue
entity["scope_facets"].add("observed_hierarchy")
parent["scope_facets"].add("observed_hierarchy")
Comment thread
seonghobae marked this conversation as resolved.
authorized_entity_id_set = {str(entity_id) for entity_id in account.corporate_entity_ids}
authorized_entity_ids = [
str(row["corporate_entity_id"])
for row in entity_rows
if str(row["corporate_entity_id"]) in authorized_entity_id_set
]
Comment thread
seonghobae marked this conversation as resolved.
source_author_affiliations = await _load_account_affiliation_hints(
conn,
[str(row["author_account_id"]) for row in source_author_rows],
[str(entity_id) for entity_id in entity_ids],
authorized_entity_ids,
)
keyman_rows = await conn.fetch(
"""
Expand All @@ -967,11 +1047,11 @@ async def read_customer_master(
where affiliation.affiliated_corporate_entity_id = any($1::uuid[])
order by person.person_name, affiliation.affiliated_organization_name
""",
entity_ids,
authorized_entity_ids,
)
side_labels = await labels_for_codes(conn, [row["person_side_code"] for row in keyman_rows])
entity_level_labels = await labels_for_codes(conn, [row["entity_level_code"] for row in entity_rows])
relationship_network = await fetch_relationship_network(conn, entity_ids)
relationship_network = await fetch_relationship_network(conn, authorized_entity_ids)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.

keymen_by_id: dict[str, dict[str, Any]] = {}
for row in keyman_rows:
Expand Down Expand Up @@ -1010,6 +1090,7 @@ async def read_customer_master(
"entity_level_label": entity_level_labels.get(
row["entity_level_code"], row["entity_level_code"]
),
"scope_facets": sorted(row["scope_facets"]),
"parent_entity_id": (
str(row["parent_entity_id"]) if row["parent_entity_id"] is not None else None
),
Expand Down Expand Up @@ -1216,8 +1297,7 @@ async def list_posts(
end as search_priority,
count(*) over() as total_count
from source_post post
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($2::text[]))
where {SOURCE_POST_VISIBILITY_SQL.format(alias='post', authorized_entity_ids='$2')}
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias="post")}
and (
$1::text is null
Expand Down
8 changes: 8 additions & 0 deletions backend/app/post_eligibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
"source_project_name",
)

# The SQL projection of the fixed ABAC rule in ``main._can_see_post``. Keep
# the authorized-id placeholder explicit so every reader query shares the
# same public-or-affiliated visibility boundary.
SOURCE_POST_VISIBILITY_SQL = (
"({alias}.visibility_code = 'public' "
"or {alias}.corporate_entity_id = any({authorized_entity_ids}::uuid[]))"
)
Comment thread
seonghobae marked this conversation as resolved.


def source_context_present_sql(alias: str) -> str:
return " or ".join(
Expand Down
Loading