core/workbench/construction_evidence.py
Shay 2c8258fe1a
feat(workbench): add UI catch-up evidence scaffolding (#891)
* feat(workbench): add construction evidence read model scaffolding

* feat(workbench-ui): add construction evidence TS types

* feat(workbench): add construction evidence journal projector

* test(workbench): cover construction evidence read model

* feat(workbench-ui): add construction evidence view helpers

* test(workbench-ui): cover construction evidence view helpers

* feat(workbench): add generalization audit evidence scaffolding

* test(workbench): cover generalization audit evidence view

* feat(workbench-ui): add generalization evidence TS types

* feat(workbench-ui): add generalization evidence view helpers

* test(workbench-ui): cover generalization evidence helpers

* feat(workbench): add proposal artifact authority scaffolding

* test(workbench): cover proposal artifact authority rules

* feat(workbench-ui): add proposal artifact TS types

* feat(workbench-ui): add proposal artifact view helpers

* test(workbench-ui): cover proposal artifact view helpers

* feat(workbench): add demo narrative evidence scaffolding

* test(workbench): cover demo narrative scaffolding

* feat(workbench-ui): add demo narrative TS types

* feat(workbench-ui): add demo narrative view helpers

* test(workbench-ui): cover demo narrative view helpers

* fix(evals): resolve discovery_candidates.jsonl via EngineStateStore
2026-06-23 10:09:45 -07:00

185 lines
5.9 KiB
Python

"""Read-only construction evidence projection helpers for Workbench.
This module is intentionally inert: it defines the Workbench-facing construction
read model and the honest missing-evidence constructor for legacy turns. It does
not parse problem text, execute candidate operators, run replay, mutate journals,
or grant serving authority.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
PipelineEvidenceStatus = Literal["recorded", "missing_evidence"]
CONSTRUCTION_EVIDENCE_ABSENT = "construction evidence was not persisted for this turn"
@dataclass(frozen=True, slots=True)
class SourceSpanView:
"""Exact source span projected to the Workbench.
The caller owns validation against the source string. This record must not be
normalized or repaired by the UI.
"""
start: int
end: int
text: str
@dataclass(frozen=True, slots=True)
class RoleObligationView:
role: str
required: bool
description: str
@dataclass(frozen=True, slots=True)
class ConstructionProposalView:
"""Diagnostic construction proposal.
A proposal is a hypothesis only. Runnable/refused authority belongs to the
corresponding ContractAssessmentView.
"""
family_id: str
relation_type: str
candidate_organ: str
status: Literal["proposed"]
evidence_spans: list[SourceSpanView] = field(default_factory=list)
role_obligations: list[RoleObligationView] = field(default_factory=list)
diagnostic_only: bool = True
serving_allowed: bool = False
@dataclass(frozen=True, slots=True)
class MentionView:
mention_id: str
kind: str
surface: str
span: SourceSpanView
fact_id: str | None = None
@dataclass(frozen=True, slots=True)
class MentionBindingView:
binding_type: str
source_mention_id: str
target_mention_id: str
evidence_spans: list[SourceSpanView] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class BoundRelationRoleView:
role: str
target_id: str
evidence_spans: list[SourceSpanView] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class BoundRelationView:
relation_type: str
roles: list[BoundRelationRoleView] = field(default_factory=list)
evidence_spans: list[SourceSpanView] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class ContractAssessmentView:
"""Read-only projection of generate.problem_frame_contracts.ContractAssessment."""
candidate_organ: str
family_id: str | None
missing_bindings: list[str] = field(default_factory=list)
unresolved_hazards: list[str] = field(default_factory=list)
runnable: bool = False
explanation: str = ""
evidence_spans: list[SourceSpanView] = field(default_factory=list)
@dataclass(frozen=True, slots=True)
class ConstructionEvidence:
"""Workbench-facing construction evidence for one turn.
This record is a projection, not an executor. `diagnostic_only=True` and
`serving_allowed=False` are load-bearing disclosure fields.
"""
schema_version: Literal["construction_evidence_v1"]
turn_id: int
status: PipelineEvidenceStatus
missing_reason: str | None
problem_text: str | None
proposals: list[ConstructionProposalView] = field(default_factory=list)
mentions: list[MentionView] = field(default_factory=list)
bindings: list[MentionBindingView] = field(default_factory=list)
bound_relations: list[BoundRelationView] = field(default_factory=list)
contract_assessments: list[ContractAssessmentView] = field(default_factory=list)
diagnostic_only: bool = True
serving_allowed: bool = False
def missing_construction_evidence(turn_id: int, reason: str) -> ConstructionEvidence:
"""Return the honest absence state for legacy turns.
Absence is not an error and not a failed proof. It means the selected turn has
no persisted construction evidence to project.
"""
return ConstructionEvidence(
schema_version="construction_evidence_v1",
turn_id=turn_id,
status="missing_evidence",
missing_reason=reason,
problem_text=None,
proposals=[],
mentions=[],
bindings=[],
bound_relations=[],
contract_assessments=[],
diagnostic_only=True,
serving_allowed=False,
)
def construction_evidence_from_journal_entry(entry: Any) -> ConstructionEvidence:
"""Project persisted construction evidence from a journal entry if present.
Current turn journals do not yet persist a construction evidence payload. This
helper is deliberately fail-closed and returns a typed missing-evidence record
rather than reconstructing a ProblemFrame from prose. When a later PR starts
persisting `construction_evidence`, this function is the narrow projection
seam to extend.
"""
turn_id = int(getattr(entry, "turn_id"))
payload = getattr(entry, "construction_evidence", None)
if payload is None:
return missing_construction_evidence(turn_id, CONSTRUCTION_EVIDENCE_ABSENT)
if isinstance(payload, ConstructionEvidence):
return payload
if not isinstance(payload, dict):
return missing_construction_evidence(
turn_id,
"construction evidence payload has unsupported shape",
)
# Future-compatible but conservative: do not attempt lossy coercion. The first
# persistence PR should replace this with explicit field-by-field projection and
# exact-span validation tests.
return missing_construction_evidence(
turn_id,
"construction evidence payload projection is not yet admitted",
)
def span_is_exact(problem_text: str, span: SourceSpanView) -> bool:
"""Return whether a span exactly matches its source text slice."""
return (
0 <= span.start <= span.end <= len(problem_text)
and bool(span.text)
and problem_text[span.start : span.end] == span.text
)