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
9 changes: 6 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -475,9 +475,12 @@ revision and configuration digest prefixes.
cutoff capture (ADR 0017): snapshot, counts, frozen membership, run,
scope, and the first status in one transaction.
`POST /api/analysis-runs/{id}/start` then runs ThreadWeave on that
frozen bag and persists run-scoped edges (ADR 0021). It does not invent
a TEPP score. Request a lineage reconstruction from the home list, open
the Pending row, then start reconstruction. Hover the Result digest
frozen bag and persists run-scoped edges (ADR 0021), or submits TEPP
through `tepp_client` (ADR 0022). It does not invent a TEPP score.
Request a lineage reconstruction from the home list, open the Pending
row, then start reconstruction. A Pending TEPP row starts a
measurement; a missing transport stays Failed /
`tepp_not_available`. Hover the Result digest
prefix, then confirm the designed A-100 fork before treating the live
Event Lineage panel as that run's tree.
`make seed` also records a TEPP measurement run through
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.91.0-analysis-run-reconstructed-edge-click.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.91.0 Analysis-run reconstructed edge click

Started reconstruction edges are buttons. Open the revised-quote child
or the pricing-follow-up parent. Live-body warning still applies.
4 changes: 4 additions & 0 deletions CHANGELOG.d/0.92.0-analysis-run-tepp-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# 0.92.0 Analysis-run TEPP start

Pending TEPP start goes through tepp_client. Missing transport stays
Failed. No invented theta.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ 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.92.0] - 2026-08-17

### Added

- **Start TEPP measurement** on a Pending TEPP row submits
`AnalysisRunRequest` through `tepp_client` (ADR 0022). A missing
`TEPP_TRANSPORT_URL` or a refused URL is Failed /
`tepp_not_available`. An accepted envelope is Failed /
`tepp_result_not_persisted`. Failed stays terminal: **Request a new
TEPP measurement** records a new Pending run. Period-report start
stays 422. No TEPP theta is invented.

## [0.91.0] - 2026-08-17

### Added

- After **Start reconstruction**, the titled A-100 edges are buttons.
Click the revised-quote child to open the live post; click the
pricing-follow-up parent to open that post. A child marked
**Updated after cutoff** still shows the live-body warning. The
popup does not invent a cutoff snapshot. No TEPP theta is invented.

## [0.90.0] - 2026-08-17

### Added
Expand Down
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ after cutoff were rewritten after the run; compare those bodies
before treating them as reconstructed evidence (ADR 0016).
`POST /api/analysis-runs` records Pending on an authorized
cutoff capture (ADR 0017). `POST /api/analysis-runs/{id}/start`
reconstructs that frozen cutoff bag (ADR 0021) and does not invent a
theta. Hover the Result prefix to read the parent-choice digest.
reconstructs that frozen cutoff bag (ADR 0021) or submits TEPP
through `tepp_client` (ADR 0022). A missing transport or unused
accepted 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.
5 changes: 3 additions & 2 deletions backend/app/analysis_run_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@

``create_pending_analysis_run`` (ADR 0017) writes snapshot, counts, frozen
membership, run, scope, and the first Pending event atomically.
``start_pending_analysis_run`` (ADR 0021) later reconstructs lineage on
that cutoff bag. Neither path invents a TEPP score.
``start_pending_analysis_run`` later reconstructs lineage (ADR 0021)
or submits TEPP through ``tepp_client`` (ADR 0022). Neither path
invents a TEPP score.
"""

from __future__ import annotations
Expand Down
173 changes: 147 additions & 26 deletions backend/app/analysis_run_start.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Start a Pending lineage reconstruction without inventing a TEPP score.
"""Start a Pending lineage reconstruction or TEPP measurement.

