Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions docs/adr/0005-relation-verification-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ keeps the channel unavailable (never fabricates a verification result)
when `SEARXNG_BASE_URL` is unset, same discipline as every other
pluggable client in this repo.

Evidence token selection excludes generic fixture descriptors such as
`fictitious`, `nonexistent`, `placeholder`, `sample`, `example`, and `demo`.
Those words can appear in unrelated search results and are not organization
identity. This keeps synthetic demo data out of the corroboration signal while
retaining distinctive organization tokens and cited URLs.

Persistence: `post_counterparty_entity` gains
`verification_status_code` (`common_lookup_value` category
`relation_verification_status`: `verify_pending` / `verify_corroborated`
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0022-authorized-tepp-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ authorized transaction:
4. submits through `TeppClient`. An empty `TEPP_TRANSPORT_URL` keeps the
default unavailable transport. A set URL POSTs the published wire
payload through the http(s)-only helper. File URLs stay unavailable;
`AnalysisRunRequest` rejects non-v1 versions and blank/non-text required
fields before the transport is called, matching TEPP's v1 JSON Schema;
5. appends Failed / `tepp_not_available` when the transport is missing
or refused, or Failed / `tepp_result_not_persisted` when TEPP accepts
an envelope this product cannot store yet.
Expand Down Expand Up @@ -94,3 +96,7 @@ World Wide Web Consortium. https://www.w3.org/TR/prov-dm/

World Wide Web Consortium. (2013). *PROV-O: The PROV ontology* (W3C
Recommendation). https://www.w3.org/TR/prov-o/

ContextualWisdomLab. (2026). *TEPP API and modular integration contract*
([Computer software documentation]). GitHub.
https://github.com/ContextualWisdomLab/TEPP/blob/main/docs/API_CONTRACT.md
7 changes: 7 additions & 0 deletions lineageweave/relation_verification.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@
"group",
"holdings",
"limited",
# Fixture descriptors are search vocabulary, not organization identity.
"fictitious",
"nonexistent",
"placeholder",
"sample",
"example",
"demo",
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

@devin-ai-integration devin-ai-integration Bot Aug 21, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: New stopwords make demo/example-named orgs permanently uncorroborated

Adding fictitious, nonexistent, placeholder, sample, example, demo to _ORG_TOKEN_STOPWORDS (relation_verification.py) means an organization whose only distinctive tokens are these words yields an empty token list and always returns STATUS_UNCORROBORATED. This is intentional per ADR 0005 (keeping synthetic demo data out of the corroboration signal). I confirmed no fixture organization names in lineageweave/fixtures.py consist of these words and existing corroboration tests (Acme on host acme.example) still match on the acme token, so no legitimate verification regresses.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"foundation",
"the",
"and",
Expand Down
40 changes: 30 additions & 10 deletions lineageweave/tepp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
lineage scores as TEPP's calibrated psychometric measurement (they answer
different questions -- see docs/lineage-bi-research-notes.md).

TEPP does not expose a live HTTP endpoint yet (as of this writing it is
Rust-crate-only; see ``docs/API_CONTRACT.md`` in that repo). This client
builds and validates the exact wire shape TEPP has published
(``schemas/analysis_run_request_v1.json``) so wiring in a real transport is
a one-line change (:meth:`TeppClient.__init__`'s ``transport`` argument) once
that endpoint exists, instead of a redesign.
TEPP's current protected main exposes Rust library/domain contracts and an
accepted target API contract, not a deployed HTTP service (see
``docs/API_CONTRACT.md`` in that repo). This client builds and validates the
exact wire shape TEPP has published
(``schemas/analysis_run_request_v1.json``), so an executable transport can be
added through :meth:`TeppClient.__init__` without a consumer redesign.
"""

from __future__ import annotations
Expand All @@ -23,14 +23,15 @@


class TeppNotAvailable(RuntimeError):
"""Raised by the default transport: TEPP has no live REST API yet."""
"""Raised when no executable TEPP transport is configured."""


def _no_transport(request: dict[str, Any]) -> dict[str, Any]:
"""Fail closed while TEPP exposes no executable transport."""
raise TeppNotAvailable(
"TEPP has no live HTTP endpoint yet (Rust-crate-only as of this writing). "
"Pass a transport= callable to TeppClient once one exists, or consume TEPP "
"as a Rust crate directly per its own docs/API_CONTRACT.md."
"No executable TEPP transport is configured. TEPP currently publishes "
"Rust library/domain contracts and an accepted target API contract; "
"configure transport= when an executable service is available."
)


Expand All @@ -50,7 +51,26 @@ class AnalysisRunRequest:
output_profile: str
contract_version: int = 1

def __post_init__(self) -> None:
"""Reject payloads that violate TEPP's v1 schema before transport."""
if type(self.contract_version) is not int or self.contract_version != 1:
raise ValueError("TEPP AnalysisRunRequest requires contract_version=1")
for field_name in (
"idempotency_key",
"tenant_workspace_id",
"snapshot_id",
"knowledge_cutoff",
"model_contract_version",
"output_profile",
):
value = getattr(self, field_name)
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"TEPP AnalysisRunRequest field {field_name} must be non-blank text"
)
Comment on lines +55 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: bool rejection for contract_version relies on exact type identity

type(self.contract_version) is not int (tepp_client.py) correctly rejects True/False because type(True) is bool, not int. This is the intended guard (the boolean regression test at test_tepp_client.py confirms) and would not be achieved by isinstance(..., int) since bool is a subclass of int. Existing constructors (analysis_run_start.py, scripts/seed_demo_data.py:1624) all pass non-blank strings and the default contract_version=1, so the new __post_init__ validation does not break current callers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +55 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Request validation raises ValueError before the fail-closed try block

