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
5 changes: 5 additions & 0 deletions CHANGELOG.d/0.97.0-analysis-run-open-period-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 0.97.0 Open the seeded period report from its analysis run

Open Period report · Succeeded · Demo Corp, then Open period report
2026-W02. The report period field is focused. Failed rows stay
closed. Mean θ stays on the report panel.
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).

## [0.97.0] - 2026-08-17

### Added

- A Succeeded period-report analysis run now opens the scored week.
After `make seed`, open **Period report · Succeeded · Demo Corp**
and click **Open period report 2026-W02**: the report period field
is focused on that week. Failed rows stay closed. Mean θ stays on
the report panel. No TEPP theta is invented.

## [0.96.0] - 2026-08-17

### Added
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ frozen cutoff bag (ADR 0021 / ADR 0023) or submits TEPP through
envelope is Failed. Failed TEPP is terminal — request a new run,
then start. Do not invent a theta. Hover the Result prefix to read
the parent-choice digest.
After `make seed`, open **Period report · Succeeded · Demo Corp**
to confirm the cutoff posts; mean θ stays on the period-report panel.
After `make seed`, open **Period report · Succeeded · Demo Corp**,
then **Open period report 2026-W02**. The report period field is
focused. Mean θ stays on the period-report panel.
2 changes: 2 additions & 0 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ async def _serialize_runs(
}
if row["scope_entity_name"]:
item["scope_entity_name"] = row["scope_entity_name"]
if row["scope_key"]:
item["scope_key"] = row["scope_key"]
payload.append(item)
return payload

Expand Down
6 changes: 4 additions & 2 deletions docs/adr/0024-seed-period-report-analysis-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ on a path that is not allowed to (ADR 0021 / ADR 0022 / ADR 0023).

- `_seed_demo_period_report` still builds the calibrated report first.
- `_seed_demo_report_run` then inserts `analysis_run_report` on the
same Demo Corp snapshot, scoped to the same corporate entity.
same Demo Corp snapshot, scoped to the same corporate entity, with
`scope_key` `2026-W02` so the home detail can open that week.
- The lifecycle is Pending → Running → Succeeded because the report
tables already hold the scored period. The run row stores only
registry digests and counts — never a theta, item bank, or provider
Expand All @@ -45,7 +46,8 @@ on a path that is not allowed to (ADR 0021 / ADR 0022 / ADR 0023).

