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
7 changes: 7 additions & 0 deletions CHANGELOG.d/2.12.2-tepp-accepted-clocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# 2.12.2 TEPP accepted evidence stores distinct receipt and row-write clocks

Accepted transport evidence persists `received_at` as the
transport-response receipt and `recorded_at` as the row-write
instant. Measurement evidence shows the second clock only when those
instants differ. Digest recomputation is unchanged. No invented
theta (ADR 0035 follow-up).
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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.12.2] - 2026-08-18

### Fixed

- Accepted TEPP transport evidence now stores **received** (transport
response) and **recorded** (row write) as distinct clocks when those
instants differ (ADR 0035 follow-up). After `make seed`, Demo Analyst
opens **TEPP measurement · Failed · Demo Corp** Measurement evidence
and sees one Received clock when seed receipt and persist share an
instant. A later start that persists in a later minute shows both
clocks. Digest recomputation is unchanged. Hidden runs stay 404.
Never invent a theta.

## [2.12.1] - 2026-08-17

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ unpublished envelope is Failed (`tepp_not_available` /
`tepp_result_not_persisted`). A published accepted acknowledgement is
Failed (`tepp_completed_result_unsupported`) and is shown as aggregate
transport evidence. Do not stamp Succeeded from that ack. Do not invent
a theta or a local psychometric substitute.
a theta or a local psychometric substitute. Measurement evidence shows
Received, and recorded only when that row-write instant differs.
The home list caption stays `kind · status · entity`; the machine
failure code is detail-only (ADR 0014). Open a Failed TEPP row, then
connect a live TEPP transport or read aggregate transport evidence.
Expand Down
52 changes: 42 additions & 10 deletions backend/app/analysis_run_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
so a crash after Running does not lose the item. ADR 0035 stores a
published TEPP accepted acknowledgement as aggregate transport
evidence and never stamps Succeeded from that ack or from a
LineageWeave-local completed envelope. Period-report stays another
path. Neither start invents a theta or a calibrated report score.
LineageWeave-local completed envelope. Accepted evidence stores
transport-response receipt and row-write time as distinct clocks
when those instants differ. Period-report stays another path.
Neither start invents a theta or a calibrated report score.
"""

from __future__ import annotations
Expand Down Expand Up @@ -130,6 +132,25 @@ def tepp_run_request(
)


def tepp_accepted_clocks(
*,
started_at: datetime,
received_at: datetime,
recorded_at: datetime,
) -> tuple[datetime, datetime]:
"""Return receipt then row-write clocks, monotonic versus start.

``received_at`` is the transport-response receipt. ``recorded_at``
is the later row-write instant. A clock that runs backward is
clamped forward so ``started_at <= received_at <= recorded_at``.
Equal instants stay equal; this helper does not invent a later
recorded clock.
"""
receipt = received_at if received_at >= started_at else started_at
recorded = recorded_at if recorded_at >= receipt else receipt
return receipt, recorded


def tepp_submit_outcome(
client: TeppClient,
request: AnalysisRunRequest,
Expand Down Expand Up @@ -709,9 +730,16 @@ async def _persist_tepp_accepted(
conn: asyncpg.Connection,
analysis_run_id: str,
evidence: TeppAcceptedEvidence,
received_at: datetime,
recorded_at: datetime,
) -> bool:
"""Store published accepted evidence. Missing table is not success."""
"""Store published accepted evidence with receipt and row-write clocks.