The new AnalysisRunRequest.__post_init__ validation (tepp_client.py) raises ValueError at construction time. In the delivery flow, tepp_run_request() is called at analysis_run_start.py inside _deliver_tepp_measurement, before the try block that only catches asyncpg.UniqueViolationError. If a required field were ever blank, this would surface as an unhandled 500 with the run left Running (the outbox row already committed in the enqueue step) rather than a Failed/tepp_not_available row per the ADR 0022 / AGENTS.md fail-closed contract. In practice all fields are str(...)-coerced non-blank values from DB columns (idempotency_key, snapshot_sha256, str(corporate_entity_id), formatted knowledge_cutoff), so this cannot fire today — noting it in case future callers pass client-influenced values.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


def to_json(self) -> dict[str, Any]:
"""Serialize the validated request into TEPP's v1 wire representation."""
return {
"contract_version": self.contract_version,
"idempotency_key": self.idempotency_key,
Expand Down
60 changes: 59 additions & 1 deletion tests/test_relation_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import pytest

from lineageweave.relation_verification import (
RelationVerificationClient,
STATUS_CORROBORATED,
STATUS_UNCORROBORATED,
NullRelationVerificationClient,
Expand All @@ -33,7 +34,16 @@ def do_GET(self) -> None: # noqa: N802 -- BaseHTTPRequestHandler API
parsed = urlparse(self.path)
query = parse_qs(parsed.query)
type(self).received_query = query.get("q", [""])[0]
if "Acme" in type(self).received_query:
if "NoList" in type(self).received_query:
payload = {"query": type(self).received_query, "results": {}}
elif "Skip" in type(self).received_query:
payload = {"query": type(self).received_query, "results": [None]}
elif "NoEvidence" in type(self).received_query:
payload = {
"query": type(self).received_query,
"results": [{"url": "https://example.com/item", "content": ""}],
}
elif "Acme" in type(self).received_query:
payload = {
"query": type(self).received_query,
"results": [
Expand Down Expand Up @@ -72,6 +82,12 @@ def test_null_client_is_unavailable_not_silently_uncorroborated() -> None:
client.verify("Acme Corp", "Voice of Customer")


def test_protocol_stub_raises_instead_of_returning_a_fake_result() -> None:
"""The protocol's executable stub must fail if called directly."""
with pytest.raises(NotImplementedError):
RelationVerificationClient.verify(object(), "Acme", "Voice of Customer")


def test_searxng_client_reports_corroborated_with_evidence_url() -> None:
server, base = _serve()
try:
Expand All @@ -98,6 +114,21 @@ def test_searxng_client_reports_uncorroborated_with_no_evidence_url_when_search_
assert result.evidence_url is None


@pytest.mark.parametrize("organization_name", ["NoList", "Skip", "NoEvidence"])
def test_searxng_client_fails_closed_for_unusable_results(organization_name: str) -> None:
"""Malformed and unciting search results remain explicitly uncorroborated."""
server, base = _serve()
try:
result = SearxngRelationVerificationClient(base_url=base).verify(
organization_name, "Voice of Customer"
)
finally:
server.shutdown()

assert result.status_code == STATUS_UNCORROBORATED
assert result.evidence_url is None


def test_query_echo_on_a_search_host_is_not_corroboration() -> None:
assert (
corroborating_evidence_url(
Expand Down Expand Up @@ -133,6 +164,33 @@ def test_legal_suffix_alone_is_not_corroboration() -> None:
)


def test_fixture_descriptors_do_not_corrobate_an_unrelated_search_hit() -> None:
"""Generic synthetic-data words must not stand in for organization identity."""
assert (
corroborating_evidence_url(
"Zzqxvthorp Fictitious Nonexistent Org",
{
"url": "https://learn.microsoft.com/writing-style",
"title": "Fictitious names and addresses",
"content": "Documentation explains fictitious and nonexistent examples.",
},
)
is None
)


def test_missing_url_and_all_generic_tokens_are_not_evidence() -> None:
"""Missing URLs and names made only of fixture words cannot cite evidence."""
assert corroborating_evidence_url("Acme Corp", {"content": "Acme"}) is None
assert (
corroborating_evidence_url(
"Fictitious Nonexistent Org",
{"url": "https://example.com/item", "content": "Fictitious"},
)
is None
)


def test_hangul_org_name_token_is_corroboration() -> None:
assert (
corroborating_evidence_url(
Expand Down
25 changes: 25 additions & 0 deletions tests/test_tepp_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from dataclasses import replace

import pytest

from lineageweave.tepp_client import AnalysisRunRequest, TeppClient, TeppNotAvailable
Expand Down Expand Up @@ -30,6 +32,29 @@ def test_to_json_matches_tepp_published_schema_shape() -> None:
}


@pytest.mark.parametrize(
("field_name", "value"),
[
("idempotency_key", " "),
("tenant_workspace_id", None),
("snapshot_id", "\t"),
("knowledge_cutoff", ""),
("model_contract_version", "\n"),
("output_profile", None),
],
)
def test_request_rejects_non_blank_schema_fields(field_name: str, value: object) -> None:
"""A v1 request must not send blank or non-text required fields."""
with pytest.raises(ValueError, match=field_name):
replace(_sample_request(), **{field_name: value})


def test_request_rejects_unknown_contract_version() -> None:
"""The adapter must not silently emit a request for another contract."""
with pytest.raises(ValueError, match="contract_version=1"):
replace(_sample_request(), contract_version=2)


def test_default_transport_fails_closed_until_tepp_ships_http() -> None:
client = TeppClient()
with pytest.raises(TeppNotAvailable):
Expand Down
Loading