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
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