Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
abcf59b
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
9fce955
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
eb06d6d
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
222262e
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
e9fe3d7
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
c290748
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
77c86e4
fix: ignore stale post detail responses
seonghobae Aug 20, 2026
f0c8b1e
test(red): reproduce stale Keyman drill state
seonghobae Aug 20, 2026
f4cac96
chore: remove red repair workflow while restacking Event Lineage PR
seonghobae Aug 20, 2026
9e5a271
fix(semantics): scope organization name resolutions by context
seonghobae Aug 20, 2026
6898f5d
fix(authz): scope counterparty entity resolution
seonghobae Aug 20, 2026
d52ae3e
fix(kg): merge catalog aliases in relationship network
seonghobae Aug 20, 2026
d425c28
fix(ontology): reject invalid summary actor types
seonghobae Aug 20, 2026
5e17a22
fix(ask): return authorized event timeline
seonghobae Aug 20, 2026
03ff920
chore: restack Event Lineage on current Ask Agent head
seonghobae Aug 20, 2026
bbb88cf
fix(test): apply context resolution migration to API DB
seonghobae Aug 20, 2026
6b431e7
fix(ask): retain bounded global context and DOM order
seonghobae Aug 20, 2026
1878eec
chore: restack Event Lineage on current Ask Agent head
seonghobae Aug 20, 2026
f0680f4
fix(events): register account operation mutations
seonghobae Aug 20, 2026
957d883
fix(semantics): label numbered footnotes
seonghobae Aug 20, 2026
557d451
fix(ask): preserve authorized lower-ranked sources
seonghobae Aug 20, 2026
2962ebe
chore: restack Event Lineage on current Ask Agent head
seonghobae Aug 20, 2026
163be7d
chore: restack Event Lineage on current Ask Agent head
seonghobae Aug 20, 2026
f4ccd3b
chore: restack Event Lineage on current Ask Agent
seonghobae Aug 20, 2026
849a59e
chore: restack Event Lineage on current Ask Agent
seonghobae Aug 20, 2026
bd5a059
chore: reconcile remote Event Lineage restack
seonghobae Aug 20, 2026
542df21
chore: restack Event Lineage on final Ask Agent
seonghobae Aug 20, 2026
8df5078
chore: restack Event Lineage on final Ask Agent
seonghobae Aug 20, 2026
d84ee2c
chore: restack Event Lineage on final Ask Agent
seonghobae Aug 20, 2026
6c4b1d5
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 20, 2026
f68fe16
Merge commit 'fb92785d388d3546a80ce0b7d2bcb57c5ae15d0f' of https://gi…
seonghobae Aug 20, 2026
8501381
fix: reject generic relation verification hits
seonghobae Aug 20, 2026
50c8b8a
fix: align embedded image accessibility assertion
seonghobae Aug 20, 2026
ad8e806
merge: propagate current Ask Agent stack into Event Lineage
seonghobae Aug 20, 2026
017b79d
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 20, 2026
afb2e92
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 20, 2026
41558dc
test: cover current content schema and embedding agent contract
seonghobae Aug 20, 2026
efe1c43
Merge remote-tracking branch 'origin/feat/event-lineage-node-keeps-gn…
seonghobae Aug 20, 2026
02f5afb
Merge current Ask Agent lineage base into event lineage
seonghobae Aug 20, 2026
ca5f793
Merge concurrent event-lineage branch updates
seonghobae Aug 20, 2026
172e52e
merge: propagate current Ask Agent lineage base
seonghobae Aug 20, 2026
f07ceb3
fix: restore unique ADR numbering
seonghobae Aug 20, 2026
2f780fe
fix: keep Valkey ADR numbering unique
seonghobae Aug 20, 2026
c2e31bb
merge: propagate corrected Weekly VOC lineage base
seonghobae Aug 20, 2026
b70e17e
merge: propagate current protected board base
seonghobae Aug 20, 2026
335dd08
fix: keep Weekly VOC test option typed
seonghobae Aug 20, 2026
993b693
merge: propagate current board branch head
seonghobae Aug 20, 2026
d5dbdf7
merge: propagate current semantic-source parent
seonghobae Aug 20, 2026
0beabed
merge: restack GNB focus onto current Ask Agent base
seonghobae Aug 20, 2026
3ed21fe
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 20, 2026
2ff8826
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 21, 2026
d207b62
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 21, 2026
bcdc459
fix: recover expired ask sessions and localize timeline
seonghobae Aug 21, 2026
8832216
fix: localize event lineage post actions
seonghobae Aug 21, 2026
39f2126
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 21, 2026
f970b21
Merge remote-tracking branch 'origin/feat/ask-agent-open-focus-event-…
seonghobae Aug 21, 2026
7ff36d4
fix: preserve semantic image and summary boundaries
seonghobae Aug 21, 2026
9f24b68
fix: keep ADR numbers unique
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ 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). Do not invent a cited post.


