Skip to content
Closed
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
12 changes: 10 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,15 +467,23 @@ run's scope whose `created_at` is at or before the run
`knowledge_cutoff`, so a later post cannot appear inside a historical
reconstruction (ADR 0016). Detail also returns revision and
configuration digest prefixes.
`make seed` also records a TEPP measurement run through
`tepp_client` on that same snapshot; the default transport is
unavailable, so that run is Failed rather than a fabricated score.
The home list is clickable: `GET /api/analysis-runs/{id}` fills a
labeled detail (cutoff, requested date, counts, status history, digests)
without exposing a DSN or raw record. Status history is detail-only
and uses lookup labels plus occurrence times; a failure event keeps
its machine `failure_code` rather than an invented caption. The
its machine `failure_code` rather than an invented caption. Failed
list rows add a next-action line (open the run, then connect the
measurement service) so `tepp_not_available` is not mistaken for a
calibrated negative result. The
payload is lookup labels plus non-negative aggregate counts -- never
source SQL, a DSN, a raw record, or a provider body. After `make seed`,
Demo Analyst and Demo Admin see "Lineage reconstruction · Succeeded ·
Demo Corp" with "3 documents" and Pending / Running / Succeeded times.
Demo Corp" with "3 documents" and Pending / Running / Succeeded times,
and "TEPP measurement · Failed · Demo Corp" whose detail history ends
in Failed / `tepp_not_available`.

## Phase 6a: fast-mlsirm dependency + Rust toolchain (infra only)

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.d/0.84.0-tepp-analysis-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 0.84.0 TEPP analysis-run seed

Seed writes `analysis_run_tepp` via `tepp_client` on the shared Demo
Corp snapshot. The home list shows Failed and the next action; detail
history keeps `tepp_not_available`. Missing transport is not a fake
measurement.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ 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.84.0] - 2026-08-16

### Added

- `make seed` records a Demo Corp TEPP measurement run through
`tepp_client` on the same snapshot as the lineage run (ADR 0013).
The default transport is unavailable, so the home list shows
"TEPP measurement · Failed · Demo Corp" and tells the operator to
open the run, then connect the measurement service. Detail history
keeps `tepp_not_available` -- never a fabricated theta. TEPP stays
a wire client, not a local psychometric engine. `make seed` skips
snapshot-count inserts once counts exist so a re-run does not hit
the freeze trigger.

## [0.83.0] - 2026-08-16

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion docs/adr/0013-normalized-analysis-run-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ Acceptance requires:
read-only administrator surface.
3. Add a normalized PostgreSQL outbox and Valkey delivery worker.
4. Add TEPP and contextual-orchestrator adapters only after their versioned
contracts are present on reviewed main branches.
contracts are present on reviewed main branches. Seed now records a
Failed TEPP run through `tepp_client` on the shared Demo Corp snapshot;
a live transport remains a later slice.
5. Execute private actual-data analysis and store only signed aggregate and
reproducibility manifests outside public source control.
6. Run browser E2E through real OIDC, product navigation, and evidence drill-down.
Expand Down
14 changes: 9 additions & 5 deletions docs/adr/0014-authorized-analysis-run-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ LineageWeave owns a fail-closed read projection of the #89 registry:

## Consequences

`make seed` writes one synthetic Demo Corp lineage run so the existing
React home page can show Analysis runs without a second application.
The detail now shows the legal lifecycle the registry already stored.
Write/rebuild APIs, TEPP submission, and a fuller Analysis Run Console
remain later slices.
`make seed` writes one synthetic Demo Corp lineage run and one TEPP
run on the same snapshot so the existing React home page can show both
kinds without a second application. The TEPP run is Failed /
`tepp_not_available` when the default transport is missing -- the list
keeps that machine code off the caption (this decision) and instead
tells the operator to open the run, then connect the measurement
service. The detail now shows the legal lifecycle the registry already
stored. Write/rebuild APIs, a live TEPP transport, and a fuller
Analysis Run Console remain later slices.

## References

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.83.0",
"version": "0.84.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
95 changes: 94 additions & 1 deletion frontend/src/AnalysisRunsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AnalysisRunsPanel } from "./AnalysisRunsPanel";
import { analysisRunCaption, shortDigest } from "./analysisRunDisplay";
import {
analysisRunCaption,
analysisRunCorpusHint,
analysisRunEmptyPostsHint,
analysisRunNextAction,
shortDigest,
} from "./analysisRunDisplay";
import type { AnalysisRun } from "./api";