After `make seed`, Demo Analyst opens Analysis runs and sees
**Period report · Succeeded · Demo Corp** next to the lineage and TEPP
rows. Opening it shows the cutoff posts. Mean θ remains on the
rows. Opening it shows the cutoff posts and **Open period report
2026-W02** (the week stored on `scope_key`). Mean θ remains on the
period-report panel. Re-seed is idempotent on
`demo-report-seed-2026-w02`.

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.96.0",
"version": "0.97.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
63 changes: 37 additions & 26 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ describe("App, authenticated", () => {
);
}
if (url.endsWith("/api/analysis-runs/run-demo-report")) {
const reportSucceeded = Boolean(options?.succeededReportRun);
const reportSucceeded = !options?.failedReportRun;
return Promise.resolve(
jsonResponse({
analysis_run_id: "run-demo-report",
Expand All @@ -188,6 +188,7 @@ describe("App, authenticated", () => {
scope_kind_code: "analysis_scope_corporate_entity",
scope_kind_label: "Corporate entity",
scope_entity_name: "Demo Corp",
scope_key: "2026-W02",
status_code: reportSucceeded ? "analysis_status_succeeded" : "analysis_status_failed",
status_label: reportSucceeded ? "Succeeded" : "Failed",
knowledge_cutoff: "2026-01-12T12:00:00Z",
Expand Down Expand Up @@ -665,31 +666,28 @@ describe("App, authenticated", () => {
},
],
},
...(options?.failedReportRun || options?.succeededReportRun
? [
{
analysis_run_id: "run-demo-report",
run_kind_code: "analysis_run_report" as const,
run_kind_label: "Period report",
scope_kind_code: "analysis_scope_corporate_entity",
scope_kind_label: "Corporate entity",
scope_entity_name: "Demo Corp",
status_code: options?.succeededReportRun
? ("analysis_status_succeeded" as const)
: ("analysis_status_failed" as const),
status_label: options?.succeededReportRun ? "Succeeded" : "Failed",
knowledge_cutoff: "2026-01-12T12:00:00Z",
requested_at: "2026-01-12T12:38:00Z",
source_counts: [
{
count_type_code: "analysis_count_document",
count_type_label: "Documents",
count_value: 3,
},
],
},
]
: []),
{
analysis_run_id: "run-demo-report",
run_kind_code: "analysis_run_report" as const,
run_kind_label: "Period report",
scope_kind_code: "analysis_scope_corporate_entity",
scope_kind_label: "Corporate entity",
scope_entity_name: "Demo Corp",
scope_key: "2026-W02",
status_code: options?.failedReportRun
? ("analysis_status_failed" as const)
: ("analysis_status_succeeded" as const),
status_label: options?.failedReportRun ? "Failed" : "Succeeded",
knowledge_cutoff: "2026-01-12T12:00:00Z",
requested_at: "2026-01-12T12:38:00Z",
source_counts: [
{
count_type_code: "analysis_count_document",
count_type_label: "Documents",
count_value: 3,
},
],
},
],
}),
);
Expand Down Expand Up @@ -1904,6 +1902,7 @@ describe("App, authenticated", () => {
const list = screen.getByRole("list", { name: "Analysis runs" });
expect(list).toHaveTextContent("Lineage reconstruction · Succeeded · Demo Corp");
expect(list).toHaveTextContent("TEPP measurement · Failed · Demo Corp");
expect(list).toHaveTextContent("Period report · Succeeded · Demo Corp");
expect(list).toHaveTextContent(
"Open this run to see why it failed, then connect the measurement service and re-run.",
);
Expand Down Expand Up @@ -2114,6 +2113,15 @@ describe("App, authenticated", () => {
name: "Open live post: Public post",
}),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Open period report 2026-W02" })).toBeInTheDocument();

const periodInput = screen.getByLabelText("Report period");
await userEvent.clear(periodInput);
await userEvent.type(periodInput, "2026-W03");
expect(periodInput).toHaveValue("2026-W03");
await userEvent.click(screen.getByRole("button", { name: "Open period report 2026-W02" }));
expect(periodInput).toHaveValue("2026-W02");
expect(periodInput).toHaveFocus();
});

it("does not tell a failed period report to connect the measurement service", async () => {
Expand All @@ -2131,6 +2139,9 @@ describe("App, authenticated", () => {

await userEvent.click(reportButton);
expect(screen.queryByRole("button", { name: "Start reconstruction" })).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Open period report 2026-W02" }),
).not.toBeInTheDocument();
expect(
await screen.findByText(
"No posts were available at this cutoff for the period report. Open a later run, or ask an administrator to capture a newer snapshot.",
Expand Down
64 changes: 59 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,28 @@ function analysisRunCanRequestTeppRetry(run: AnalysisRun): boolean {
return run.run_kind_code === "analysis_run_tepp" && run.status_code === "analysis_status_failed";
}

const REPORT_PERIOD_KEY = /^\d{4}-W\d{2}$/;

/**
* Period code stored on a succeeded report run's scope key.
*
* That key is a week label, not a theta. Missing or malformed keys
* stay closed so we do not invent a period.
*/
function analysisRunReportPeriod(run: AnalysisRun): string | null {
if (run.run_kind_code !== "analysis_run_report") {
return null;
}
if (run.status_code !== "analysis_status_succeeded") {
return null;
}
const key = run.scope_key;
if (!key || !REPORT_PERIOD_KEY.test(key)) {
return null;
}
return key;
}

/**
* Open options for a reconstructed parent or child.
*
Expand All @@ -1699,9 +1721,11 @@ function analysisRunPostOpenOptions(run: AnalysisRun, postId: string): SelectPos
function AnalysisRunsPanel({
accessToken,
onSelectPost,
onSelectReportPeriod,
}: {
accessToken: string;
onSelectPost: (postId: string, options?: SelectPostOptions) => void;
onSelectReportPeriod?: (periodCode: string) => void;
}) {
const [runs, setRuns] = useState<AnalysisRun[] | null>(null);
const [selected, setSelected] = useState<AnalysisRun | null>(null);
Expand Down Expand Up @@ -1872,6 +1896,21 @@ function AnalysisRunsPanel({
{requesting ? "Recording the run..." : "Request a new TEPP measurement"}
</button>
)}
{analysisRunReportPeriod(selected) && onSelectReportPeriod && (
<button
className="keyman-select"
aria-label={`Open period report ${analysisRunReportPeriod(selected)}`}
onClick={() => {
const periodCode = analysisRunReportPeriod(selected);
if (periodCode) {
onSelectReportPeriod(periodCode);
document.getElementById("report-period")?.focus();
}
}}
>
Open period report {analysisRunReportPeriod(selected)}
</button>
)}
{selected.reconstructed_edges && selected.reconstructed_edges.length > 0 && (
<ul aria-label="Reconstructed lineage edges">
{selected.reconstructed_edges.map((edge) => (
Expand Down Expand Up @@ -2020,13 +2059,16 @@ function ReportsPanel({
accessToken,
canRebuild,
onSelectPost,
period,
onSelectPeriod,
}: {
accessToken: string;
canRebuild: boolean;
onSelectPost: (postId: string) => void;
period: string;
onSelectPeriod: (periodCode: string) => void;
}) {
const [grouping, setGrouping] = useState("process_unit");
const [period, setPeriod] = useState("2026-W02");
const [payload, setPayload] = useState<PeriodReports | null>(null);
const [index, setIndex] = useState<PeriodReportIndex | null>(null);
const [comparison, setComparison] = useState<PeriodComparison | null>(null);
Expand Down Expand Up @@ -2096,9 +2138,10 @@ function ReportsPanel({
<label>
Period
<input
id="report-period"
aria-label="Report period"
value={period}
onChange={(event) => setPeriod(event.target.value)}
onChange={(event) => onSelectPeriod(event.target.value)}
/>
</label>
</div>
Expand Down Expand Up @@ -2128,7 +2171,7 @@ function ReportsPanel({
<button
className="post-list-item"
aria-label={`Open report period ${row.period_code}`}
onClick={() => setPeriod(row.period_code)}
onClick={() => onSelectPeriod(row.period_code)}
>
<span className="ticket-title">
{row.period_code}: mean θ {row.mean_theta.toFixed(2)}
Expand Down Expand Up @@ -2227,6 +2270,7 @@ function PostList({ accessToken }: { accessToken: string }) {
const [canRebuild, setCanRebuild] = useState(false);
const [rebuilding, setRebuilding] = useState(false);
const [rebuildError, setRebuildError] = useState<string | null>(null);
const [reportPeriod, setReportPeriod] = useState("2026-W02");

function selectPost(postId: string, options?: SelectPostOptions) {
setSelectedPostId(postId);
Expand Down Expand Up @@ -2268,8 +2312,18 @@ function PostList({ accessToken }: { accessToken: string }) {
return (
<>
<CalendarPanel accessToken={accessToken} onSelectPost={selectPost} />
<AnalysisRunsPanel accessToken={accessToken} onSelectPost={selectPost} />
<ReportsPanel accessToken={accessToken} canRebuild={canRebuild} onSelectPost={selectPost} />
<AnalysisRunsPanel
accessToken={accessToken}
onSelectPost={selectPost}
onSelectReportPeriod={setReportPeriod}
/>
<ReportsPanel
accessToken={accessToken}
canRebuild={canRebuild}
onSelectPost={selectPost}
period={reportPeriod}
onSelectPeriod={setReportPeriod}
/>
<section className="popup-section lineage-home">
<div className="lineage-home-header">
<h2>Event Lineage</h2>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ export interface AnalysisRun {
scope_kind_code: string;
scope_kind_label: string;
scope_entity_name?: string;
scope_key?: string;
status_code: AnalysisRunStatusCode | null;
status_label: string | null;
failure_code?: string;
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.96.0"
__version__ = "0.97.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.96.0"
version = "0.97.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
9 changes: 5 additions & 4 deletions scripts/seed_demo_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,11 +1609,12 @@ def _seed_demo_report_run(cur, requested_by_account_id, corporate_entity_id) ->
cur.execute(
"""
insert into analysis_run_scope
(analysis_run_id, scope_kind_code, corporate_entity_id)
values (%s, 'analysis_scope_corporate_entity', %s)
on conflict (analysis_run_id) do nothing
(analysis_run_id, scope_kind_code, corporate_entity_id, scope_key)
values (%s, 'analysis_scope_corporate_entity', %s, %s)
on conflict (analysis_run_id) do update
set scope_key = excluded.scope_key
""",
(run_id, corporate_entity_id),
(run_id, corporate_entity_id, "2026-W02"),
)
for ordinal, status, occurred in (
(1, "analysis_status_pending", "2026-01-12T12:39:00Z"),
Expand Down
17 changes: 17 additions & 0 deletions tests/test_seed_report_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ def test_seed_demo_report_run_inserts_succeeded_report_without_a_theta() -> None
for event_params in status_params
)
assert not any("analysis_run_outbox" in sql for sql in cursor.statements)
scope_params = [
params
for sql, params in zip(cursor.statements, cursor.params, strict=True)
if "insert into analysis_run_scope" in sql
]
assert any(
event_params is not None and "2026-W02" in event_params
for event_params in scope_params
)
assert not any(
event_params is not None
and any(
isinstance(value, str) and ("theta" in value.lower() or "θ" in value)
for value in event_params
)
for event_params in scope_params
)
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.