Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b65ee74
feat(lineage): require TEPP criterion anchor for weights
seonghobae Aug 25, 2026
9e830ad
Merge remote-tracking branch 'origin/main' into feat/tepp-lineage-anc…
seonghobae Aug 25, 2026
f2da8d3
fix(tepp): preserve exact cutoff precision
seonghobae Aug 25, 2026
4af4ba0
feat(dashboard): add evidence operations workspace
seonghobae Aug 25, 2026
506ffb8
Merge branch 'feat/tepp-lineage-anchor-consumer' into feat/evidence-o…
seonghobae Aug 25, 2026
f40ecef
fix(tepp): enforce canonical anchor UUID
seonghobae Aug 25, 2026
e9ca1d0
fix(frontend): keep merged OIDC imports minimal
seonghobae Aug 25, 2026
f61c146
Merge remote-tracking branch 'origin/feat/tepp-lineage-anchor-consume…
seonghobae Aug 25, 2026
61fd631
Merge pull request #607 from ContextualWisdomLab/feat/evidence-operat…
seonghobae Aug 25, 2026
f8ad49d
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 25, 2026
0700943
feat: complete semantic dashboard evidence paths
seonghobae Aug 25, 2026
353d39b
Merge current main and repair dashboard delivery
seonghobae Aug 25, 2026
bc182f1
fix(tepp): promote only exact accepted lineage vectors
seonghobae Aug 25, 2026
83c817a
Revert "fix(tepp): promote only exact accepted lineage vectors"
seonghobae Aug 25, 2026
e9763ed
Reconcile concurrent dashboard delivery update
seonghobae Aug 25, 2026
34414c1
Merge remote-tracking branch 'origin/feat/tepp-lineage-anchor-consume…
seonghobae Aug 25, 2026
d89e448
fix(tepp): promote exact accepted lineage vectors
seonghobae Aug 25, 2026
8c7d39b
Merge remote-tracking branch 'origin/feat/tepp-lineage-anchor-consume…
seonghobae Aug 25, 2026
e844aaa
Merge remote-tracking branch 'origin/feat/tepp-lineage-anchor-consume…
seonghobae Aug 25, 2026
5c2a430
fix: bind semantic retrieval to resolved model
seonghobae Aug 25, 2026
b73243a
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Aug 25, 2026
002d1d5
fix: bind semantic retrieval to resolved model and page VOC
seonghobae Aug 25, 2026
08a689a
fix(ui): preserve settled calendar status semantics
seonghobae Aug 25, 2026
409e9b3
Merge remote-tracking branch 'refs/remotes/origin/pr-614-latest' into…
seonghobae Aug 25, 2026
fcbe697
Merge remote-tracking branch 'origin/main' into feat/dashboard-live-e…
seonghobae Aug 25, 2026
425f398
Merge remote-tracking branch 'origin/feat/dashboard-live-evidence' in…
seonghobae Aug 25, 2026
718aa1f
fix(lineage): preserve focused isolation reason
seonghobae Aug 25, 2026
43f0751
fix: close semantic evidence review gaps
seonghobae Aug 25, 2026
ec1ea32
docs: refresh exact-head product gap baseline
seonghobae Aug 25, 2026
fe9573d
fix(ui): preserve dashboard controls during reload
seonghobae Aug 25, 2026
f0b9602
feat(dashboard): distinguish retryable analysis failures
seonghobae Aug 25, 2026
416fd19
fix(ui): isolate similar VOC pagination by post
seonghobae Aug 25, 2026
6e486e8
docs: record exact-head dashboard acceptance evidence
seonghobae Aug 25, 2026
70bda1d
fix(ui): keep failed-analysis guidance coherent
seonghobae Aug 25, 2026
82fd36f
fix(ui): drop stale similar VOC pages after navigation
seonghobae Aug 25, 2026
a6f3183
Merge branch 'feat/dashboard-live-evidence' of https://github.com/Con…
seonghobae Aug 25, 2026
5abc5dd
test: type similar VOC fetch fallback
seonghobae Aug 25, 2026
801f6aa
feat(dashboard): cite linked evidence posts
seonghobae Aug 25, 2026
64cf83b
fix(api): keep similar VOC query in one audited template
seonghobae Aug 25, 2026
e6a18b2
fix(ask): restrict evidence to semantic candidates
seonghobae Aug 25, 2026
cbb5911
fix(api): bound similar VOC adjudication
seonghobae Aug 25, 2026
3e3b3ee
docs: fix temporal topic compute contracts (#617)
seonghobae Aug 25, 2026
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
20 changes: 9 additions & 11 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,15 @@ repos in the ecosystem:
trajectories, uncertainty-quantified estimates) is
[TEPP](https://github.com/ContextualWisdomLab/TEPP)'s job.

This is why the org-wide rule that mathematical/psychometrics computation
layers must be Rust with GPU + CPU multithreading does not apply to this
repo: LineageWeave does no such computation. Its heaviest per-request work
is fusing a handful of `[0, 1]` channel scores over a bounded candidate
window (`reconstruct.DEFAULT_CANDIDATE_WINDOW`, default 50) -- a scheduling
and orchestration problem, not a numerical-estimation one. If a future
version added real statistical inference (e.g. estimating thread-assignment
uncertainty), that layer would move into TEPP rather than being built here,
consistent with the dependency direction the ecosystem's own architecture
docs already establish (`psychometrics-commons`'s TRD explicitly forbids a
downstream product from reimplementing a measurement engine's model).
ADR 0208 fixes the end state: LineageWeave retains wire validation,
authorization, provenance persistence, and UI projection only. The current
Python IRT/report, residual-map, similarity, graph-ranking, and fusion paths
are explicitly inventoried migration debt rather than evidence that this
repository owns their mathematics. They move by construct to TEPP,
fast-mlsirm, or RankWeave after versioned Rust CPU/GPU owner contracts pass
recovery/equivalence checks; affected product paths fail closed during each
cutover rather than substituting a local estimate. See
`docs/doctoring/python-mathematical-compute-boundary-audit.md`.

## Data flow

Expand Down
15 changes: 15 additions & 0 deletions backend/app/analysis_run_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,21 @@ async def _persist_tepp_result(
anchor["criterion_validity_status"],
anchor["validated_pair_count"],
)
await conn.execute(
"""
update lineage_channel_weight
set anchor_method_code = 'tepp_lineage_criterion_v1'
where estimation_run_id = $1
and estimation_method_code = 'mls2plm_expected_information'
and source_snapshot_sha256 = $2
and knowledge_cutoff = $3
and sample_pair_count = $4
""",
estimation_run_id,
anchor["source_snapshot_sha256"],
anchor_cutoff,
anchor["validated_pair_count"],
)
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
except (asyncpg.PostgresError, TypeError, ValueError):
return False
return True
Expand Down
29 changes: 26 additions & 3 deletions backend/app/global_ask_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from fastapi import HTTPException, status

from lineageweave.ask_delivery import build_ask_delivery
from lineageweave.embedding_client import EmbeddingClient, NullEmbeddingClient
from lineageweave.http_client import HttpClientError
from lineageweave.observability import record_server_failure
from lineageweave.post_chat import (
Expand Down Expand Up @@ -213,6 +214,7 @@ async def compute_global_ask_answer(
process_unit_ids: set[str],
process_scope_limited: bool,
chat_client: PostChatClient,
embedding_client: EmbeddingClient | None = None,
) -> dict[str, Any]:
"""Assemble one complete Ask answer payload from authorized evidence.

Expand Down Expand Up @@ -243,6 +245,7 @@ def can_see(row: asyncpg.Record) -> bool:
process_unit_ids,
question=question_text,
today=today,
embedding_client=embedding_client,
)
except Exception as exc:
log_internal_fault("global_ask", exc)
Expand Down Expand Up @@ -346,6 +349,7 @@ async def process_global_ask_job(
*,
job_id: str,
chat_factory: Callable[[], PostChatClient],
embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient,
) -> None:
"""Claim, answer, and settle one Ask job.

Expand Down Expand Up @@ -392,6 +396,7 @@ async def process_global_ask_job(
process_unit_ids=process_unit_ids,
process_scope_limited=process_scope_limited,
chat_client=chat_client,
embedding_client=embedding_factory(),
),
timeout=JOB_DEADLINE_SECONDS,
)
Expand Down Expand Up @@ -505,6 +510,7 @@ async def consume_global_ask_stream_once(
*,
last_id: str,
chat_factory: Callable[[], PostChatClient],
embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient,
limiter: asyncio.Semaphore | None = None,
tasks: set[asyncio.Task] | None = None,
) -> str:
Expand All @@ -524,12 +530,21 @@ async def consume_global_ask_stream_once(
job_id = str(fields.get("global_ask_job_id", "")).strip()
if job_id:
if limiter is None:
await process_global_ask_job(pool, job_id=job_id, chat_factory=chat_factory)
await process_global_ask_job(
pool,
job_id=job_id,
chat_factory=chat_factory,
embedding_factory=embedding_factory,
)
else:
await limiter.acquire()
task = asyncio.create_task(
_process_and_release(
pool, job_id=job_id, chat_factory=chat_factory, limiter=limiter
pool,
job_id=job_id,
chat_factory=chat_factory,
embedding_factory=embedding_factory,
limiter=limiter,
)
)
if tasks is not None:
Expand All @@ -544,11 +559,17 @@ async def _process_and_release(
*,
job_id: str,
chat_factory: Callable[[], PostChatClient],
embedding_factory: Callable[[], EmbeddingClient],
limiter: asyncio.Semaphore,
) -> None:
"""Run one dispatched job and free its concurrency slot afterwards."""
try:
await process_global_ask_job(pool, job_id=job_id, chat_factory=chat_factory)
await process_global_ask_job(
pool,
job_id=job_id,
chat_factory=chat_factory,
embedding_factory=embedding_factory,
)
finally:
limiter.release()

Expand All @@ -568,6 +589,7 @@ async def run_global_ask_worker(
pool: asyncpg.Pool,
*,
chat_factory: Callable[[], PostChatClient],
embedding_factory: Callable[[], EmbeddingClient] = NullEmbeddingClient,
) -> None:
"""Run the at-least-once Ask consumer with periodic queued-row recovery."""
last_id = await _stream_tail(client)
Expand All @@ -586,6 +608,7 @@ async def run_global_ask_worker(
pool,
last_id=last_id,
chat_factory=chat_factory,
embedding_factory=embedding_factory,
limiter=limiter,
tasks=tasks,
)
Expand Down
16 changes: 8 additions & 8 deletions backend/app/lineage_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,14 @@ async def load_estimated_channel_weights(
sets: dict[str, list] = {}
for row in all_rows:
sets.setdefault(row["channel_set_code"], []).append(row)
rows = next(
(
candidate
for candidate in sets.values()
if {row["channel_code"] for row in candidate} == active_channels
),
[],
)
matching_sets = [
candidate
for candidate in sets.values()
if {row["channel_code"] for row in candidate} == active_channels
]
if len(matching_sets) != 1:
return None
rows = matching_sets[0]
Comment thread
seonghobae marked this conversation as resolved.
persisted = {row["channel_code"]: float(row["weight_value"]) for row in rows}
if not persisted or set(persisted) != active_channels:
return None
Expand Down
114 changes: 112 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
from lineageweave.post_summary import ContextualOrchestratorPostSummaryClient, NullPostSummaryClient
from lineageweave.relation_verification import NullRelationVerificationClient, SearxngRelationVerificationClient
from lineageweave.semantic_hints import customer_hint_trust, format_semantic_hints
from lineageweave.similar_voc import ContextualOrchestratorSimilarVocAnalysisClient
from lineageweave.ontology import LW
from lineageweave.rankweave_client import build_rankweave_client
from lineageweave.naruon_calendar_workspace import (
Expand Down Expand Up @@ -220,6 +221,8 @@

_POST_READ = "post_read"
_POST_ADMIN = "post_admin"
_SIMILAR_VOC_PAGE_SIZE = 8
_SIMILAR_VOC_REQUEST_TIMEOUT_SECONDS = 180.0


@asynccontextmanager
Expand Down Expand Up @@ -272,6 +275,7 @@
chat_factory=lambda: _post_chat_client(
timeout=load_settings().orchestrator_answer_timeout_seconds
),
embedding_factory=_embedding_client,
)
)
app.state.global_ask_worker = global_ask_worker
Expand Down Expand Up @@ -482,6 +486,18 @@
)


def _similar_voc_client():
"""Live semantic-pair client, or ``None`` when inference is unavailable."""
settings = load_settings()
if not (settings.orchestrator_base_url and settings.orchestrator_api_key):
return None
return ContextualOrchestratorSimilarVocAnalysisClient(
base_url=settings.orchestrator_base_url,
api_key=settings.orchestrator_api_key,
timeout=_SIMILAR_VOC_REQUEST_TIMEOUT_SECONDS,
)


def _rankweave_client():
"""In-process RankWeave unless RANKWEAVE_DISABLED=1 (ADR 0024)."""
return build_rankweave_client(disabled=load_settings().rankweave_disabled)
Expand Down Expand Up @@ -756,7 +772,11 @@
async with pool.acquire() as conn:
try:
return await fetch_operations_dashboard(
conn, account.corporate_entity_ids, period_start, period_end
conn,
account.corporate_entity_ids,
account.process_unit_ids,
period_start,
period_end,
)
except ValueError as exc:
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
Expand Down Expand Up @@ -1754,7 +1774,8 @@
# Safe SQL: the eligibility predicate is an immutable schema fragment; post id is bound.
row = await conn.fetchrow( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli
"""
select source_post.post_id, source_post.post_title, source_post.voc_type_code,
select source_post.post_id, source_post.post_title, source_post.post_body,
source_post.voc_type_code,
source_post.visibility_code, source_post.corporate_entity_id,
source_post.process_unit_id, source_post.created_at, source_post.author_account_id,
source_post.source_process_unit_code, source_post.source_author_code,
Expand All @@ -1776,6 +1797,95 @@
return row


@app.get("/api/posts/{post_id}/similar-voc")
async def read_similar_voc(
post_id: str,
offset: int = Query(0, ge=0),
account: CurrentAccount = Depends(get_current_account),
Comment on lines +1800 to +1804

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: Similar VOC fires orchestrator work on every post open

Opening any post detail auto-calls read_similar_voc (backend/app/main.py:1800), which runs up to 8 concurrent orchestrator adjudications inline with a 180s timeout. Bounded and ADR-0206-authorized, but every post open now issues expensive LLM work against the shared gateway.

Open in Devin Review

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

pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Return authorized, semantically adjudicated prior VOC evidence.

Persisted ``repeat_issue`` classifications narrow the candidate corpus
without lexical matching. contextual-orchestrator then establishes each
pair; event time orders the display and is not a relevance score.
"""
focal = await _load_visible_post(post_id, account, pool)
client = _similar_voc_client()
if client is None:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"similar VOC inference is unavailable; configure contextual-orchestrator and retry",
)
async with pool.acquire() as conn:
rows = await conn.fetch(
f"""
select post.post_id, post.post_title, post.post_body,
post.visibility_code, post.corporate_entity_id, post.process_unit_id,
coalesce(post.event_occurred_at, post.created_at) as occurred_at
Comment thread
seonghobae marked this conversation as resolved.
from operations_case_classification classification
join source_post post on post.post_id = classification.post_id
where classification.case_kind_code = 'repeat_issue'
and post.post_id <> $1
and post.post_body <> ''
and (post.visibility_code = 'public'
or (post.corporate_entity_id::text = any($2::text[])
and (cardinality($3::text[]) = 0
or post.process_unit_id::text = any($3::text[]))))
and {SOURCE_POST_ELIGIBILITY_SQL.format(alias='post')}
order by coalesce(post.event_occurred_at, post.created_at) desc, post.post_id
offset $4 limit $5
""",
post_id,
list(account.corporate_entity_ids),
list(account.process_unit_ids),
offset,
_SIMILAR_VOC_PAGE_SIZE + 1,
)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment on lines +1821 to +1844
Comment on lines +1821 to +1844
candidates = [row for row in rows[:_SIMILAR_VOC_PAGE_SIZE] if _can_see_post(account, row)]

async def _adjudicate(candidate: asyncpg.Record):
with use_llm_metadata(build_post_llm_metadata(post_id, focal)):
return await asyncio.to_thread(
client.analyze,
focal["post_title"],
focal["post_body"],
str(candidate["post_id"]),
candidate["post_title"],
candidate["post_body"],
)

try:
results = await asyncio.wait_for(
asyncio.gather(*(_adjudicate(candidate) for candidate in candidates), return_exceptions=True),
timeout=_SIMILAR_VOC_REQUEST_TIMEOUT_SECONDS,
)
except TimeoutError:
results = ()
Comment on lines +1858 to +1864

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: Batch timeout discards completed Similar VOC results

On asyncio.wait_for timeout at backend/app/main.py:1863, results becomes () and all completed per-candidate adjudications are dropped, returning empty items even when some finished. Per-candidate errors are kept via return_exceptions, but a batch timeout loses partial work.

Open in Devin Review

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

items = []
for candidate, evidence in zip(candidates, results):
if evidence is None or isinstance(evidence, BaseException):
continue
items.append(
{
"post_id": evidence.candidate_post_id,
"post_title": candidate["post_title"],
"issue_summary": evidence.issue_summary,
"focal_evidence_text": evidence.focal_evidence_text,
"candidate_evidence_text": evidence.candidate_evidence_text,
"customer_cohort_text": evidence.customer_cohort_text,
"action_history": evidence.action_history,
"occurred_at": candidate["occurred_at"].isoformat(),
}
)
return {
"items": items,
"next_offset": offset + _SIMILAR_VOC_PAGE_SIZE
if len(rows) > _SIMILAR_VOC_PAGE_SIZE
else None,
}
Comment thread
seonghobae marked this conversation as resolved.


async def _load_post_semantic_hints(conn: asyncpg.Connection, post_id: str) -> str:
"""Render author, business-unit, sales-pool, and customer hints without treating them as proof."""
rows = await conn.fetch(
Expand Down
16 changes: 9 additions & 7 deletions backend/app/operations_case_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from __future__ import annotations

import hashlib
from typing import Any, Protocol

from backend.app.post_content_queue import source_body_sha256
from lineageweave.operations_case_analysis import OperationsCase


Expand All @@ -23,8 +23,8 @@ async def executemany(self, query: str, args: list[tuple[object, ...]]) -> Any:


def source_body_digest(body: str) -> str:
"""Return the digest that binds inference to an exact source body."""
return hashlib.sha256(body.encode("utf-8")).hexdigest()
"""Return the digest that binds inference to an exact focal source body."""
return source_body_sha256(body)


async def persist_operations_cases(
Expand All @@ -40,22 +40,24 @@ async def persist_operations_cases(
await conn.execute(
"insert into operations_case_analysis (post_id, source_body_sha256, orchestrator_session_id) values ($1, $2, $3)",
post_id,
source_body_digest(source_body),
source_body_sha256(source_body),
orchestrator_session_id,
)
for case in cases:
await conn.execute(
"insert into operations_case_classification (post_id, case_kind_code, summary_text, evidence_text) values ($1, $2, $3, $4)",
"insert into operations_case_classification (post_id, case_kind_code, summary_text, evidence_text, evidence_post_id, evidence_input_sha256) values ($1, $2, $3, $4, $5, $6)",
post_id,
case.case_kind_code,
case.summary_text,
case.evidence_text,
case.evidence_post_id,
case.evidence_input_sha256,
)
if case.facts:
await conn.executemany(
"insert into operations_case_fact (post_id, case_kind_code, fact_ordinal, fact_type_code, value_text, evidence_text) values ($1, $2, $3, $4, $5, $6)",
"insert into operations_case_fact (post_id, case_kind_code, fact_ordinal, fact_type_code, value_text, evidence_text, evidence_post_id, evidence_input_sha256) values ($1, $2, $3, $4, $5, $6, $7, $8)",
[
(post_id, case.case_kind_code, ordinal, fact.fact_type_code, fact.value_text, fact.evidence_text)
(post_id, case.case_kind_code, ordinal, fact.fact_type_code, fact.value_text, fact.evidence_text, fact.evidence_post_id, fact.evidence_input_sha256)
for ordinal, fact in enumerate(case.facts)
],
)
Loading
Loading