## Tests
Expand Down
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,8 @@ rebuild), the post list with a named Weekly VOC ISO-8601 week filter
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), and the full detail popup includes Korean
(ADR 0096). A linked Event Lineage node opened from a focused popup keeps
those flags (ADR 0097), and 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
9 changes: 9 additions & 0 deletions CHANGELOG.d/2.17.0-event-lineage-node-keeps-gnb-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 2.17.0 A linked Event Lineage node keeps GNB focus

Opening a linked Event Lineage DAG node from a GNB-focused popup keeps
Event Lineage focused and names Keyman and evaluation next. A home-list
DAG walk does not. No TEPP theta is invented.

Ask Agent now replaces an expired saved session once, then stores the new
session. The Event Lineage timeline heading, list, and post-button
accessibility labels are translated in the supported locales.
8 changes: 8 additions & 0 deletions CHANGELOG.d/2.17.0-image-and-summary-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 2.17.0 — Preserve table image boundaries and avoid duplicate summaries

## Fixed

- Inline or invalid images no longer split a table row or surrounding
paragraph into disconnected semantic units.
- Opening a post requests its summary once; the explicit retry action remains
the only path that requests another summary.
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.17.0] - 2026-08-19

### Added

- Opening a linked Event Lineage DAG node from a GNB-focused popup now
keeps Event Lineage focused and names Keyman and evaluation as the next
read. A home-list DAG walk does not add that focus or copy. No TEPP
theta is invented. No cited post, customer, week, or cutoff body is
invented (ADR 0097 / ADR 0096 / ADR 0016).

## [2.16.0] - 2026-08-19

### Added
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,9 @@ Open Ask Agent. After an authorized answer, cited posts are current. Open
a cited post: Event Lineage takes focus and names Keyman and evaluation
next (ADR 0096). A home-list open does not. Do not invent a theta or a
cited post.

## Event Lineage DAG walk (v2.17.0)

From a GNB-focused popup, open a linked Event Lineage node: Event Lineage
stays focused and names the new post as current (ADR 0097). A home-list
DAG walk does not. Do not invent a theta.
19 changes: 19 additions & 0 deletions backend/app/activity_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def _stream_key(post_id: str) -> str:
return f"activity:{post_id}"


def _operation_stream_key(account_id: str) -> str:
return f"operation:{account_id}"


def ticket_created_summary(ticket_title: str) -> str:
"""The ``summary`` field ``ticket_created`` producers must share."""
return f"Ticket created: {ticket_title}"
Expand Down Expand Up @@ -74,6 +78,21 @@ async def publish_activity_event(
)


async def publish_operation_event(
client: redis.Redis,
actor_account_id: str,
event_type: str,
summary: str,
) -> str:
"""Register an account operation that has no single owning post."""
return await client.xadd(
_operation_stream_key(actor_account_id),
_activity_fields(event_type, actor_account_id, summary),
maxlen=1000,
approximate=True,
)


def publish_activity_event_sync(
client: Any,
post_id: str,
Expand Down
101 changes: 79 additions & 22 deletions backend/app/entity_relationship_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
OrganizationRelationship,
)

# Keep the relationship-network response bounded independently of frontend caps.
_RELATIONSHIP_NETWORK_LIMIT = 100