ADR 0021. ``POST /api/analysis-runs/{id}/start`` transitions Pending to
Running, runs ThreadWeave on the frozen cutoff bag, persists run-scoped
edges, then stamps Succeeded. TEPP and period-report stay other paths.
ADR 0021 reconstructs lineage. ADR 0022 starts TEPP through
``tepp_client`` only. Period-report stays another path. Neither start
invents a theta or a calibrated report score.
"""

from __future__ import annotations
Expand All @@ -20,15 +20,20 @@
fetch_visible_analysis_run,
)
from backend.app.lineage_ingestion import records_from_source_posts
from lineageweave.http_client import HttpClientError, post_json
from lineageweave.lineage_persistence import lineage_edge_specs
from lineageweave.models import Edge
from lineageweave.tepp_client import AnalysisRunRequest, TeppClient, TeppNotAvailable

_LINEAGE_KIND = "analysis_run_lineage"
_TEPP_KIND = "analysis_run_tepp"
_REPORT_KIND = "analysis_run_report"
_PENDING = "analysis_status_pending"
_RUNNING = "analysis_status_running"
_SUCCEEDED = "analysis_status_succeeded"
_FAILED = "analysis_status_failed"
_TEPP_MODEL_CONTRACT = "tepp-analysis-run-v1"
_TEPP_OUTPUT_PROFILE = "calibrated_event_measurement"


class AnalysisRunStartError(AnalysisRunCreateError):
Expand All @@ -53,19 +58,14 @@ def reconstruction_result_digest(edges: list[Edge]) -> str:


def start_kind_rejection(run_kind_code: str) -> AnalysisRunStartError | None:
"""Return a 422 when start is not a lineage reconstruction.
"""Return a 422 when start cannot run this kind.

TEPP and period-report keep their own transports. This path must not
invent a theta or a calibrated report score.
Lineage reconstructs the frozen bag. TEPP submits through
``tepp_client`` and never invents a theta. Period-report stays on
its own rebuild path.
"""
if run_kind_code == _LINEAGE_KIND:
if run_kind_code in {_LINEAGE_KIND, _TEPP_KIND}:
return None
if run_kind_code == _TEPP_KIND:
return AnalysisRunStartError(
422,
"Connect a TEPP transport from a Failed TEPP row. "
"This start path does not invent a measurement.",
)
if run_kind_code == _REPORT_KIND:
return AnalysisRunStartError(
422,
Expand All @@ -74,11 +74,69 @@ def start_kind_rejection(run_kind_code: str) -> AnalysisRunStartError | None:
)
return AnalysisRunStartError(
422,
"Start reconstructs a Pending lineage run only. "
"Start reconstructs a Pending lineage run or submits TEPP. "
"This start path does not invent a measurement.",
)


def configured_tepp_client(transport_url: str = "") -> TeppClient:
"""Build a TEPP client from an optional HTTP transport URL.

An empty URL keeps the default unavailable transport. A set URL
POSTs TEPP's published wire payload. File URLs and other schemes
stay unavailable -- this is not a local psychometric substitute.
"""
url = transport_url.strip()
if not url:
return TeppClient()

def transport(payload: dict[str, Any]) -> dict[str, Any]:
try:
return post_json(url, payload, headers={}, timeout=30.0)
except (HttpClientError, ValueError, TypeError) as exc:
raise TeppNotAvailable(str(exc)) from exc

return TeppClient(transport=transport)


def tepp_run_request(
*,
idempotency_key: str,
snapshot_sha256: str,
knowledge_cutoff: datetime,
corporate_entity_id: str,
) -> AnalysisRunRequest:
"""Build TEPP's published request from the frozen run, never a theta."""
cutoff = knowledge_cutoff
if cutoff.tzinfo is None:
cutoff = cutoff.replace(tzinfo=timezone.utc)
return AnalysisRunRequest(
idempotency_key=idempotency_key,
tenant_workspace_id=str(corporate_entity_id),
snapshot_id=snapshot_sha256,
knowledge_cutoff=cutoff.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
model_contract_version=_TEPP_MODEL_CONTRACT,
output_profile=_TEPP_OUTPUT_PROFILE,
)


def tepp_submit_outcome(
client: TeppClient,
request: AnalysisRunRequest,
) -> tuple[str, str]:
"""Submit through ``tepp_client``. Never invent or persist a theta.

A missing transport is ``tepp_not_available``. An accepted envelope
is not a persistable measurement until TEPP publishes one, so the
run stays Failed / ``tepp_result_not_persisted``.
"""
try:
client.submit_analysis_run(request)
except TeppNotAvailable:
return _FAILED, "tepp_not_available"
return _FAILED, "tepp_result_not_persisted"


