Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
943ab63
ci: run buyer-surface cutoff repair test-first
seonghobae Aug 19, 2026
8e5be3e
ci: cover direct and DAG cutoff navigation
seonghobae Aug 19, 2026
3f4c41e
ci: restore canonical tests workflow
seonghobae Aug 19, 2026
4b80c87
ci: remove temporary cutoff repair workflow
seonghobae Aug 19, 2026
c96fa28
ci: repair analysis-run DAG cutoff context
seonghobae Aug 19, 2026
c23b81a
ci: repair analysis-run cutoff navigation
seonghobae Aug 19, 2026
b78b8c5
chore: remove one-shot repair workflow
seonghobae Aug 19, 2026
be0a544
chore: remove duplicate cutoff repair workflow
seonghobae Aug 19, 2026
aad290b
ci: execute PR 264 cutoff repair
seonghobae Aug 19, 2026
8cf3491
ci: run PR 264 cutoff repair on branch push
seonghobae Aug 19, 2026
4e4dae5
ci: repair ontology-grounded semantic paths on PR 264
seonghobae Aug 19, 2026
1b73e66
ci: run cutoff repair through canonical tests workflow
seonghobae Aug 19, 2026
796af57
ci: stage deterministic PR 264 cutoff repair
seonghobae Aug 19, 2026
a409895
ci: execute deterministic cutoff repair
seonghobae Aug 19, 2026
821432a
fix(ui): preserve analysis-run cutoff across DAG navigation
github-actions[bot] Aug 19, 2026
c616a4a
ci: guard PR 264 against the unrelated ontology writer
seonghobae Aug 19, 2026
f74377f
ci: restore canonical PR 264 verification
seonghobae Aug 19, 2026
3e37f02
ci: add direct ontology semantic-path repair
seonghobae Aug 19, 2026
f010d9b
ci: remove completed cutoff repair helper
seonghobae Aug 19, 2026
6cf7f3b
ci: replace stale semantic repair workflow
seonghobae Aug 19, 2026
f68b8d7
ci: execute ontology semantic-path repair
seonghobae Aug 19, 2026
af9d25a
test: stage ontology semantic-path repair
seonghobae Aug 19, 2026
06a5b37
ci: run test-first ontology semantic-path repair
seonghobae Aug 19, 2026
8431998
ci: move semantic-path repair out of PR 264
seonghobae Aug 19, 2026
4e601f6
ci: remove relocated semantic-path repair helper
seonghobae Aug 19, 2026
757757b
ci: consolidate PR 264 Knowledge Graph semantic repair
seonghobae Aug 19, 2026
2234fc7
ci: keep semantic repair exact-head safe
seonghobae Aug 19, 2026
0664679
test: align semantic repair fixtures with production schema
seonghobae Aug 19, 2026
35143d7
test: require TEPP-backed project history evidence
seonghobae Aug 20, 2026
7f726a3
test(ui): require the TEPP project timeline answer surface
seonghobae Aug 20, 2026
d7f4f14
feat: add the strict TEPP project-history client
seonghobae Aug 20, 2026
ff69cfa
feat: select authorized evidence for TEPP project histories
seonghobae Aug 20, 2026
101dcf6
feat: open Customer master related posts onto Event Lineage (v2.15.0)
seonghobae Aug 19, 2026
f6fccb1
feat(ui): add the accessible TEPP project history timeline
seonghobae Aug 20, 2026
df0ee78
style(ui): present the TEPP timeline responsively
seonghobae Aug 20, 2026
ce41de1
docs(storybook): demonstrate the minimum TEPP buyer timeline
seonghobae Aug 20, 2026
59b8153
fix: preserve Event Lineage focus after related loads
seonghobae Aug 20, 2026
43070dd
feat: open Ask Agent cited posts onto Event Lineage (v2.16.0)
seonghobae Aug 19, 2026
674b33e
feat: keep GNB Event Lineage focus on a linked DAG node (v2.17.0)
seonghobae Aug 19, 2026
8be6cca
fix(ui): preserve analysis-run cutoff across DAG navigation
seonghobae Aug 20, 2026
f9d750b
ci: materialize the bounded PR 281 integration patch
seonghobae Aug 20, 2026
2c69d97
ci: verify and publish the PR 281 TEPP integration
seonghobae Aug 20, 2026
d1e7a19
chore: restack project-history TDD on exact buyer parent
seonghobae Aug 20, 2026
3ea68e7
ci: run project-history repair on exact branch pushes
seonghobae Aug 20, 2026
88b1ac6
ci: bound project-history repair runtime
seonghobae Aug 20, 2026
fe8c2f1
ci: expose project-history repair to PR exact-head runs
seonghobae Aug 20, 2026
a2a677d
ci: trigger project-history verification on PR edits
seonghobae Aug 20, 2026
ca91dfe
test(red): define evidence-bound project history projection
seonghobae Aug 20, 2026
f174743
feat: build visible project-history evidence paths
seonghobae Aug 20, 2026
ed01445
test(red): require ABAC-safe project history repository
seonghobae Aug 20, 2026
854081f
feat: load project histories through ABAC-safe evidence reads
seonghobae Aug 20, 2026
c57f1cd
test(red): require reversible project-history lookup indexes
seonghobae Aug 20, 2026
891eadd
feat: index and roll back exact project-history lookups
seonghobae Aug 20, 2026
cbd1c08
test: prove project history authorization in PostgreSQL
seonghobae Aug 20, 2026
4761443
test(red): retain the authorized focus in bounded histories
seonghobae Aug 20, 2026
5af1330
fix: keep the current event in truncated project histories
seonghobae Aug 20, 2026
5bc174d
test(red): define the authorized project-history HTTP contract
seonghobae Aug 20, 2026
244388a
feat: expose a strict project-history API router
seonghobae Aug 20, 2026
f66e5c8
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
194c631
test(red): define the accessible project-history timeline
seonghobae Aug 20, 2026
5b4070c
feat: render accessible evidence-bound project histories
seonghobae Aug 20, 2026
60c741e
test(red): require an explicit project-history disclosure
seonghobae Aug 20, 2026
8ea0f73
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
51ea771
chore: remove self-modifying project history repair
seonghobae Aug 20, 2026
d082118
merge: restack onto feat/event-lineage-node-keeps-gnb-focus-v2170
seonghobae Aug 20, 2026
2b28927
Merge remote-tracking branch 'origin/feat/tepp-project-history-buyer-…
seonghobae Aug 20, 2026
cc2435e
Merge commit '2b289275757fc4629a7f59c86e316fd3fb84ea1c' of https://gi…
seonghobae Aug 20, 2026
ba3101b
Merge commit '86c5f88826b9554a642f0ab8a9f51288da19820d' of https://gi…
seonghobae Aug 20, 2026
669dfdf
fix: use canonical popup focus contract
seonghobae Aug 20, 2026
e9f1185
merge: restack project history onto Global Ask verification
seonghobae Aug 20, 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: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ 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
Expand Down
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
require_summary_source_body,
)
from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL
from backend.app.project_history_api import router as project_history_router
from backend.app.demo_scope import (
fetch_demo_corporate_entity_ids,
has_real_source_context,
Expand Down Expand Up @@ -257,6 +258,7 @@ async def lifespan(app: FastAPI):
allow_methods=["GET", "POST", "PATCH"],
allow_headers=["Authorization"],
)
app.include_router(project_history_router)