async def ingest_post_entity_relationships(
conn: asyncpg.Connection,
Expand Down Expand Up @@ -90,7 +93,67 @@ def attach_resolved_entity_ids(
]


async def fetch_post_counterparties(conn: asyncpg.Connection, post_id: str) -> list[dict[str, Any]]:
def merge_relationship_network_rows(
rows: Sequence[Mapping[str, Any]],
candidates: Sequence[CorporateEntityCandidate],
) -> list[dict[str, Any]]:
"""Merge raw-name variants only when they share one unique catalog id."""
candidate_names = {candidate.corporate_entity_id: candidate.entity_name for candidate in candidates}
merged: dict[tuple[str, str], dict[str, Any]] = {}
for row in rows:
raw_name = str(row["counterparty_entity_name"])
corporate_entity_id = resolve_corporate_entity(raw_name, candidates)
key = ("entity", corporate_entity_id) if corporate_entity_id else ("name", raw_name)
entry = merged.setdefault(
key,
{
"counterparty_entity_name": candidate_names.get(corporate_entity_id, raw_name),
"corporate_entity_id": corporate_entity_id,
"total_post_count": 0,
"relationship_counts": {},
},
)
entry["total_post_count"] += int(row["total_post_count"])
relationships = row["relationships"]
if isinstance(relationships, str):
relationships = json.loads(relationships)
for relationship in relationships:
relationship_key = (
relationship["relationship_type_code"],
relationship["relationship_label"],
)
counts = entry["relationship_counts"]
counts[relationship_key] = counts.get(relationship_key, 0) + int(relationship["post_count"])

result: list[dict[str, Any]] = []
for entry in merged.values():
relationships = [
{
"relationship_type_code": code,
"relationship_label": label,
"post_count": post_count,
}
for (code, label), post_count in entry["relationship_counts"].items()
]
relationships.sort(key=lambda relationship: (-relationship["post_count"], relationship["relationship_type_code"]))
result.append(
{
"counterparty_entity_name": entry["counterparty_entity_name"],
"corporate_entity_id": entry["corporate_entity_id"],
"total_post_count": entry["total_post_count"],
"relationships": relationships,
"multi_role": len(relationships) > 1,
}
)
result.sort(key=lambda entry: (-entry["total_post_count"], entry["counterparty_entity_name"]))
return result[:_RELATIONSHIP_NETWORK_LIMIT]


async def fetch_post_counterparties(
conn: asyncpg.Connection,
post_id: str,
authorized_corporate_entity_ids: Sequence[str] = (),
) -> list[dict[str, Any]]:
"""Classified counterparties with a cataloged org id when the name resolves.

Unresolved names keep ``corporate_entity_id`` null -- a missing
Expand All @@ -108,7 +171,15 @@ async def fetch_post_counterparties(conn: asyncpg.Connection, post_id: str) -> l
""",
post_id,
)
candidate_rows = await conn.fetch("select corporate_entity_id, entity_name from corporate_entity")
candidate_rows = (
await conn.fetch(
"select corporate_entity_id, entity_name from corporate_entity "
"where corporate_entity_id = any($1::uuid[])",
list(authorized_corporate_entity_ids),
)
if authorized_corporate_entity_ids
else []
)
candidates = [
CorporateEntityCandidate(str(row["corporate_entity_id"]), row["entity_name"])
for row in candidate_rows
Comment on lines 171 to 185

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: Counterparty resolution candidate set narrowed to authorized entities

fetch_post_counterparties (entity_relationship_ingestion.py) and fetch_relationship_network (entity_relationship_ingestion.py) now build their CorporateEntityCandidate list only from corporate_entity rows whose id is in the authorized-entity set, replacing the previous unconditional select ... from corporate_entity. This is an intentional security narrowing (covered by the new test_counterparties_do_not_expose_unauthorized_catalog_entity), but it is also a behavioral change: a counterparty name that appears on a public post and matches a real catalog entity the caller is NOT affiliated with will now resolve to null instead of carrying the catalog id. That is the safer outcome (the related endpoints re-check ABAC anyway), but consumers relying on public-post names resolving to catalog ids will see fewer resolved links.

(Refers to this code)

Open in Devin Review

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

Expand Down Expand Up @@ -232,27 +303,13 @@ async def fetch_relationship_network(
""",
list(corporate_entity_ids),
)
candidate_rows = await conn.fetch("select corporate_entity_id, entity_name from corporate_entity")
candidate_rows = await conn.fetch(
"select corporate_entity_id, entity_name from corporate_entity "
"where corporate_entity_id = any($1::uuid[])",
list(corporate_entity_ids),
)
candidates = [
CorporateEntityCandidate(str(row["corporate_entity_id"]), row["entity_name"])
for row in candidate_rows
]
network: list[dict[str, Any]] = []
for row in rows:
relationships = (
json.loads(row["relationships"])
if isinstance(row["relationships"], str)
else row["relationships"]
)
network.append(
{
"counterparty_entity_name": row["counterparty_entity_name"],
"corporate_entity_id": resolve_corporate_entity(
row["counterparty_entity_name"], candidates
),
"total_post_count": row["total_post_count"],
"relationships": relationships,
"multi_role": len(relationships) > 1,
}
)
return network
return merge_relationship_network_rows(rows, candidates)
7 changes: 5 additions & 2 deletions backend/app/five_w1h_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from collections.abc import Callable, Sequence
from typing import Any

import asyncpg
Expand All @@ -18,6 +18,7 @@ async def load_five_w1h_slots(
conn: asyncpg.Connection,
post_id: str,
can_see_post: Callable[[asyncpg.Record], bool],
authorized_corporate_entity_ids: Sequence[str] = (),
) -> dict[str, Any]:
"""Build 5W1H from stored projections and visible lineage only."""
summary = await fetch_persisted_summary(conn, post_id) or {}
Expand All @@ -41,7 +42,9 @@ async def load_five_w1h_slots(
)
linked_titles = [row["post_title"] for row in rows if can_see_post(row)]

counterparties = await fetch_post_counterparties(conn, post_id)
counterparties = await fetch_post_counterparties(
conn, post_id, authorized_corporate_entity_ids
)
slots = assemble_five_w1h_slots(
roles=summary.get("roles_and_responsibilities", []),
key_events=summary.get("key_events", []),
Expand Down
Loading
Loading