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: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ All notable changes to this project are documented here. Format follows

### Fixed

- `GET /healthz`: a stray decorator had stacked this route onto
`read_tenant_settings`, so the liveness probe silently required auth and
hit Postgres instead of returning `{"status": "ok"}`, and the real
`healthz()` handler had no route at all. Restored the decorator to the
correct handler.
- Closed the repository-wide docstring-coverage gap: added the 35 missing
public docstrings the AST audit found across `lineageweave/` and
`backend/app/`.
- The product-gap baseline now records private-runtime findings only as
aggregate synthetic-fixture contracts and identifies the existing
post-scoped lineage DAG without retaining post or organization identifiers.
Expand Down
1 change: 1 addition & 0 deletions backend/app/analysis_run_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def configured_tepp_client(transport_url: str = "", api_key: str = "") -> TeppCl
return TeppClient()

def transport(payload: dict[str, Any]) -> dict[str, Any]:
"""POST the TEPP wire payload to `url`, raising TeppNotAvailable on any transport failure."""
try:
headers = {"authorization": f"Bearer {api_key}"} if api_key.strip() else {}
return post_json(url, payload, headers=headers, timeout=30.0)
Expand Down
2 changes: 2 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

@dataclass(frozen=True)
class Settings:
"""Immutable snapshot of the backend's environment-driven configuration."""

database_url: str
# Reachable *from this backend process* -- used only to fetch JWKS
# signing keys. Inside docker-compose this is the internal service DNS
Expand Down
13 changes: 11 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,12 @@ async def _post_filter_options(
)


@app.get("/healthz")