def _require_post_read(account: CurrentAccount) -> None:
Expand Down
220 changes: 220 additions & 0 deletions backend/app/project_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
"""ABAC-safe PostgreSQL projection for Buyer project histories."""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Any, Protocol

from backend.app.post_eligibility import SOURCE_POST_ELIGIBILITY_SQL
from lineageweave.project_history import build_project_history_projection, normalize_project_key

PROJECT_HISTORY_DEFAULT_LIMIT = 64
PROJECT_HISTORY_MAXIMUM_LIMIT = 128


class ProjectHistoryConnection(Protocol):
"""Minimal asynchronous query port required by this repository."""

async def fetch(self, query: str, *args: object) -> Sequence[Mapping[str, Any]]:
"""Execute a bounded read query and return mapping-like rows."""

...


_ELIGIBILITY = SOURCE_POST_ELIGIBILITY_SQL.format(alias="post")
_PROJECT_MATCH = """
(
lower(normalize(btrim(coalesce(post.source_project_code, '')), NFKC)) = $1
or lower(normalize(btrim(coalesce(post.source_project_name, '')), NFKC)) = $1
or exists (
select 1
from post_project_mention mention
where mention.post_id = post.post_id
and (
lower(normalize(btrim(mention.project_key), NFKC)) = $1
or lower(normalize(btrim(mention.project_name), NFKC)) = $1
)
)
)
"""
_EVENT_SQL = f"""
select post.post_id,
post.post_title,
post.created_at,
post.voc_type_code,
post.source_stage_code,
post.source_detail_state_code
from source_post post
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($2::text[]))
and {_ELIGIBILITY}
and post.created_at <= $3
and {_PROJECT_MATCH}
order by post.created_at, post.post_id
limit $4
"""
_FOCUS_SQL = f"""
select post.post_id,
post.post_title,
post.created_at,
post.voc_type_code,
post.source_stage_code,
post.source_detail_state_code
from source_post post
where (post.visibility_code = 'public'
or post.corporate_entity_id::text = any($2::text[]))
and {_ELIGIBILITY}
and post.created_at <= $3
and post.post_id = $4::uuid
and {_PROJECT_MATCH}
limit 1
"""
_MATCH_SQL = """
select post.post_id,
'source_project_code'::text as match_kind_code,
post.source_project_code as matched_value,
null::numeric as confidence,
null::text as ontology_iri,
'source_post.source_project_code'::text as provenance
from source_post post
where post.post_id = any($1::uuid[])
and lower(normalize(btrim(coalesce(post.source_project_code, '')), NFKC)) = $2
union all
select post.post_id,
'source_project_name'::text,
post.source_project_name,
null::numeric,
null::text,
'source_post.source_project_name'::text
from source_post post
where post.post_id = any($1::uuid[])
and lower(normalize(btrim(coalesce(post.source_project_name, '')), NFKC)) = $2
union all
select mention.post_id,
'semantic_project_key'::text,
mention.project_key,
mention.confidence,
mention.ontology_iri,
'post_project_mention.project_key'::text
from post_project_mention mention
where mention.post_id = any($1::uuid[])
and lower(normalize(btrim(mention.project_key), NFKC)) = $2
union all
select mention.post_id,
'semantic_project_name'::text,
mention.project_name,
mention.confidence,
mention.ontology_iri,
'post_project_mention.project_name'::text
from post_project_mention mention
where mention.post_id = any($1::uuid[])
and lower(normalize(btrim(mention.project_name), NFKC)) = $2
order by post_id, match_kind_code, matched_value
"""
_ROLE_SQL = """
select role.post_id,
role.actor_name,
role.responsibility,
role.actor_type_code,
role.affiliated_organization_name,
role.cataloged_person_id,
role.cataloged_team_id,
role.cataloged_corporate_entity_id
from post_summary_role role
where role.post_id = any($1::uuid[])
order by role.post_id, role.actor_type_code, role.actor_name, role.responsibility
"""
_EDGE_SQL = """
select edge.parent_post_id, edge.child_post_id, edge.fused_score
from post_lineage_edge edge
where edge.parent_post_id = any($1::uuid[])
and edge.child_post_id = any($1::uuid[])
order by edge.child_post_id, edge.parent_post_id
"""