Missing table is not success. Callers pass transport-response
receipt as ``received_at`` and the row-write instant as
``recorded_at``. This function binds those two values as given and
does not invent a later recorded clock when they are equal.
"""
try:
await conn.execute(
"""
Expand All @@ -726,7 +754,7 @@ async def _persist_tepp_accepted(
evidence.run_state,
evidence.idempotency_key,
evidence.evidence_sha256(),
recorded_at,
received_at,
recorded_at,
)
except asyncpg.UndefinedTableError:
Expand All @@ -742,20 +770,24 @@ async def _deliver_tepp_measurement(
tepp_client: TeppClient,
) -> None:
"""Submit the frozen snapshot through ``tepp_client``. Never persist a theta."""
now = datetime.now(timezone.utc)
started_at = datetime.now(timezone.utc)
request = tepp_run_request(
idempotency_key=str(locked["idempotency_key"]),
snapshot_sha256=str(locked["snapshot_sha256"]),
knowledge_cutoff=locked["knowledge_cutoff"],
corporate_entity_id=str(locked["corporate_entity_id"]),
)
status_code, failure_code, accepted = tepp_submit_outcome(tepp_client, request)
finished = datetime.now(timezone.utc)
if finished < now:
finished = now
received_at = datetime.now(timezone.utc)
recorded_at = datetime.now(timezone.utc)
receipt, recorded = tepp_accepted_clocks(
started_at=started_at,
received_at=received_at,
recorded_at=recorded_at,
)
if accepted is not None:
stored = await _persist_tepp_accepted(
conn, analysis_run_id, accepted, finished
conn, analysis_run_id, accepted, receipt, recorded
)
if not stored:
status_code, failure_code = _FAILED, "tepp_result_not_persisted"
Expand All @@ -764,6 +796,6 @@ async def _deliver_tepp_measurement(
analysis_run_id,
await _next_status_ordinal(conn, analysis_run_id),
status_code,
finished,
recorded,
failure_code,
)
13 changes: 13 additions & 0 deletions docs/adr/0035-tepp-accepted-transport-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ Existing volumes apply `0029_analysis_run_tepp_accepted.sql` after
0028. Granted retention purge empties the accepted table when it
exists.

## Follow-up — v2.12.2 distinct receipt and row-write clocks

Decision 3 already named `received_at` (transport-response receipt)
and `recorded_at` (row persistence). v2.12.1 bound one application
instant into both columns, so Measurement evidence copy always showed
two clocks. v2.12.2 passes the post-transport instant as
`received_at` and the row-write instant as `recorded_at`, clamped so
start ≤ receipt ≤ row-write (National Institute of Standards and
Technology, 2015). Authorized copy shows the second clock only when
the displayed instants differ. Digest recomputation is unchanged and
still excludes clocks. Hidden runs stay 404 (ADR 0014). Migration
0029 is not rewritten; the two columns already exist.

## References — APA 7th