const sampleRun: AnalysisRun = {
Expand All @@ -19,6 +25,20 @@ const sampleRun: AnalysisRun = {
source_counts: [],
};

const failedTeppRun: AnalysisRun = {
analysis_run_id: "run-demo-tepp",
run_kind_code: "analysis_run_tepp",
run_kind_label: "TEPP measurement",
scope_kind_code: "analysis_scope_corporate_entity",
scope_kind_label: "Corporate entity",
scope_entity_name: "Demo Corp",
status_code: "analysis_status_failed",
status_label: "Failed",
knowledge_cutoff: "2026-01-12T12:00:00Z",
requested_at: "2026-01-12T12:34:00Z",
source_counts: [],
};

describe("analysisRunCaption", () => {
it("joins kind, status, and scope so the operator knows which run to open", () => {
expect(analysisRunCaption(sampleRun)).toBe(
Expand All @@ -27,6 +47,32 @@ describe("analysisRunCaption", () => {
});
});

describe("analysisRunNextAction", () => {
it("tells the operator to open a failed run and reconnect the service", () => {
expect(analysisRunNextAction(failedTeppRun)).toMatch(/connect the measurement service/);
});

it("hides a next action on a succeeded run", () => {
expect(analysisRunNextAction(sampleRun)).toBeNull();
});
});

describe("analysisRunEmptyPostsHint", () => {
it("names TEPP so the empty list is not read as a reconstruction miss", () => {
expect(analysisRunEmptyPostsHint(failedTeppRun)).toMatch(/for TEPP to measure/);
});
});

describe("analysisRunCorpusHint", () => {
it("says cutoff posts are the TEPP measurement bag, not a reconstruction", () => {
expect(analysisRunCorpusHint(failedTeppRun)).toMatch(/cutoff corpus TEPP would measure/);
});

it("stays silent on a lineage run", () => {
expect(analysisRunCorpusHint(sampleRun)).toBeNull();
});
});

describe("shortDigest", () => {
it("returns a 12-character prefix for comparing an approved revision", () => {
expect(shortDigest("c".repeat(40))).toBe("c".repeat(12));
Expand Down Expand Up @@ -80,4 +126,51 @@ describe("AnalysisRunsPanel", () => {
).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Open run post:/ })).not.toBeInTheDocument();
});

it("opens a failed TEPP run and shows the machine failure code", async () => {
vi.stubGlobal(
"fetch",
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/api/analysis-runs/run-demo-tepp")) {
return Promise.resolve(
new Response(
JSON.stringify({
...failedTeppRun,
visible_posts: [{ post_id: "post-1", post_title: "Public post" }],
status_history: [
{
status_ordinal: 3,
status_code: "analysis_status_failed",
status_label: "Failed",
occurred_at: "2026-01-12T12:37:00Z",
failure_code: "tepp_not_available",
},
],
}),
{ status: 200 },
),
);
}
if (url.endsWith("/api/analysis-runs")) {
return Promise.resolve(
new Response(JSON.stringify({ analysis_runs: [failedTeppRun] }), { status: 200 }),
);
}
return Promise.resolve(new Response(null, { status: 404 }));
}),
);

render(<AnalysisRunsPanel accessToken="test-access-token" onSelectPost={() => undefined} />);
expect(
await screen.findByText(/Open this run to see why it failed/),
).toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", {
name: "Open analysis run: TEPP measurement · Failed · Demo Corp",
}),
);
expect(await screen.findByText(/Failed 2026-01-12 12:37 · tepp_not_available/)).toBeInTheDocument();
expect(screen.getByText(/cutoff corpus TEPP would measure/)).toBeInTheDocument();
});
});
46 changes: 28 additions & 18 deletions frontend/src/AnalysisRunsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import {
fetchAnalysisRuns,
type AnalysisRun,
} from "./api";
import { analysisRunCaption, shortDigest } from "./analysisRunDisplay";
import {
analysisRunCaption,
analysisRunCorpusHint,
analysisRunEmptyPostsHint,
analysisRunNextAction,
shortDigest,
} from "./analysisRunDisplay";

/**
* Home-page Analysis runs list and authorized detail.
Expand Down Expand Up @@ -47,6 +53,8 @@ export function AnalysisRunsPanel({
if (error && runs === null) return <p className="error">{error}</p>;
if (runs === null) return <p>Loading analysis runs...</p>;

const corpusHint = selected ? analysisRunCorpusHint(selected) : null;

return (
<section className="popup-section lineage-home">
<div className="lineage-home-header">
Expand All @@ -64,6 +72,7 @@ export function AnalysisRunsPanel({
(count) => count.count_type_code === "analysis_count_document",
);
const caption = analysisRunCaption(run);
const nextAction = analysisRunNextAction(run);
return (
<li key={run.analysis_run_id} className="ticket-list-item">
<button
Expand All @@ -77,6 +86,7 @@ export function AnalysisRunsPanel({
{documentCount.count_value} {documentCount.count_type_label.toLowerCase()}
</span>
)}
{nextAction && <span className="post-meta">{nextAction}</span>}
</button>
</li>
);
Expand Down Expand Up @@ -116,24 +126,24 @@ export function AnalysisRunsPanel({
</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>
<>
{corpusHint && <p className="post-meta">{corpusHint}</p>}
<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>
</>
) : (
<p className="popup-placeholder">
No posts were available at this cutoff. Open a later run, or ask an
administrator to capture a newer snapshot.
</p>
<p className="popup-placeholder">{analysisRunEmptyPostsHint(selected)}</p>
)}
</div>
)}
Expand Down
Loading