Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 5 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,11 @@ it fetches a genuine access token from a live Keycloak, verifies the
allow/deny ABAC boundary against a throwaway migrated Postgres database
(a private post scoped to a *different* corporate entity is proven
excluded from the list and 403s on direct fetch), and proves a forged
token is rejected. Its dev-only FastAPI `TestClient` uses Starlette with the
project's official `httpx` dev dependency; no alternate transport package is
introduced. `scripts/seed_demo_data.py` populates the docker-compose
token is rejected. Its dev-only FastAPI `TestClient` uses Starlette's
supported `httpx2` transport alongside the project's official `httpx`
dependency. The transport package is dev-only and exists solely for the
current Starlette integration contract; production runtime dependencies remain
unchanged. `scripts/seed_demo_data.py` populates the docker-compose
stack itself with the same shape of synthetic data for manual/frontend use.
`CORSMiddleware` (`backend/app/main.py`) allows exactly the frontend's
origin(s) (`FRONTEND_ORIGINS`), `GET` and `POST` (the extract-keymen
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.d/2.20.0-backend-contract-regressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Restored the runtime-only TEPP credential setting, kept API integration fixtures collision-free, aligned asynchronous Ask tests with semantic retrieval, replaced deprecated FastAPI 422 constants, and moved Starlette integration tests to its supported `httpx2` transport.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to this project are documented here. Format follows

### Added

- Persist explicit paragraph, list, table, MathML formula, and caller-parsed
conversation-turn semantic-unit kinds without inferring absent boundaries.
- Event Lineage now persists each reconstructed connection's independent
channel scores, the normalized weights actually used, and their
contributions. The Event Lineage DAG discloses those exact values as inferred
Expand Down
2 changes: 2 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Settings:
valkey_url: str
searxng_base_url: str
tepp_transport_url: str
tepp_api_key: str
caldav_base_url: str
naruon_calendar_base_url: str
naruon_calendar_service_token: str
Expand Down Expand Up @@ -171,6 +172,7 @@ def load_settings() -> Settings:
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", ""),
tepp_api_key=os.environ.get("TEPP_API_KEY", ""),
Comment thread
seonghobae marked this conversation as resolved.
caldav_base_url=os.environ.get("CALDAV_BASE_URL", "").strip(),
naruon_calendar_base_url=os.environ.get("NARUON_CALENDAR_BASE_URL", "").strip(),
naruon_calendar_service_token=os.environ.get(
Expand Down
22 changes: 11 additions & 11 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,7 @@ async def resolve_customer_master_hint(
) from exc
if resolution is None:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
status.HTTP_422_UNPROCESSABLE_CONTENT,
Comment thread
seonghobae marked this conversation as resolved.
"this hint could not be resolved to a corroborated organization name",
)
return resolution
Expand Down Expand Up @@ -1589,7 +1589,7 @@ async def read_post(
as_of_clock = parse_as_of_clock(as_of)
except ValueError as exc:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
status.HTTP_422_UNPROCESSABLE_CONTENT,
"as_of must be an ISO-8601 timestamp. Use the run cutoff, "
"then compare the known body with the live body.",
) from exc
Expand Down Expand Up @@ -2213,7 +2213,7 @@ async def read_ontology_neighborhood(
try:
cutoff_clock = parse_as_of_clock(knowledge_cutoff)
except ValueError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
try:
async with pool.acquire() as conn:
neighborhood = await visible_ontology_neighborhood(
Expand Down Expand Up @@ -2629,7 +2629,7 @@ async def compare_period_groupings(
try:
parse_period_code(period_code)
except ValueError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
async with pool.acquire() as conn:
rows = await fetch_period_comparison(conn, period_code)
demo_entity_ids: set[str] = set()
Expand Down Expand Up @@ -2685,7 +2685,7 @@ async def list_period_reports(
"""Available calibrated periods for one grouping kind (FIPC trend)."""
_require_post_read(account)
if grouping_kind not in GROUPING_KINDS:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "unknown grouping_kind")
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, "unknown grouping_kind")
async with pool.acquire() as conn:
summaries = await list_period_report_summaries(conn, grouping_kind)
demo_entity_ids: set[str] = set()
Expand Down Expand Up @@ -2715,11 +2715,11 @@ async def read_period_reports(
"""Calibrated IRT scores for one grouping kind and calendar period."""
_require_post_read(account)
if grouping_kind not in GROUPING_KINDS:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "unknown grouping_kind")
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, "unknown grouping_kind")
try:
parse_period_code(period_code)
except ValueError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
async with pool.acquire() as conn:
reports = await fetch_period_reports(conn, grouping_kind, period_code)
demo_entity_ids: set[str] = set()
Expand Down Expand Up @@ -2792,11 +2792,11 @@ async def rebuild_period_report_endpoint(
"""Refit or FIPC-score every group in the period. post_admin only."""
_require_post_admin(account)
if grouping_kind not in GROUPING_KINDS:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "unknown grouping_kind")
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, "unknown grouping_kind")
try:
parse_period_code(period_code)
except ValueError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
async with pool.acquire() as conn:
async with conn.transaction():
reports = await rebuild_period_reports(conn, grouping_kind, period_code)
Expand Down Expand Up @@ -3001,7 +3001,7 @@ async def chat_about_post(
question = request.question.strip()
if not question:
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY, "question is required"
status.HTTP_422_UNPROCESSABLE_CONTENT, "question is required"
)
post = await _load_visible_post(post_id, account, pool)
post_metadata = build_post_llm_metadata(post_id, post)
Expand Down Expand Up @@ -3594,7 +3594,7 @@ async def read_calendar(
_require_post_read(account)
if (window_start is None) ^ (window_end is None):
raise HTTPException(
status.HTTP_422_UNPROCESSABLE_ENTITY,
status.HTTP_422_UNPROCESSABLE_CONTENT,
"window_start and window_end must be supplied together",
)
settings = load_settings()
Expand Down
23 changes: 21 additions & 2 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from lineageweave.http_client import HttpClientError, get_json, post_form
from lineageweave.knowledge_graph import knowledge_graph_edges_for_post
from lineageweave.post_chat import ChatSourceDocument
from lineageweave.post_summary import POST_SUMMARY_CONTRACT_VERSION

_POSTGRES_ADMIN_DSN = os.environ.get(
Expand Down Expand Up @@ -568,7 +569,7 @@ def _seed_analysis_run(
)
other_account_id = cur.fetchone()[0]
visible_run_id = _seed_analysis_run(
"a" * 64,
"0" * 64,
Comment thread
seonghobae marked this conversation as resolved.
"visible-own-corp",
account_id,
"analysis_scope_corporate_entity",
Expand Down Expand Up @@ -3900,7 +3901,9 @@ def answer(self, question: str, sources) -> None:
)

assert response.status_code == 503
assert "no complete evidence object" in response.json()["detail"]
assert response.json()["detail"] == (
"Post chat is temporarily unavailable. Saved evidence is still available."
)


def test_live_chat_provider_error_does_not_leak_raw_error(
Expand Down Expand Up @@ -3943,6 +3946,14 @@ class _FailingAskClient:
def answer(self, question: str, sources) -> object:
raise Exception("raw-global-provider-secret")

async def _source(*_args, **_kwargs):
return [
ChatSourceDocument(
seeded_db["own_private_post_id"], "Authorized source", "Evidence"
)
]

monkeypatch.setattr("backend.app.global_ask_queue.gather_global_chat_sources", _source)
Comment thread
seonghobae marked this conversation as resolved.
monkeypatch.setattr("backend.app.main._post_chat_client", lambda **_kwargs: _FailingAskClient())
headers = {"Authorization": f"Bearer {demo_analyst_token}"}

Expand Down Expand Up @@ -5093,6 +5104,14 @@ def answer(self, question, sources): # noqa: ARG002 - contract shape
cited_post_ids=(sources[0].post_id,),
)

async def _source(*_args, **_kwargs):
return [
ChatSourceDocument(
seeded_db["own_private_post_id"], "Authorized source", "Evidence"
)
]

monkeypatch.setattr("backend.app.global_ask_queue.gather_global_chat_sources", _source)
monkeypatch.setattr("backend.app.main._post_chat_client", lambda **_kwargs: _FakeChatClient())
headers = {"Authorization": f"Bearer {demo_analyst_token}"}
submitted = client.post(
Expand Down
5 changes: 4 additions & 1 deletion backend/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def test_tepp_transport_url_defaults_empty_and_is_not_a_score(monkeypatch) -> No
monkeypatch.delenv("TEPP_TRANSPORT_URL", raising=False)
assert load_settings().tepp_transport_url == ""
monkeypatch.setenv("TEPP_TRANSPORT_URL", "https://tepp.example/v1/analysis-runs")
assert load_settings().tepp_transport_url == "https://tepp.example/v1/analysis-runs"
monkeypatch.setenv("TEPP_API_KEY", "runtime-only-secret")
settings = load_settings()
assert settings.tepp_transport_url == "https://tepp.example/v1/analysis-runs"
assert settings.tepp_api_key == "runtime-only-secret"


def test_keyverse_issuer_overrides_local_keycloak_and_uses_oidc_discovery(monkeypatch) -> None:
Expand Down
43 changes: 43 additions & 0 deletions docs/adr/0223-explicit-semantic-content-unit-kinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ADR 0223: Explicit semantic content unit kinds

**Status:** Accepted
**Date:** 2026-08-26

## Context

PRD-FR-4 requires ordered paragraph, list, table, formula,
conversation-turn, and image-region semantic units. The source parser already
kept those boundaries, but `post_content_unit.unit_kind_code` collapsed every
textual DOM unit to `dom` and every markup-free unit to `plain_text`. A stored
row therefore could not disclose which source boundary produced its embedding.

## Decision

1. PostgreSQL admits `paragraph`, `list`, `table`, `formula`, and
`conversation_turn` as governed `post_content_unit_kind` values. Existing
`plain_text`, `dom`, and `image` values remain valid historical values.
2. New writes classify only explicit source boundaries: paragraph/plain-text
chunks, `li`, table rows, top-level MathML `math`, and caller-parsed
conversation turns. Unknown DOM blocks remain `dom`; no prose pattern or
model guess manufactures a kind.
3. MathML is retained as one ordered formula boundary. This decision does not
parse, evaluate, or assign mathematical meaning to the expression.
4. Image regions remain normalized children of their document-order image
unit under ADR 0091 rather than duplicating them as top-level content units.
5. A source adapter may pass already parsed `Chunk` units to persistence.
LineageWeave does not infer RFC 5322 sender boundaries from an opaque body.

## Consequences

- Embedding rows remain attached to the same ordered source unit while their
stored kind becomes inspectable and stable.
- Existing rows are not rewritten, so provenance is preserved.
- Formula evaluation and formula ontology remain outside LineageWeave.

## References

World Wide Web Consortium. (2025). *MathML Core* (Candidate Recommendation
Snapshot, June 24, 2025). https://www.w3.org/TR/2025/CR-mathml-core-20250624/

Resnick, P. W. (Ed.). (2008). *Internet message format* (RFC 5322). Internet
Engineering Task Force. https://www.rfc-editor.org/rfc/rfc5322
2 changes: 1 addition & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ decision from them.
|---|---|
| [`product-requirements.md`](../product-requirements.md) | Product requirements projection across the ADR set; ADRs remain normative |
| [`product-technical-gap-baseline.md`](../product-technical-gap-baseline.md) | Product/technical traceability projection across the ADR set; ADRs remain normative |
| [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md) |
| [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md), [0223](0223-explicit-semantic-content-unit-kinds.md) |
| [`PROV_O_IMPLEMENTATION.md`](../PROV_O_IMPLEMENTATION.md) | [0065](0065-prov-o-provenance-boundary.md) |
| [`PROV_O_IMPLEMENTATION_MATRIX.md`](../PROV_O_IMPLEMENTATION_MATRIX.md) | [0065](0065-prov-o-provenance-boundary.md) |
| [`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md) | [0207](0207-repository-case-ontology-namespace-canonical.md), [0157](0157-public-ontology-namespace-identity.md) |
Expand Down
Loading
Loading