def start_write_conflict_error() -> AnalysisRunStartError:
"""Next action when a concurrent start already wrote this run."""
return AnalysisRunStartError(
Expand Down Expand Up @@ -201,14 +259,17 @@ async def start_pending_analysis_run(
analysis_run_id: str,
account_id: str,
affiliated_entity_ids: list[str],
tepp_client: TeppClient | None = None,
) -> dict[str, Any]:
"""Run ThreadWeave on a visible Pending lineage row.

TEPP and period-report are rejected so this path cannot invent a
theta. A Succeeded retry returns the stored reconstruction (documented
no-op replay). A Running or concurrent write is 409. Hidden runs 404.
The run row is locked before Running so a double-click is 409 or a
replay, never a 500.
"""Run ThreadWeave or submit TEPP on a visible Pending row.

Period-report is rejected so this path cannot invent a calibrated
score. TEPP goes through ``tepp_client`` and stays Failed when the
transport is missing or the envelope is not persistable. A Succeeded
retry returns the stored reconstruction (documented no-op replay).
A Running or concurrent write is 409. Hidden runs 404. The run row
is locked before Running so a double-click is 409 or a replay,
never a 500.
"""
try:
UUID(analysis_run_id)
Expand All @@ -231,15 +292,19 @@ async def start_pending_analysis_run(
if current["status_code"] != _PENDING:
raise AnalysisRunStartError(
409,
"Open this run. Start is only for a Pending lineage reconstruction.",
"Open this run. Start is only for a Pending lineage reconstruction "
"or TEPP measurement.",
)

locked = await conn.fetchrow(
"""
select run.analysis_run_id, run.knowledge_cutoff,
run.analysis_source_snapshot_id, scope.corporate_entity_id
select run.analysis_run_id, run.knowledge_cutoff, run.run_kind_code,
run.idempotency_key, run.analysis_source_snapshot_id,
snapshot.snapshot_sha256, scope.corporate_entity_id
from analysis_run run
join analysis_run_scope scope on scope.analysis_run_id = run.analysis_run_id
join analysis_source_snapshot snapshot
on snapshot.analysis_source_snapshot_id = run.analysis_source_snapshot_id
where run.analysis_run_id = $1
for update of run
""",
Expand All @@ -266,7 +331,18 @@ async def start_pending_analysis_run(
if locked_status != _PENDING:
raise AnalysisRunStartError(
409,
"Open this run. Start is only for a Pending lineage reconstruction.",
"Open this run. Start is only for a Pending lineage reconstruction "
"or TEPP measurement.",
)

if locked["run_kind_code"] == _TEPP_KIND:
return await _start_tepp_measurement(
conn,
analysis_run_id=analysis_run_id,
account_id=account_id,
affiliated_entity_ids=affiliated_entity_ids,
locked=locked,
tepp_client=tepp_client or TeppClient(),
)

now = datetime.now(timezone.utc)
Expand Down Expand Up @@ -334,3 +410,48 @@ async def start_pending_analysis_run(
if started is None:
raise AnalysisRunStartError(404, "This analysis run is not visible.")
return started


async def _start_tepp_measurement(
conn: asyncpg.Connection,
*,
analysis_run_id: str,
account_id: str,
affiliated_entity_ids: list[str],
locked: asyncpg.Record,
tepp_client: TeppClient,
) -> dict[str, Any]:
"""Submit the frozen snapshot through ``tepp_client``. Never persist a theta."""
now = datetime.now(timezone.utc)
running_ordinal = await _next_status_ordinal(conn, analysis_run_id)
try:
await _append_status(conn, analysis_run_id, running_ordinal, _RUNNING, now)
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 = tepp_submit_outcome(tepp_client, request)
finished = datetime.now(timezone.utc)
if finished < now:
finished = now
await _append_status(
conn,
analysis_run_id,
running_ordinal + 1,
status_code,
finished,
failure_code,
)
except asyncpg.UniqueViolationError as exc:
raise start_write_conflict_error() from exc
started = await fetch_visible_analysis_run(
conn,
analysis_run_id,
account_id,
affiliated_entity_ids,
)
if started is None:
raise AnalysisRunStartError(404, "This analysis run is not visible.")
return started
4 changes: 4 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class Settings:
# means the verification channel is unavailable, same "no fake
# channel" discipline as every other pluggable client.
searxng_base_url: str
# Optional TEPP HTTP transport. Empty keeps TeppClient's default
# unavailable transport. Never a local psychometric substitute.
tepp_transport_url: str

@property
def keycloak_jwks_uri(self) -> str:
Expand Down Expand Up @@ -80,4 +83,5 @@ def load_settings() -> Settings:
vision_model=os.environ.get("VISION_MODEL", ""),
valkey_url=os.environ.get("VALKEY_URL", "redis://localhost:16379/0"),
searxng_base_url=os.environ.get("SEARXNG_BASE_URL", ""),
tepp_transport_url=os.environ.get("TEPP_TRANSPORT_URL", ""),
)
13 changes: 9 additions & 4 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
)
from backend.app.analysis_run_start import (
AnalysisRunStartError,
configured_tepp_client,
start_pending_analysis_run,
)
from backend.app.activity_stream import (
Expand Down Expand Up @@ -1263,13 +1264,16 @@ async def start_analysis_run(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Start ThreadWeave on a visible Pending lineage run.
"""Start ThreadWeave or submit TEPP on a visible Pending run.

post_read is enough. Hidden runs 404. TEPP and period-report are 422
so this path cannot invent a theta. A Succeeded retry returns the
stored tree. A Running restart is 409.
post_read is enough. Hidden runs 404. Period-report is 422 so this
path cannot invent a calibrated score. TEPP goes through
``tepp_client`` and stays Failed when the transport is missing or
the envelope is not persistable. A Succeeded lineage retry returns
the stored tree. A Running restart is 409.
"""
_require_post_read(account)
settings = load_settings()
async with pool.acquire() as conn:
async with conn.transaction():
try:
Expand All @@ -1278,6 +1282,7 @@ async def start_analysis_run(
analysis_run_id=analysis_run_id,
account_id=account.user_account_id,
affiliated_entity_ids=list(account.corporate_entity_ids),
tepp_client=configured_tepp_client(settings.tepp_transport_url),
)
except AnalysisRunStartError as exc:
raise HTTPException(exc.status_code, exc.detail) from exc
Expand Down
Loading