@app.get("/api/settings", response_model=dict)
async def read_tenant_settings(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
):
"""Return the tenant's current brand name, defaulting to "LineageWeave" if unset."""
async with pool.acquire() as conn:
row = await conn.fetchrow("SELECT brand_name FROM tenant_settings WHERE id = 1")
if not row:
Expand All @@ -602,6 +601,7 @@ async def update_tenant_settings(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
):
"""Admin-only: upsert the tenant's brand name and return the stored value."""
# Only admins can change settings
_require_post_admin(account)
brand_name = payload.get("brandName", "LineageWeave")
Expand All @@ -614,6 +614,7 @@ async def update_tenant_settings(
return {"brandName": brand_name}


@app.get("/healthz")
async def healthz() -> dict[str, str]:
"""Liveness probe: the process is up. Does not touch Postgres."""
return {"status": "ok"}
Comment thread
seonghobae marked this conversation as resolved.
Expand Down Expand Up @@ -658,10 +659,14 @@ async def read_me(


class LocalePreferenceRequest(BaseModel):
"""Body of a PATCH /api/me/preferences request."""

preferred_locale: Literal["en", "ko", "zh", "ja", "vi"]


class CustomerHintResolveRequest(BaseModel):
"""Body of a POST /api/customer-master/resolve-hint request."""

hint_code: str


Expand Down Expand Up @@ -2663,6 +2668,8 @@ async def ask_agent(


class PostBookmarkRequest(BaseModel):
"""Body of a POST /api/posts/{post_id}/bookmark request."""

bookmarked: bool


Expand All @@ -2672,6 +2679,7 @@ async def read_post_bookmark(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Report whether the current account has bookmarked this post."""
await _load_visible_post(post_id, account, pool)
async with pool.acquire() as conn:
row = await conn.fetchrow(
Expand All @@ -2689,6 +2697,7 @@ async def write_post_bookmark(
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Set or clear the current account's bookmark on this post."""
await _load_visible_post(post_id, account, pool)
async with pool.acquire() as conn:
if request.bookmarked:
Expand Down
3 changes: 3 additions & 0 deletions backend/app/post_content_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

@dataclass(frozen=True)
class PostContentJobRequest:
"""One queued or running post-content ingestion job."""

post_id: str
source_body_sha256: str
status_code: str
Expand All @@ -35,6 +37,7 @@ def source_body_sha256(body: str) -> str:


def post_content_api_status(status_code: str | None, *, content_present: bool) -> str:
"""Map a job's internal status code to the API-facing status string."""
if status_code in _ACTIVE:
return "processing"
if status_code == FAILED:
Expand Down
13 changes: 13 additions & 0 deletions backend/app/post_content_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ async def process_post_content_job(
embedding_factory: Callable[[], EmbeddingClient],
structure_factory: Callable[[], PostStructureClient],
) -> None:
"""Claim, run, and record the outcome of one post-content ingestion job.

Claims the job for `post_id`/`source_body_digest` (a no-op if it is
already claimed, stale, or superseded), normalizes and persists the
post body through the given provider clients, then marks the job
succeeded or durably failed for retry.
"""
settings = load_settings()
row = await _claim_job(
pool,
Expand Down Expand Up @@ -283,6 +290,12 @@ async def consume_post_content_stream_once(
embedding_factory: Callable[[], EmbeddingClient],
structure_factory: Callable[[], PostStructureClient],
) -> str:
"""Process one batch of the Valkey wake-up stream and return the new cursor.

Reads up to 10 entries after `last_id`, runs `process_post_content_job`
for each, and returns the last-seen entry id so the caller can resume
from there on the next poll.
"""
batches = await client.xread({POST_CONTENT_STREAM_KEY: last_id}, count=10, block=1000)
for _stream_name, entries in batches:
for entry_id, fields in entries:
Expand Down
4 changes: 3 additions & 1 deletion backend/app/post_eligibility.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Shared source-post eligibility SQL for buyer evidence reads."""
"""Shared source-post eligibility SQL for analysis-facing evidence reads."""

SOURCE_CONTEXT_COLUMNS = (
"source_author_code",
Expand All @@ -17,12 +17,14 @@


def source_context_present_sql(alias: str) -> str:
"""SQL fragment: true if any source-context column on `alias` is non-blank."""
return " or ".join(
f"nullif(btrim({alias}.{column}), '') is not null" for column in SOURCE_CONTEXT_COLUMNS
)


def source_context_missing_sql(alias: str) -> str:
"""SQL fragment: true if every source-context column on `alias` is blank."""
return " and ".join(
f"nullif(btrim({alias}.{column}), '') is null" for column in SOURCE_CONTEXT_COLUMNS
)
Expand Down
3 changes: 3 additions & 0 deletions backend/app/post_evaluation_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

@dataclass(frozen=True)
class PersistedEvaluation:
"""One persisted per-criterion LLM-as-a-Judge response for a post."""

criterion_code: str
criterion_label: str | None
response_category: int
Expand Down Expand Up @@ -50,6 +52,7 @@ async def ingest_post_evaluation(


async def fetch_post_evaluation(conn: asyncpg.Connection, post_id: str) -> list[PersistedEvaluation]:
"""Load this post's persisted per-criterion evaluation responses, ordered by criterion code."""
rows = await conn.fetch(
"""
select e.criterion_code, v.lookup_label as criterion_label,
Expand Down
60 changes: 60 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
/ "migrations"
/ "0102_project_bound_summary_event.sql"
)
_TENANT_SETTINGS_MIGRATION = (
Path(__file__).resolve().parents[2] / "migrations" / "0103_tenant_settings.sql"
)
_CHANNEL_WEIGHT_MIGRATION = (
Path(__file__).resolve().parents[2]
/ "migrations"
Expand Down Expand Up @@ -242,6 +245,7 @@ def seeded_db(demo_analyst_token):
cur.execute(_MAJOR_EVENT_ACTION_MIGRATION.read_text())
cur.execute(_PROJECT_BOUND_ACTION_MIGRATION.read_text())
cur.execute(_PROJECT_BOUND_EVENT_MIGRATION.read_text())
cur.execute(_TENANT_SETTINGS_MIGRATION.read_text())
cur.execute(_CHANNEL_WEIGHT_MIGRATION.read_text())
cur.execute(_LEFTOVER_OBSERVED_EXPECTED_MIGRATION.read_text())
cur.execute(_LEFTOVER_MAP_RANK_MIGRATION.read_text())
Expand Down Expand Up @@ -2091,6 +2095,51 @@ def test_nonexistent_post_is_not_found(client, demo_analyst_token) -> None:
assert response.status_code == 404


def test_settings_get_returns_the_seeded_brand_name(
client, demo_analyst_token, seeded_db
) -> None:
"""An authenticated reader receives the persisted synthetic brand."""

response = client.get(
"/api/settings",
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert response.status_code == 200
assert response.json() == {"brandName": "LineageWeave"}


def test_update_settings_requires_post_admin(client, demo_analyst_token, seeded_db) -> None:
"""A non-admin reader cannot mutate tenant presentation settings."""

response = client.patch(
"/api/settings",
json={"brandName": "Someone Else's Brand"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert response.status_code == 403


def test_update_settings_as_admin_changes_the_brand_name(
client, demo_analyst_token, seeded_db
) -> None:
"""A post admin can persist and subsequently read a synthetic brand."""

_grant_post_admin(seeded_db["dsn"])
patch_response = client.patch(
"/api/settings",
json={"brandName": "Renamed Corp"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert patch_response.status_code == 200
assert patch_response.json() == {"brandName": "Renamed Corp"}

get_response = client.get(
"/api/settings",
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert get_response.json() == {"brandName": "Renamed Corp"}


def test_missing_token_is_unauthorized(client) -> None:
response = client.get("/api/posts")
assert response.status_code in (401, 403)
Expand Down Expand Up @@ -5145,3 +5194,14 @@ def test_post_search_matches_source_record_key_and_one_character_typo(
fuzzy = client.get("/api/posts", params={"search": typo}, headers=headers)
assert fuzzy.status_code == 200, fuzzy.text
assert any(post["post_id"] == seeded_db["own_private_post_id"] for post in fuzzy.json()["posts"])


def test_healthz_is_a_public_liveness_probe_not_tenant_settings(client) -> None:
"""/healthz must stay the plain liveness probe, never the tenant-settings route it once
collided with when a stray decorator stacked onto read_tenant_settings."""
health = client.get("/healthz")
assert health.status_code == 200
assert health.json() == {"status": "ok"}

unauthenticated_settings = client.get("/api/settings")
assert unauthenticated_settings.status_code in (401, 403)
10 changes: 9 additions & 1 deletion lineageweave/caldav_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Small independent CalDAV event-consumption port for the buyer calendar."""
"""Small independent CalDAV event-consumption port for the organization calendar."""

from __future__ import annotations

Expand All @@ -14,19 +14,26 @@

@dataclass(frozen=True)
class CalDavEvent:
"""One calendar event read from the configured CalDAV source."""

event_id: str
summary: str
starts_at: str


class NullCalDavClient:
"""Fail-closed CalDAV client used when CALDAV_BASE_URL is unset; never fabricates events."""

available = False

def list_events(self) -> list[CalDavEvent]:
"""Always return no events; there is no CalDAV source to query."""
return []


class HttpCalDavClient:
"""CalDAV client backed by a real HTTP events endpoint."""

available = True

def __init__(self, base_url: str) -> None:
Expand All @@ -36,6 +43,7 @@ def __init__(self, base_url: str) -> None:
self._events_url = f"{base_url.rstrip('/')}/events"

def list_events(self) -> list[CalDavEvent]:
"""Fetch and parse events from the configured CalDAV endpoint."""
payload = get_json(self._events_url, timeout=10)
rows = payload.get("events")
if not isinstance(rows, list):
Expand Down
2 changes: 2 additions & 0 deletions lineageweave/chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ def _split_dom_units(raw_text: str) -> list[tuple[str, int]]:
current: list[str] = []

def flush() -> None:
"""Join the buffered lines into one DOM unit and clear the buffer."""
if current:
raw_unit = "\n".join(current)
if raw_unit.strip():
Expand Down Expand Up @@ -513,6 +514,7 @@ def _split_plain_text_units(text: str) -> list[tuple[str, int, str]]:
current: list[str] = []

def flush() -> None:
"""Normalize the buffered lines into one plain-text unit and clear the buffer."""
if current:
raw_unit = "\n".join(current)
normalized = normalize_semantic_text(raw_unit)
Expand Down
11 changes: 11 additions & 0 deletions lineageweave/post_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

@dataclass(frozen=True)
class StructureDecision:
"""One unit's adjudicated indent level, with the evidence behind it."""

unit_index: int
indent_level: int
confidence: float
Expand All @@ -20,24 +22,32 @@ class StructureDecision:


class PostStructureClient(Protocol):
"""Port for adjudicating a post's document structure (indent levels per unit)."""

available: bool

def infer(
self, post_title: str, units: list[dict[str, object]]
) -> tuple[StructureDecision, ...]:
"""Return one StructureDecision per unit, or raise if unavailable."""
raise NotImplementedError


class NullPostStructureClient:
"""Fail-closed structure client used when no orchestrator is configured."""

available = False

def infer(
self, post_title: str, units: list[dict[str, object]]
) -> tuple[StructureDecision, ...]:
"""Always raise; there is no structure-adjudication backend to call."""
raise RuntimeError("post structure adjudication is not available")


class ContextualOrchestratorPostStructureClient:
"""Structure client backed by a real contextual-orchestrator deployment."""

available = True

_DECISION_ITEM_SCHEMA = {
Expand Down Expand Up @@ -71,6 +81,7 @@ def __init__(self, base_url: str, api_key: str, timeout: float = 600.0):
def infer(
self, post_title: str, units: list[dict[str, object]]
) -> tuple[StructureDecision, ...]:
"""Ask the orchestrator to adjudicate an indent level for each unit."""
if not units:
return ()
response = post_json(
Expand Down
Loading