American Educational Research Association, American Psychological
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": "2.12.1",
"version": "2.12.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
48 changes: 46 additions & 2 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ describe("App, authenticated", () => {
succeededReportRun?: boolean;
succeededTeppRun?: boolean;
acceptedTeppRun?: boolean;
distinctTeppClocks?: boolean;
omitTeppRecordedAt?: boolean;
pendingTeppRun?: boolean;
hiddenAnalysisRun?: boolean;
pluralAffiliations?: boolean;
Expand Down Expand Up @@ -325,7 +327,13 @@ describe("App, authenticated", () => {
tepp_idempotency_key: "demo-tepp-seed-2026-w02-succeeded",
tepp_evidence_sha256: "a".repeat(64),
tepp_received_at: "2026-01-12T12:45:00Z",
tepp_recorded_at: "2026-01-12T12:45:00Z",
...(options?.omitTeppRecordedAt
? {}
: {
tepp_recorded_at: options?.distinctTeppClocks
? "2026-01-12T12:46:00Z"
: "2026-01-12T12:45:00Z",
}),
tepp_completed_artifact_available: false,
}
: {};
Expand Down Expand Up @@ -722,7 +730,13 @@ describe("App, authenticated", () => {
tepp_run_state: "accepted",
tepp_evidence_sha256: "a".repeat(64),
tepp_received_at: "2026-01-12T12:45:00Z",
tepp_recorded_at: "2026-01-12T12:45:00Z",
...(options?.omitTeppRecordedAt
? {}
: {
tepp_recorded_at: options?.distinctTeppClocks
? "2026-01-12T12:46:00Z"
: "2026-01-12T12:45:00Z",
}),
tepp_completed_artifact_available: false,
}
: {}),
Expand Down Expand Up @@ -2944,6 +2958,8 @@ describe("App, authenticated", () => {
expect(screen.getAllByText("aggregate transport evidence").length).toBeGreaterThan(0);
expect(screen.getByText("a".repeat(64))).toBeInTheDocument();
expect(screen.getByText(/accepted run demo-tepp-accepted-opaque/)).toBeInTheDocument();
expect(screen.getByText("Received 2026-01-12 12:45")).toBeInTheDocument();
expect(screen.queryByText(/recorded 2026-01-12/)).not.toBeInTheDocument();
expect(screen.getByText(/completed-artifact identity/i)).toBeInTheDocument();
expect(screen.queryByText(/validated multilevel estimate/i)).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Copy evidence SHA-256" }));
Expand All @@ -2952,6 +2968,34 @@ describe("App, authenticated", () => {
expect(screen.queryByText(/2 affiliations/)).not.toBeInTheDocument();
});

it("shows two TEPP clocks only when receipt and row-write differ", async () => {
stubBackend({ acceptedTeppRun: true, distinctTeppClocks: true });
render(<App />);

await userEvent.click(
await screen.findByRole("button", {
name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to read aggregate transport evidence. Completed TEPP measurement identity is unavailable until TEPP publishes a versioned completed-result contract.",
}),
);
expect(
await screen.findByText("Received 2026-01-12 12:45 · recorded 2026-01-12 12:46"),
).toBeInTheDocument();
expect(screen.queryByText(/theta/i)).not.toBeInTheDocument();
});

it("shows only the receipt clock when recorded time is absent", async () => {
stubBackend({ acceptedTeppRun: true, omitTeppRecordedAt: true });
render(<App />);

await userEvent.click(
await screen.findByRole("button", {
name: "Open analysis run: TEPP measurement · Failed · Demo Corp. Open this run to read aggregate transport evidence. Completed TEPP measurement identity is unavailable until TEPP publishes a versioned completed-result contract.",
}),
);
expect(await screen.findByText("Received 2026-01-12 12:45")).toBeInTheDocument();
expect(screen.queryByText(/recorded 2026-01-12/)).not.toBeInTheDocument();
});

it("records a pending lineage run and opens the authorized detail", async () => {
const fetchMock = stubBackend();
render(<App />);
Expand Down
33 changes: 29 additions & 4 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,34 @@ function analysisRunCorpusHint(run: AnalysisRun): string | null {
}
}

/**
* Buyer-visible TEPP receipt clock. Minute precision matches other run clocks.
*/
function formatTeppEvidenceClock(iso: string): string {
return iso.slice(0, 16).replace("T", " ");
}

/**
* Authorized TEPP clocks. A second clock appears only when instants differ.
*
* Equal receipt and row-write values stay one sentence so the copy does
* not invent a second clock. Missing recorded time is receipt only.
*/
function teppAcceptedClockCopy(
receivedAt: string,
recordedAt: string | undefined,
): string {
const received = formatTeppEvidenceClock(receivedAt);
if (recordedAt === undefined) {
return `Received ${received}`;
}
const recorded = formatTeppEvidenceClock(recordedAt);
if (recorded === received) {
return `Received ${received}`;
}
return `Received ${received} · recorded ${recorded}`;
}

/**
* Authorized TEPP transport evidence. Never a validated multilevel estimate.
*
Expand Down Expand Up @@ -2071,10 +2099,7 @@ function TeppMeasurementEvidence({ run }: { run: AnalysisRun }) {
</p>
{run.tepp_received_at && (
<p className="post-meta">
Received {run.tepp_received_at.slice(0, 16).replace("T", " ")}
{run.tepp_recorded_at
? ` · recorded ${run.tepp_recorded_at.slice(0, 16).replace("T", " ")}`
: ""}
{teppAcceptedClockCopy(run.tepp_received_at, run.tepp_recorded_at)}
</p>
)}
<p className="post-meta">
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__ = "2.12.1"
__version__ = "2.12.2"
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 = "2.12.1"
version = "2.12.2"
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
Loading