class ProjectHistoryNotFound(LookupError):
"""No authorized project history matched the requested identity."""


async def fetch_project_history_projection(
conn: ProjectHistoryConnection,
*,
project_key: str,
focus_post_id: str | None,
knowledge_cutoff: datetime,
corporate_entity_ids: Sequence[str],
limit: int = PROJECT_HISTORY_DEFAULT_LIMIT,
) -> dict[str, Any]:
"""Return a bounded project history from authorized PostgreSQL evidence.

The query applies source eligibility, cutoff, and ABAC before selecting
event IDs. All subsequent match, role, and lineage reads are constrained to
that visible ID set, so hidden rows cannot affect counts, transitions, or
prior-history paths. An authorized focus event remains in a truncated
projection even when it falls beyond the earliest page.
"""

if limit < 1 or limit > PROJECT_HISTORY_MAXIMUM_LIMIT:
raise ValueError("project history limit is outside the supported bound")
normalized_key = normalize_project_key(project_key)
rows = list(
await conn.fetch(
_EVENT_SQL,
normalized_key,
list(corporate_entity_ids),
knowledge_cutoff,
limit + 1,
)
)
truncated = len(rows) > limit
event_rows = rows[:limit]
if not event_rows:
raise ProjectHistoryNotFound(project_key)
visible_ids = [str(row["post_id"]) for row in event_rows]
if focus_post_id is not None and focus_post_id not in set(visible_ids):
focus_rows = list(
await conn.fetch(
_FOCUS_SQL,
normalized_key,
list(corporate_entity_ids),
knowledge_cutoff,
focus_post_id,
)
)
if not focus_rows:
raise ProjectHistoryNotFound(project_key)
truncated = True
event_rows = (event_rows[: limit - 1] if limit > 1 else []) + [focus_rows[0]]
event_rows.sort(key=lambda row: (row["created_at"], str(row["post_id"])))
visible_ids = [str(row["post_id"]) for row in event_rows]

match_rows, role_rows, edge_rows = await _fetch_project_children(
conn,
visible_ids=visible_ids,
normalized_key=normalized_key,
)
return build_project_history_projection(
project_key=project_key,
focus_event_id=focus_post_id,
event_rows=event_rows,
match_rows=match_rows,
role_rows=role_rows,
edge_rows=edge_rows,
truncated=truncated,
)


async def _fetch_project_children(
conn: ProjectHistoryConnection,
*,
visible_ids: Sequence[str],
normalized_key: str,
) -> tuple[list[Mapping[str, Any]], list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Fetch only child evidence whose endpoints are already authorized."""

matches = list(await conn.fetch(_MATCH_SQL, list(visible_ids), normalized_key))
roles = list(await conn.fetch(_ROLE_SQL, list(visible_ids)))
edges = list(await conn.fetch(_EDGE_SQL, list(visible_ids)))
return matches, roles, edges
Loading