core/docs/handoff/workbench-catchup-PR2-construction-read-model-brief-2026-06-23.md
Shay 172afc2d9e
docs(workbench): align UI catch-up plan and route docs (#890)
* docs(workbench): refresh UI route inventory

* docs(workbench): add master UI catch-up plan

* docs(workbench): add UI guide catch-up addendum

* docs(workbench): add construction read model implementation brief

* docs(workbench): add trace construction tab implementation brief

* docs(workbench): add generalization audit UI brief

* docs(workbench): add proposal artifact and demo theater brief

* docs: remove trailing whitespaces
2026-06-23 10:02:17 -07:00

6 KiB

Workbench Catch-up PR-2 Brief — Construction Evidence Read Model

Target PR title: feat(workbench): expose construction evidence read model Base: fresh origin/main after the docs catch-up PR merges Scope: backend read-only schemas + API endpoint only

Goal

Expose the proposal-first construction seam to Workbench as deterministic, read-only evidence for a journaled turn.

The surface to expose is:

surface/process cue
-> ConstructionProposal(status="proposed")
-> exact spans / mention bindings / bound relations
-> ContractAssessment
-> diagnostic runnable/refused

This PR must not create the frontend tab. It only creates the read model that the frontend can consume in the next PR.

Why

Trace currently exposes pipeline, field, bundle, surfaces, grounding, verdicts, metadata, and raw turn evidence. It does not expose the new proposal-first construction substrate. That makes the Workbench lag the kernel.

The demo-critical distinction is:

Proposal != Runnable
ContractAssessment determines runnable/refused
Diagnostic only
Serving disallowed

Required files to inspect first

  • workbench/api.py
  • workbench/schemas.py
  • workbench/readers.py
  • workbench/journal.py
  • workbench/pipeline_record.py
  • generate/construction_affordances.py
  • generate/problem_frame_contracts.py
  • generate/problem_frame.py
  • generate/kernel_facts.py
  • workbench-ui/src/types/api.ts
  • scripts/dump-api-schemas.py
  • existing API tests under tests/ and workbench-ui/src/api/

Backend endpoint

Add:

GET /trace/<turn_id>/construction

Response envelope is normal ok(...) / error(...) through WorkbenchApi.

Proposed schema

Add Python dataclasses in workbench/schemas.py:

@dataclass(frozen=True, slots=True)
class SourceSpanView:
    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:
    family_id: str
    relation_type: str
    candidate_organ: str
    status: Literal["proposed"]
    evidence_spans: list[SourceSpanView]
    role_obligations: list[RoleObligationView]
    diagnostic_only: bool
    serving_allowed: bool

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

@dataclass(frozen=True, slots=True)
class BoundRelationRoleView:
    role: str
    target_id: str
    evidence_spans: list[SourceSpanView]

@dataclass(frozen=True, slots=True)
class BoundRelationView:
    relation_type: str
    roles: list[BoundRelationRoleView]
    evidence_spans: list[SourceSpanView]

@dataclass(frozen=True, slots=True)
class ContractAssessmentView:
    candidate_organ: str
    family_id: str | None
    missing_bindings: list[str]
    unresolved_hazards: list[str]
    runnable: bool
    explanation: str
    evidence_spans: list[SourceSpanView]

@dataclass(frozen=True, slots=True)
class ConstructionEvidence:
    schema_version: Literal["construction_evidence_v1"]
    turn_id: int
    status: PipelineEvidenceStatus
    missing_reason: str | None
    problem_text: str | None
    proposals: list[ConstructionProposalView]
    mentions: list[MentionView]
    bindings: list[MentionBindingView]
    bound_relations: list[BoundRelationView]
    contract_assessments: list[ContractAssessmentView]
    diagnostic_only: bool
    serving_allowed: bool

Mirror the same shape in workbench-ui/src/types/api.ts.

Reader behavior

Add a read function in workbench/readers.py, likely:

def read_construction_evidence(entry: TurnJournalEntry) -> ConstructionEvidence:
    ...

Preferred behavior:

  1. If the journaled turn carries a persisted problem frame or construction detail, project it losslessly into the read model.
  2. If not persisted, return:
ConstructionEvidence(
    schema_version="construction_evidence_v1",
    turn_id=entry.turn_id,
    status="missing_evidence",
    missing_reason="construction evidence was not persisted for this turn",
    problem_text=None,
    proposals=[],
    mentions=[],
    bindings=[],
    bound_relations=[],
    contract_assessments=[],
    diagnostic_only=True,
    serving_allowed=False,
)

Do not reconstruct ProblemFrames from prose inside the reader. This endpoint is a projection, not a parser or runner.

API dispatch

In workbench/api.py, add before the generic /trace/<turn_id> branch:

if method == "GET" and path.startswith("/trace/") and path.endswith("/construction"):
    ...

The path should load the journal entry exactly like pipeline/field/bundle routes.

Hard constraints

  • No mutation.
  • No replay execution.
  • No candidate operator execution.
  • No search.
  • No solving.
  • No serving dispatch.
  • No hidden reconstruction from raw problem text.
  • No fabricated problem frame when evidence is absent.
  • Exact spans only: if span data exists, problem_text[start:end] == span.text must hold or the reader must mark/refuse the row rather than silently repair it.

Tests to add/run when local execution is available

Backend tests:

uv run python -m pytest -q tests/test_workbench_api.py tests/test_workbench_construction_evidence.py

Frontend/schema checks after TS mirror:

cd workbench-ui
pnpm test
pnpm build

Suggested new tests:

  • /trace/<id>/construction returns missing_evidence for legacy turns without construction records.
  • endpoint rejects non-integer turn ids with not_found.
  • endpoint returns normal JSON error envelope for missing turn.
  • exact span projector preserves start, end, text without normalization.
  • TS schema snapshot reflects Python schema.

Non-goals

  • No frontend tab.
  • No route registry changes.
  • No proposal ratification changes.
  • No generalization audit work.
  • No CORE-Logos Studio.
  • No practice trace reader.