Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@ read of the #89 registry. `GET /api/analysis-runs` and
in SQL: the requester always sees their own run; a corporate-entity or
process-unit scope is visible only to affiliated accounts; a
thread-group scope is visible only when the account can already see a
post in that group; `all_visible` is requester-only. Hidden runs 404.
post in that group; `all_visible` is requester-only. Hidden runs 404. Detail also lists ABAC-visible post titles in the
run's scope so a buyer can open a post without seeing hidden rows.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, counts, status history)
without exposing a DSN or raw record. Status history is detail-only
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.82.0-analysis-run-post-clickthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.82.0 analysis-run post click-through

Detail lists ABAC-visible post titles in the run scope. Hidden
other-corp private posts never appear. Synthetic titles only.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ 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).

## [0.82.0] - 2026-08-16

### Added

- Analysis-run detail lists ABAC-visible posts in the run's scope.
After `make seed`, the Demo Corp lineage run opens the Demo public
post. Hidden other-corp private posts never appear. List payloads
stay aggregates-only.

## [0.81.0] - 2026-08-16

### Added
Expand Down
57 changes: 57 additions & 0 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
run.code_revision_sha,
scope.scope_kind_code,
scope.corporate_entity_id,
scope.process_unit_id,
scope.scope_key,
corp.entity_name as scope_entity_name,
status.status_code,
status.failure_code
Expand Down Expand Up @@ -217,4 +219,59 @@ async def fetch_visible_analysis_run(
if row["failure_code"]:
detail["failure_code"] = row["failure_code"]
detail["status_history"] = await _status_history(conn, analysis_run_id)
detail["visible_posts"] = await fetch_visible_scope_posts(
conn,
row["scope_kind_code"],
row["corporate_entity_id"],
row["process_unit_id"],
row["scope_key"],
affiliated_entity_ids,
)
return detail


async def fetch_visible_scope_posts(
conn: asyncpg.Connection,
scope_kind_code: str,
corporate_entity_id: Any,
process_unit_id: Any,
scope_key: str | None,
affiliated_entity_ids: list[str],
) -> list[dict[str, str]]:
"""ABAC-visible post titles in the run's scope -- never a hidden body."""
if scope_kind_code == "analysis_scope_corporate_entity" and corporate_entity_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where corporate_entity_id = $1 "
"order by created_at, post_title",
corporate_entity_id,
)
elif scope_kind_code == "analysis_scope_process_unit" and process_unit_id:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where process_unit_id = $1 "
"order by created_at, post_title",
process_unit_id,
)
elif scope_kind_code == "analysis_scope_thread_group" and scope_key:
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post where thread_group_key = $1 "
"order by created_at, post_title",
scope_key,
)
elif scope_kind_code == "analysis_scope_all_visible":
rows = await conn.fetch(
"select post_id, post_title, visibility_code, corporate_entity_id "
"from source_post order by created_at, post_title"
)
else:
return []
affiliated = {str(entity_id) for entity_id in affiliated_entity_ids}
posts: list[dict[str, str]] = []
for row in rows:
visible = row["visibility_code"] == "public" or str(row["corporate_entity_id"]) in affiliated
if not visible:
continue
posts.append({"post_id": str(row["post_id"]), "post_title": row["post_title"]})
return posts
4 changes: 4 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,11 @@ def test_analysis_runs_are_labeled_aggregates_and_hide_other_scopes(
"2026-01-12T12:33",
]
assert all("failure_code" not in event for event in history)
titles = {post["post_title"] for post in body["visible_posts"]}
assert "Own-corp private post" in titles
assert "Other-corp private post" not in titles
assert "postgresql://" not in str(body)
assert "visible_posts" not in visible

hidden = client.get(
f"/api/analysis-runs/{seeded_db['hidden_run_id']}",
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.81.0",
"version": "0.82.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ describe("App, authenticated", () => {
count_value: 3,
},
],
visible_posts: [{ post_id: "post-1", post_title: "Public post" }],
status_history: [
{
status_ordinal: 1,
Expand Down Expand Up @@ -1389,7 +1390,11 @@ describe("App, authenticated", () => {
expect(history).toHaveTextContent("Pending 2026-01-12 12:31");
expect(history).toHaveTextContent("Running 2026-01-12 12:32");
expect(history).toHaveTextContent("Succeeded 2026-01-12 12:33");
expect(screen.getByRole("button", { name: "Open run post: Public post" })).toBeInTheDocument();
expect(screen.queryByText(/postgresql:\/\//)).not.toBeInTheDocument();

await userEvent.click(screen.getByRole("button", { name: "Open run post: Public post" }));
await waitFor(() => expect(screen.getByText("The full body text.")).toBeInTheDocument());
});

it("shows the calibrated period-report mean theta on the home page", async () => {
Expand Down
25 changes: 23 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,13 @@ function analysisRunCaption(run: AnalysisRun): string {
.join(" · ");
}

function AnalysisRunsPanel({ accessToken }: { accessToken: string }) {
function AnalysisRunsPanel({
accessToken,
onSelectPost,
}: {
accessToken: string;
onSelectPost: (postId: string) => void;
}) {
const [runs, setRuns] = useState<AnalysisRun[] | null>(null);
const [selected, setSelected] = useState<AnalysisRun | null>(null);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -1442,6 +1448,21 @@ function AnalysisRunsPanel({ accessToken }: { accessToken: string }) {
))}
</ol>
)}
{selected.visible_posts && selected.visible_posts.length > 0 && (
<ul aria-label="Posts in this analysis run">
{selected.visible_posts.map((post) => (
<li key={post.post_id}>
<button
className="keyman-select"
aria-label={`Open run post: ${post.post_title}`}
onClick={() => onSelectPost(post.post_id)}
>
{post.post_title}
</button>
</li>
))}
</ul>
)}
</div>
)}
</section>
Expand Down Expand Up @@ -1736,7 +1757,7 @@ function PostList({ accessToken }: { accessToken: string }) {
return (
<>
<CalendarPanel accessToken={accessToken} onSelectPost={setSelectedPostId} />
<AnalysisRunsPanel accessToken={accessToken} />
<AnalysisRunsPanel accessToken={accessToken} onSelectPost={setSelectedPostId} />
<ReportsPanel accessToken={accessToken} canRebuild={canRebuild} onSelectPost={setSelectedPostId} />
<section className="popup-section lineage-home">
<div className="lineage-home-header">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ export interface AnalysisRun {
requested_at: string;
source_counts: AnalysisRunCount[];
status_history?: AnalysisRunStatusEvent[];
visible_posts?: { post_id: string; post_title: string }[];
}

export function fetchAnalysisRuns(accessToken: string): Promise<{ analysis_runs: AnalysisRun[] }> {
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
"sentence_excerpts",
]

__version__ = "0.81.0"
__version__ = "0.82.0"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "0.81.0"
version = "0.82.0"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.