Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a534573
fix: prioritize evidence-bound operations backfill
Aug 26, 2026
a630a66
test: gate operations runtime acceptance
Aug 26, 2026
5d2a26d
fix: keep runtime evidence outside checkout
Aug 26, 2026
59a63e8
Merge remote-tracking branch 'origin/fix/structured-workflow-exact-pi…
Aug 26, 2026
33cd926
test: prepare exact Dashboard load evidence
Aug 26, 2026
8b54b2f
fix: require durable digest for preferred backfill
Aug 26, 2026
8437a73
test: gate dashboard runtime evidence by exact images
Aug 26, 2026
006713e
docs: refresh dashboard acceptance head
Aug 26, 2026
f151ad0
docs: record canonical WAL pressure gap
Aug 26, 2026
bee26e5
build: keep revision label outside dependency cache
Aug 26, 2026
d21b702
test: require responsive dashboard acceptance evidence
Aug 26, 2026
239b6ba
test: support isolated dashboard acceptance project
Aug 26, 2026
10cdafb
test: verify runtime frontend endpoint contract
Aug 26, 2026
3d20c36
Merge updated structured-workflow pin into operations priority
Aug 26, 2026
53686aa
test: wait for dashboard backend readiness
Aug 26, 2026
763867e
Merge remote-tracking branch 'origin/feat/operations-candidate-priori…
Aug 26, 2026
5565568
Merge remote-tracking branch 'origin/fix/structured-workflow-exact-pi…
Aug 26, 2026
771415f
fix: keep worker health probes lightweight
Aug 26, 2026
5516e8b
Merge updated structured-workflow parent into operations priority
Aug 26, 2026
fada4d1
fix: read k6 summary export metrics
Aug 26, 2026
b41e8ec
fix: keep shell health probe silent
Aug 26, 2026
b40f5e1
Merge commit 'refs/codex/audit-711-new' into HEAD
Aug 26, 2026
f599d65
fix: accept worker heartbeat without newline
Aug 26, 2026
d0634d1
Merge commit 'refs/codex/audit-711-copyfix' into HEAD
Aug 26, 2026
c01de07
Merge commit 'refs/codex/audit-711-after725' into HEAD
Aug 26, 2026
2f5d9ee
fix(ui): keep mobile navigation and locale copy complete
Aug 26, 2026
e4a3701
fix(dashboard): render complete voice taxonomy
Aug 26, 2026
ea60884
fix(dashboard): retain external coverage rate
Aug 26, 2026
7c7bb2c
Merge pull request #736 from ContextualWisdomLab/fix/mobile-gnb-voice…
seonghobae Aug 26, 2026
9762f43
Merge remote-tracking branch 'origin/feat/operations-candidate-priori…
Aug 26, 2026
4c1a06b
fix(dashboard): name external share denominator
Aug 26, 2026
6aa4088
perf: bound dashboard backfill priority plan
Aug 26, 2026
a837ee5
docs: cite expanded Voice taxonomy authority
Aug 26, 2026
2fb753a
test: report actual explain scan loops
Aug 26, 2026
3eea373
fix: skip impossible priority scan
Aug 26, 2026
0a8bd0b
Merge pull request #737 from ContextualWisdomLab/fix/dashboard-comple…
seonghobae Aug 26, 2026
9a3f380
Merge branch 'feat/operations-candidate-priority' of https://github.c…
Aug 26, 2026
1aed4e6
fix: audit shared backfill SQL calls
Aug 26, 2026
f352414
fix: deduplicate backfill tier transitions
Aug 26, 2026
73848b2
docs: record backfill tier snapshot boundary
Aug 26, 2026
e1e5d9d
Merge pull request #739 from ContextualWisdomLab/perf/dashboard-backf…
seonghobae Aug 26, 2026
65e1dcd
feat: research public sources for post units (#714)
seonghobae Aug 26, 2026
7bdef97
docs: add current product and MCP manuals (#747)
seonghobae Aug 27, 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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ MCP_RATE_LIMIT_WINDOW_SECONDS=
# running contextual-orchestrator to turn the channels on.
ORCHESTRATOR_BASE_URL=
ORCHESTRATOR_API_KEY=
SOURCE_RESEARCH_MAXIMUM_LEADS=
SOURCE_RESEARCH_MAXIMUM_RESULTS=

# GitHub workflows inject the canonical provider names from masked secrets.
# Non-GitHub Compose runs also accept the operator's ~/.env compatibility
Expand Down
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@ contextual-orchestrator owns model discovery and selection.

`NullEmbeddingClient`, `NullAdjudicationClient`,
`NullKeymanExtractionClient`, `NullEntityRelationshipClient`,
`NullPostSummaryClient`, `NullPostChatClient`, and
`NullCommitmentExtractionClient` (and any new channel client you add)
`NullPostSummaryClient`, `NullPostChatClient`,
`NullCommitmentExtractionClient`, `NullRelationVerificationClient`,
`NullClaimVerificationClient`, and `NullSourceResearchClient`
(and any new channel client you add)
must set `available = False` and make their channel dropped +
renormalized (`reconstruct.active_weights`), never silently return a
placeholder score, invented Keyman, guessed relationship, fabricated
Expand All @@ -210,6 +212,14 @@ adjudication does -- never a raw LLM API. Demo TEPP seed goes through
envelope is Failed (`tepp_not_available` / `tepp_result_not_persisted`),
never a fabricated theta or a local psychometric substitute.

Public source-reference research (ADR 0248) is a post-scoped write action
on existing semantic units or image regions. Only `visibility_code=public`
posts may send lead text to SearXNG or retrieve a result URL. Private posts
fail closed without egress. Redirects and non-global targets are rejected.
Unavailable search, retrieval, or adjudication is `research_unavailable`,
never a fabricated supported/refuted judgment. Global Ask public
verification (ADR 0215) still never fetches result URLs.

The lineage `text` channel follows [ADR 0190](docs/adr/0190-lineage-text-channel-embedding-swap.md):
when an embedding provider is configured, `reconstruct()` precomputes
batched label embeddings once per reconstruction and scores cosine
Expand Down
13 changes: 13 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,19 @@ against a deliberately fabricated one in the same request, asserting
the former comes back `verify_corroborated` with a real evidence URL
and the latter `verify_uncorroborated` with none.

## Phase 6e: post-scoped source-reference research

Issue #611's remaining ADR 0133 criterion is a different workflow from
relation verification and from Global Ask snippet verification (ADR 0215).
A public post may send an existing semantic unit or image-region excerpt
to self-hosted SearXNG, retrieve one cited public page under SSRF and
redirect rejection, and ask contextual-orchestrator to judge in
`mode="verify"`. Private posts fail closed without egress. Citations
persist to `source_research_citation` (migration 0236, ADR 0248). The
reader next action is to open the cited public resource and compare it
with the highlighted passage or image detail. Global Ask still never
fetches result URLs.

## Phase 7: R&R's named actor is a PROV-O Agent, not always a person

`post_summary.py`'s R&R extraction forced every named actor into a
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.d/2.19.0-post-scoped-source-reference-research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 2.19.0 — Post-scoped source-reference research

## Added

- Public posts can research a highlighted passage or image detail against a
cited public page (ADR 0248, remaining ADR 0133 / issue #611). The workflow
reuses self-hosted SearXNG, retrieves one public HTTP(S) target with
redirects disabled and non-global addresses rejected, and judges through
contextual-orchestrator `mode=verify`. Private posts fail closed without
egress. Deployments must set both source-research resource budgets explicitly;
otherwise the channel remains unavailable. Citations persist in 3NF
`source_research_citation`.
- Reader next action: open the cited public resource, then compare it with
the highlighted passage or image detail. Supported or refuted judgments
without a cited URL downgrade to not enough information. Missing search,
retrieval, or adjudication is `research_unavailable`, never a fabricated
score.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ All notable changes to this project are documented here. Format follows

### Added

- Customer, MCP-client, and operator manuals now describe the shipped
Dashboard, twelve Voice categories, product evidence, asynchronous Ask
citations and related public originals, canonical Compose stack, OIDC/MCP
session handling, k6 observation procedure, and fail-closed TEPP boundary.
Each unavailable result points to a recovery action instead of exposing an
internal model or provider choice.

- Public posts can research a highlighted passage or image detail against a
cited public page (ADR 0248 / remaining ADR 0133). SearXNG finds candidates;
retrieval refuses redirects and non-global targets; contextual-orchestrator
judges in `mode=verify`. Private posts fail closed without sending content,
and absent explicit source-research resource budgets keep the channel unavailable.
After seed, open a public post and choose **Research public sources**, then
open the cited public resource and compare it with that highlighted content.
- Expanded Voice-of-X post taxonomy (ADR 0246): the governed `voc_type`
scheme adds Voice of Supplier, Employee, Business, Regulator, Investor,
Society, and Process as source-post categories. Ontology, SHACL, the
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Create/start endpoint rules (ADR 0017 / 0021), tie-vs-miss similarity
(ADR 0026), R&R catalog ids (ADR 0019 / 0027), leftover pairs
(ADR 0048–0164 / 0182 / 0201), the text-channel embedding swap and cosine
clamp (ADR 0190), per-edge channel-score persistence (ADR 0195),
migration replay (ADR 0166), docstring coverage, and the measurement
boundary are all stated in [AGENTS.md](AGENTS.md) -- read it before
changing code, tests, or runtime policy rather than restating anything
here.
migration replay (ADR 0166), docstring coverage, source-reference
research (ADR 0248), and the measurement boundary are all stated in
[AGENTS.md](AGENTS.md) -- read it before changing code, tests, or runtime
policy rather than restating anything here.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ docker compose --profile mcp up mcp
# Streamable HTTP resource: http://localhost:18001/mcp
```

For client initialization, tool arguments, durable status handling, and quota
recovery, see the [MCP manual](docs/manuals/mcp-manual.md). Workspace users can
start with the [user guide](docs/manuals/user-guide.md); deployment and incident
procedures are in the [operations manual](docs/manuals/operations-manual.md).

`GET /api/posts`, `GET /api/posts/{post_id}`,
`GET /api/posts/{post_id}/keymen`, `GET /api/keymen/{person_id}/related`,
`GET /api/posts/{post_id}/affiliate-tree`,
Expand Down
2 changes: 2 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --extra backend --no-editable \
&& chown -R appuser:appuser /app

ARG LINEAGEWEAVE_SOURCE_REVISION=unknown
LABEL org.opencontainers.image.revision=${LINEAGEWEAVE_SOURCE_REVISION}
USER appuser
EXPOSE 8000
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
8 changes: 8 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class Settings:
orchestrator_answer_timeout_seconds: float
valkey_url: str
searxng_base_url: str
source_research_maximum_leads: int | None
source_research_maximum_results: int | None
tepp_transport_url: str
tepp_api_key: str
caldav_base_url: str
Expand Down Expand Up @@ -205,6 +207,12 @@ def load_settings() -> Settings:
),
valkey_url=os.environ.get("VALKEY_URL", "redis://localhost:16379/0"),
searxng_base_url=os.environ.get("SEARXNG_BASE_URL", ""),
source_research_maximum_leads=_optional_positive_int(
"SOURCE_RESEARCH_MAXIMUM_LEADS"
),
source_research_maximum_results=_optional_positive_int(
"SOURCE_RESEARCH_MAXIMUM_RESULTS"
),
tepp_transport_url=os.environ.get("TEPP_TRANSPORT_URL", ""),
tepp_api_key=os.environ.get("TEPP_API_KEY", ""),
caldav_base_url=os.environ.get("CALDAV_BASE_URL", "").strip(),
Expand Down
24 changes: 18 additions & 6 deletions backend/app/global_ask_queue.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
gather_global_chat_sources,
prepare_global_question_embedding,
)
from .source_research_ingestion import list_ask_source_references

GLOBAL_ASK_STREAM_KEY = "global_ask_request_stream"

Expand Down Expand Up @@ -438,13 +439,18 @@ def can_see(row: asyncpg.Record) -> bool:
verify_external=verify_external,
client=verification_client,
)
if knowledge_cutoff is None:
async with pool.acquire() as conn:
async with pool.acquire() as conn:
if knowledge_cutoff is None:
lineage_graph = await lineage_graphs_for_posts(conn, can_see, cited_ids)
images = await cited_post_images(conn, cited_ids)
else:
lineage_graph = {"nodes": [], "edges": [], "truncated": False}
images = []
else:
lineage_graph = {"nodes": [], "edges": [], "truncated": False}
images = []
source_references = await list_ask_source_references(
conn,
cited_ids,
checked_by=knowledge_cutoff,
)
cited_posts = cited_post_summaries(usable_sources, cited_ids)
cited_events = cited_post_events(usable_sources, cited_ids)
cited_evidence = cited_post_evidence(usable_sources, cited_ids)
Expand All @@ -462,9 +468,15 @@ def can_see(row: asyncpg.Record) -> bool:
"cited_events": cited_events,
"cited_post_evidence": cited_evidence,
"cited_post_images": images,
"cited_source_references": source_references,
"source_post_ids": [source.post_id for source in sources],
"lineage_graph": lineage_graph,
"delivery": build_ask_delivery(answer.answer_text, cited_posts, cited_evidence),
"delivery": build_ask_delivery(
answer.answer_text,
cited_posts,
cited_evidence,
source_references,
),
"external_verification_status": verification_status,
"external_claims": [claim.to_payload() for claim in external_claims],
"next_action": next_action,
Expand Down
137 changes: 137 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@
)
from backend.app.ranking_ingestion import load_visible_ranking_posts
from backend.app.relation_verification_ingestion import verify_post_relations_from_pool
from backend.app.source_research_ingestion import (
list_source_research_citations,
research_post_sources_from_pool,
)
from backend.app.report_ingestion import (
GROUPING_KINDS,
fetch_period_comparison,
Expand Down Expand Up @@ -239,6 +243,12 @@
NullRelationVerificationClient,
SearxngRelationVerificationClient,
)
from lineageweave.source_reference_research import (
PRIVATE_POST_UNAVAILABLE,
VISIBILITY_PUBLIC,
NullSourceResearchClient,
SearxngOrchestratedSourceResearchClient,
)
from lineageweave.semantic_hints import customer_hint_trust, format_semantic_hints
from lineageweave.semantic_query import (
ContextualOrchestratorSemanticQueryClient,
Expand Down Expand Up @@ -349,6 +359,27 @@ def _claim_verification_client_factory():
return _claim_verification_client()


def _source_research_client():
"""Return the post-scoped public-research client, or its unavailable null."""

settings = load_settings()
if not (
settings.searxng_base_url
and settings.orchestrator_base_url
and settings.orchestrator_api_key
and settings.source_research_maximum_leads is not None
and settings.source_research_maximum_results is not None
):
return NullSourceResearchClient()
return SearxngOrchestratedSourceResearchClient(
settings.searxng_base_url,
settings.orchestrator_base_url,
settings.orchestrator_api_key,
maximum_leads=settings.source_research_maximum_leads,
maximum_results=settings.source_research_maximum_results,
)


def _organization_name_resolution_client():
"""Live orchestrator client when configured; otherwise the unavailable null."""
settings = load_settings()
Expand Down Expand Up @@ -2459,6 +2490,112 @@ async def verify_post_entity_relationships(
}


@app.get("/api/posts/{post_id}/research-citations")
async def read_post_research_citations(
post_id: str,
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
"""Return persisted public-research citations for this post's source leads."""

post = await _load_visible_post(post_id, account, pool)
if str(post["visibility_code"]) != VISIBILITY_PUBLIC:
return {
"post_id": str(post["post_id"]),
"visibility_code": post["visibility_code"],
"unavailable_reason": PRIVATE_POST_UNAVAILABLE,
"citations": [],
}
async with pool.acquire() as conn:
citations = await list_source_research_citations(conn, post_id)
return {
"post_id": str(post["post_id"]),
"visibility_code": post["visibility_code"],
"unavailable_reason": None,
"citations": [
{
"lead_kind_code": row["lead_kind_code"],
"lead_source_unit_id": row["lead_source_unit_id"],
"lead_image_region_id": row["lead_image_region_id"],
"lead_excerpt_text": row["lead_excerpt_text"],
"search_query_text": row["search_query_text"],
"evidence_url": row["evidence_url"],
"evidence_title_text": row["evidence_title_text"],
"evidence_excerpt_text": row["evidence_excerpt_text"],
"judgment_code": row["judgment_code"],
"rationale_text": row["rationale_text"],
"next_action_text": row["next_action_text"],
"checked_at": row["checked_at"],
}
for row in citations
],
}


@app.post("/api/posts/{post_id}/research-citations")
async def research_post_source_references(
post_id: str,
account: CurrentAccount = Depends(get_current_account),
pool: asyncpg.Pool = Depends(get_pool),
valkey: redis.Redis = Depends(get_valkey),
) -> dict[str, Any]:
"""Search and retrieve a public resource for this post's source leads.

Private posts fail closed without sending content. Gated by post_admin
because retrieval is a real external-search write action.
"""

_require_post_admin(account)
post = await _load_visible_post(post_id, account, pool)
if str(post["visibility_code"]) != VISIBILITY_PUBLIC:
return {
"post_id": str(post["post_id"]),
"visibility_code": post["visibility_code"],
"unavailable_reason": PRIVATE_POST_UNAVAILABLE,
"citations": [],
}
client = _source_research_client()
if not client.available:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Public research is unavailable. Ask an administrator to enable it, "
"then try again.",
)
try:
with use_llm_metadata(build_post_llm_metadata(post_id, post)):
run = await research_post_sources_from_pool(
pool,
client,
post_id,
visibility_code=str(post["visibility_code"]),
)
except (HttpClientError, OSError, ValueError) as exc:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Public research could not be completed. Try again later or review "
"this post's existing evidence.",
) from exc
except Exception as exc: # noqa: BLE001 - provider boundary is fail-closed.
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Public research could not be completed. Try again later or review "
"this post's existing evidence.",
) from exc
await publish_activity_event(
valkey,
post_id,
"source_research_checked",
account.user_account_id,
f"Public sources reviewed: {len(run.citations)} item(s)",
)
return {
"post_id": run.post_id,
"visibility_code": run.visibility_code,
"unavailable_reason": run.unavailable_reason,
"citations": [citation.to_payload() for citation in run.citations],
}


@app.post("/api/posts/{post_id}/extract-keymen")
async def extract_post_keymen(
post_id: str,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ async def lifespan(_: MCPServer) -> AsyncIterator[McpAppContext]:
"lineageweave",
title="LineageWeave",
description="Authenticated provenance-bearing lineage intelligence.",
version="2.18.0",
version="2.19.0",
lifespan=lifespan,
token_verifier=token_verifier or KeyverseMcpTokenVerifier(resolved),
auth=AuthSettings(
Expand Down
Loading