feat(workbench-ui): expand Trace construction evidence inspection
Expand the Trace Construction panel from a summary scaffold into a read-only evidence inspection surface. Adds detail sections for construction proposals, mentions, mention bindings, bound relations, contract assessments, and source-span exactness rows. Preserves the proposal/contract authority boundary: proposals remain diagnostic and contract assessments determine runnable/blocker disposition. Validation: workbench-ui, smoke, and lane-shas GitHub Actions passed on PR head d20d143b5744d15d73d07a515b466143af6a2d52.
This commit is contained in:
parent
a19acac320
commit
ff1ef10bd7
4 changed files with 507 additions and 21 deletions
116
workbench-ui/src/app/trace/ConstructionEvidencePanel.test.tsx
Normal file
116
workbench-ui/src/app/trace/ConstructionEvidencePanel.test.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { render, screen, within } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ConstructionEvidence } from "../../types/constructionEvidence";
|
||||
import { ConstructionEvidencePanel } from "./ConstructionEvidencePanel";
|
||||
|
||||
const recordedEvidence: ConstructionEvidence = {
|
||||
schema_version: "construction_evidence_v1",
|
||||
turn_id: 7,
|
||||
status: "recorded",
|
||||
missing_reason: null,
|
||||
problem_text: "Lena has 3 marbles.",
|
||||
proposals: [
|
||||
{
|
||||
family_id: "binding.quantity_entity",
|
||||
relation_type: "quantity_entity",
|
||||
candidate_organ: "quantity_entity_binding_candidate.v1",
|
||||
status: "proposed",
|
||||
evidence_spans: [{ start: 9, end: 10, text: "3" }],
|
||||
role_obligations: [
|
||||
{
|
||||
role: "quantity",
|
||||
required: true,
|
||||
description: "source quantity mention",
|
||||
},
|
||||
],
|
||||
diagnostic_only: true,
|
||||
serving_allowed: false,
|
||||
},
|
||||
],
|
||||
mentions: [
|
||||
{
|
||||
mention_id: "m_lena",
|
||||
kind: "entity",
|
||||
surface: "Lena",
|
||||
span: { start: 0, end: 4, text: "Lena" },
|
||||
fact_id: null,
|
||||
},
|
||||
{
|
||||
mention_id: "m_bad",
|
||||
kind: "entity",
|
||||
surface: "Lena",
|
||||
span: { start: 0, end: 4, text: "Lena!" },
|
||||
fact_id: null,
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
{
|
||||
binding_type: "quantity_to_entity",
|
||||
source_mention_id: "m_quantity",
|
||||
target_mention_id: "m_lena",
|
||||
evidence_spans: [{ start: 0, end: 10, text: "Lena has 3" }],
|
||||
},
|
||||
],
|
||||
bound_relations: [
|
||||
{
|
||||
relation_type: "quantity_entity",
|
||||
roles: [
|
||||
{
|
||||
role: "entity",
|
||||
target_id: "m_lena",
|
||||
evidence_spans: [{ start: 0, end: 4, text: "Lena" }],
|
||||
},
|
||||
{
|
||||
role: "quantity",
|
||||
target_id: "m_quantity",
|
||||
evidence_spans: [{ start: 9, end: 10, text: "3" }],
|
||||
},
|
||||
],
|
||||
evidence_spans: [{ start: 0, end: 10, text: "Lena has 3" }],
|
||||
},
|
||||
],
|
||||
contract_assessments: [
|
||||
{
|
||||
candidate_organ: "quantity_entity_binding_candidate.v1",
|
||||
family_id: "binding.quantity_entity",
|
||||
missing_bindings: ["entity"],
|
||||
unresolved_hazards: ["ambiguous_quantity"],
|
||||
runnable: false,
|
||||
explanation: "missing entity",
|
||||
evidence_spans: [{ start: 0, end: 4, text: "Lena!" }],
|
||||
},
|
||||
],
|
||||
diagnostic_only: true,
|
||||
serving_allowed: false,
|
||||
};
|
||||
|
||||
describe("ConstructionEvidencePanel", () => {
|
||||
it("renders read-only construction detail sections without granting proposal authority", () => {
|
||||
render(
|
||||
<ConstructionEvidencePanel
|
||||
evidence={recordedEvidence}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
turnId={7}
|
||||
errorMessage={(error) => String(error)}
|
||||
/>,
|
||||
);
|
||||
|
||||
const panel = screen.getByTestId("construction-evidence-panel");
|
||||
expect(within(panel).getByText("Proposals")).toBeInTheDocument();
|
||||
expect(within(panel).getByText("Mentions")).toBeInTheDocument();
|
||||
expect(within(panel).getByText("Bindings")).toBeInTheDocument();
|
||||
expect(within(panel).getByText("Bound relations")).toBeInTheDocument();
|
||||
expect(within(panel).getByText("Contract assessments")).toBeInTheDocument();
|
||||
expect(within(panel).getByText("Source span checks")).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
within(panel).getByText("diagnostic proposal only; contract assessment determines runnable status"),
|
||||
).toBeInTheDocument();
|
||||
expect(within(panel).getAllByText("false").length).toBeGreaterThan(0);
|
||||
expect(within(panel).getAllByText(/quantity_entity_binding_candidate\.v1/).length).toBeGreaterThan(0);
|
||||
expect(within(panel).getAllByText("0:4 Lena — exact").length).toBeGreaterThan(0);
|
||||
expect(within(panel).getAllByText("0:4 Lena! — inexact").length).toBeGreaterThan(0);
|
||||
expect(within(panel).getByText("curl /trace/7/construction")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,10 @@ import { EmptyState } from "../../design/components/states/EmptyState";
|
|||
import { ErrorState } from "../../design/components/states/ErrorState";
|
||||
import { LoadingState } from "../../design/components/states/LoadingState";
|
||||
import { traceConstructionReproducer } from "./constructionEvidenceEndpoint";
|
||||
import { constructionEvidencePanelModel } from "./constructionEvidencePanelModel";
|
||||
import {
|
||||
type ConstructionEvidenceDetailSection,
|
||||
constructionEvidencePanelModel,
|
||||
} from "./constructionEvidencePanelModel";
|
||||
|
||||
export interface ConstructionEvidencePanelProps {
|
||||
evidence?: ConstructionEvidence | null;
|
||||
|
|
@ -20,6 +23,36 @@ function rowValue(value: string) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function DetailSection({ section }: { section: ConstructionEvidenceDetailSection }) {
|
||||
return (
|
||||
<section className="grid gap-2 rounded-md border border-[var(--color-border-subtle)] p-3">
|
||||
<h3 className="m-0 text-xs font-semibold uppercase text-[var(--color-text-secondary)]">
|
||||
{section.title}
|
||||
</h3>
|
||||
{section.items.length === 0 ? (
|
||||
<p className="m-0 text-sm text-[var(--color-text-secondary)]">{section.emptyMessage}</p>
|
||||
) : (
|
||||
<div className="grid gap-3">
|
||||
{section.items.map((item) => (
|
||||
<article className="grid gap-2" key={item.title}>
|
||||
<h4 className="m-0 font-mono text-xs text-[var(--color-text-primary)]">
|
||||
{item.title}
|
||||
</h4>
|
||||
<MetadataTable
|
||||
rows={item.rows.map((row) => ({
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
mono: true,
|
||||
}))}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConstructionEvidencePanel({
|
||||
evidence,
|
||||
isLoading,
|
||||
|
|
@ -91,6 +124,31 @@ export function ConstructionEvidencePanel({
|
|||
/>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-3" data-testid="construction-evidence-details">
|
||||
{model.detailSections.map((section) => (
|
||||
<DetailSection key={section.title} section={section} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="grid gap-2 rounded-md border border-[var(--color-border-subtle)] p-3">
|
||||
<h3 className="m-0 text-xs font-semibold uppercase text-[var(--color-text-secondary)]">
|
||||
Source span checks
|
||||
</h3>
|
||||
{model.sourceSpanRows.length > 0 ? (
|
||||
<MetadataTable
|
||||
rows={model.sourceSpanRows.map((row) => ({
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
mono: true,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<p className="m-0 text-sm text-[var(--color-text-secondary)]">
|
||||
No source spans recorded.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<div className="mb-1 text-xs font-semibold text-[var(--color-text-secondary)]">
|
||||
reproducer
|
||||
|
|
|
|||
|
|
@ -17,6 +17,91 @@ const missingEvidence: ConstructionEvidence = {
|
|||
serving_allowed: false,
|
||||
};
|
||||
|
||||
function recordedEvidence(): ConstructionEvidence {
|
||||
return {
|
||||
...missingEvidence,
|
||||
status: "recorded",
|
||||
missing_reason: null,
|
||||
problem_text: "Lena has 3 marbles.",
|
||||
proposals: [
|
||||
{
|
||||
family_id: "binding.quantity_entity",
|
||||
relation_type: "quantity_entity",
|
||||
candidate_organ: "quantity_entity_binding_candidate.v1",
|
||||
status: "proposed",
|
||||
evidence_spans: [{ start: 9, end: 10, text: "3" }],
|
||||
role_obligations: [
|
||||
{
|
||||
role: "quantity",
|
||||
required: true,
|
||||
description: "source quantity mention",
|
||||
},
|
||||
{
|
||||
role: "entity",
|
||||
required: true,
|
||||
description: "source entity mention",
|
||||
},
|
||||
],
|
||||
diagnostic_only: true,
|
||||
serving_allowed: false,
|
||||
},
|
||||
],
|
||||
mentions: [
|
||||
{
|
||||
mention_id: "m_lena",
|
||||
kind: "entity",
|
||||
surface: "Lena",
|
||||
span: { start: 0, end: 4, text: "Lena" },
|
||||
fact_id: null,
|
||||
},
|
||||
{
|
||||
mention_id: "m_bad",
|
||||
kind: "entity",
|
||||
surface: "Lena",
|
||||
span: { start: 0, end: 4, text: "Lena!" },
|
||||
fact_id: null,
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
{
|
||||
binding_type: "quantity_to_entity",
|
||||
source_mention_id: "m_quantity",
|
||||
target_mention_id: "m_lena",
|
||||
evidence_spans: [{ start: 0, end: 10, text: "Lena has 3" }],
|
||||
},
|
||||
],
|
||||
bound_relations: [
|
||||
{
|
||||
relation_type: "quantity_entity",
|
||||
roles: [
|
||||
{
|
||||
role: "entity",
|
||||
target_id: "m_lena",
|
||||
evidence_spans: [{ start: 0, end: 4, text: "Lena" }],
|
||||
},
|
||||
{
|
||||
role: "quantity",
|
||||
target_id: "m_quantity",
|
||||
evidence_spans: [{ start: 9, end: 10, text: "3" }],
|
||||
},
|
||||
],
|
||||
evidence_spans: [{ start: 0, end: 10, text: "Lena has 3" }],
|
||||
},
|
||||
],
|
||||
contract_assessments: [
|
||||
{
|
||||
candidate_organ: "quantity_entity_binding_candidate.v1",
|
||||
family_id: "binding.quantity_entity",
|
||||
missing_bindings: ["entity"],
|
||||
unresolved_hazards: ["ambiguous_quantity"],
|
||||
runnable: false,
|
||||
explanation: "missing entity",
|
||||
evidence_spans: [{ start: 0, end: 4, text: "Lena!" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("construction evidence panel model", () => {
|
||||
it("models missing evidence honestly", () => {
|
||||
const model = constructionEvidencePanelModel(missingEvidence);
|
||||
|
|
@ -38,27 +123,13 @@ describe("construction evidence panel model", () => {
|
|||
{ key: "bound_relations", value: "0" },
|
||||
{ key: "contract_assessments", value: "0" },
|
||||
]);
|
||||
expect(model.sourceSpanRows).toEqual([]);
|
||||
expect(model.detailSections.find((section) => section.title === "Proposals")?.items).toEqual([]);
|
||||
expect(model.showRaw).toBe(false);
|
||||
});
|
||||
|
||||
it("models recorded contract assessment blockers", () => {
|
||||
const model = constructionEvidencePanelModel({
|
||||
...missingEvidence,
|
||||
status: "recorded",
|
||||
missing_reason: null,
|
||||
problem_text: "Lena has 3 marbles.",
|
||||
contract_assessments: [
|
||||
{
|
||||
candidate_organ: "quantity_entity_binding_candidate.v1",
|
||||
family_id: "binding.quantity_entity",
|
||||
missing_bindings: ["entity"],
|
||||
unresolved_hazards: ["ambiguous_quantity"],
|
||||
runnable: false,
|
||||
explanation: "missing entity",
|
||||
evidence_spans: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const model = constructionEvidencePanelModel(recordedEvidence());
|
||||
|
||||
expect(model.emptyMessage).toBe(null);
|
||||
expect(model.countRows.find((row) => row.key === "contract_assessments")?.value).toBe("1");
|
||||
|
|
@ -70,4 +141,49 @@ describe("construction evidence panel model", () => {
|
|||
]);
|
||||
expect(model.showRaw).toBe(true);
|
||||
});
|
||||
|
||||
it("models proposals, mentions, bindings, bound relations, and exact span checks", () => {
|
||||
const model = constructionEvidencePanelModel(recordedEvidence());
|
||||
|
||||
const proposals = model.detailSections.find((section) => section.title === "Proposals");
|
||||
expect(proposals?.items[0]).toMatchObject({
|
||||
title: "proposal 1: quantity_entity_binding_candidate.v1",
|
||||
rows: expect.arrayContaining([
|
||||
{ key: "status", value: "proposed" },
|
||||
{ key: "diagnostic_only", value: "true" },
|
||||
{ key: "serving_allowed", value: "false" },
|
||||
{
|
||||
key: "authority",
|
||||
value: "diagnostic proposal only; contract assessment determines runnable status",
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
const mentions = model.detailSections.find((section) => section.title === "Mentions");
|
||||
expect(mentions?.items.map((item) => item.title)).toEqual([
|
||||
"mention m_lena: Lena",
|
||||
"mention m_bad: Lena",
|
||||
]);
|
||||
|
||||
const bindings = model.detailSections.find((section) => section.title === "Bindings");
|
||||
expect(bindings?.items[0].rows).toContainEqual({
|
||||
key: "target_mention_id",
|
||||
value: "m_lena",
|
||||
});
|
||||
|
||||
const relations = model.detailSections.find((section) => section.title === "Bound relations");
|
||||
expect(relations?.items[0].rows).toContainEqual({
|
||||
key: "roles",
|
||||
value: "entity=m_lena, quantity=m_quantity",
|
||||
});
|
||||
|
||||
expect(model.sourceSpanRows).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ key: "proposal 1.1", value: "9:10 3 — exact" },
|
||||
{ key: "mention m_lena", value: "0:4 Lena — exact" },
|
||||
{ key: "mention m_bad", value: "0:4 Lena! — inexact" },
|
||||
{ key: "contract_assessment 1.1", value: "0:4 Lena! — inexact" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import type { ConstructionEvidence } from "../../types/constructionEvidence";
|
||||
import type {
|
||||
BoundRelationRoleView,
|
||||
ConstructionEvidence,
|
||||
SourceSpanView,
|
||||
} from "../../types/constructionEvidence";
|
||||
import {
|
||||
assessmentBlockerSummary,
|
||||
assessmentDisposition,
|
||||
constructionEvidenceEmptyMessage,
|
||||
sourceSpanIsExact,
|
||||
sourceSpanLabel,
|
||||
} from "./constructionEvidenceView";
|
||||
import { traceConstructionReproducer } from "./constructionEvidenceEndpoint";
|
||||
|
||||
|
|
@ -11,6 +17,17 @@ export interface ConstructionEvidenceSummaryRow {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export interface ConstructionEvidenceDetailItem {
|
||||
title: string;
|
||||
rows: ConstructionEvidenceSummaryRow[];
|
||||
}
|
||||
|
||||
export interface ConstructionEvidenceDetailSection {
|
||||
title: string;
|
||||
emptyMessage: string;
|
||||
items: ConstructionEvidenceDetailItem[];
|
||||
}
|
||||
|
||||
export interface ConstructionEvidencePanelModel {
|
||||
status: ConstructionEvidence["status"];
|
||||
emptyMessage: string | null;
|
||||
|
|
@ -18,9 +35,186 @@ export interface ConstructionEvidencePanelModel {
|
|||
authorityRows: ConstructionEvidenceSummaryRow[];
|
||||
countRows: ConstructionEvidenceSummaryRow[];
|
||||
assessmentRows: ConstructionEvidenceSummaryRow[];
|
||||
detailSections: ConstructionEvidenceDetailSection[];
|
||||
sourceSpanRows: ConstructionEvidenceSummaryRow[];
|
||||
showRaw: boolean;
|
||||
}
|
||||
|
||||
function boolLabel(value: boolean): string {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
function noneIfEmpty(values: readonly string[]): string {
|
||||
return values.length > 0 ? values.join(", ") : "none";
|
||||
}
|
||||
|
||||
function spanExactness(problemText: string | null, span: SourceSpanView): string {
|
||||
const exactness = sourceSpanIsExact(problemText, span) ? "exact" : "inexact";
|
||||
return `${sourceSpanLabel(span)} — ${exactness}`;
|
||||
}
|
||||
|
||||
function spanList(problemText: string | null, spans: readonly SourceSpanView[]): string {
|
||||
return spans.length > 0
|
||||
? spans.map((span) => spanExactness(problemText, span)).join("; ")
|
||||
: "none";
|
||||
}
|
||||
|
||||
function roleList(roles: readonly BoundRelationRoleView[]): string {
|
||||
return roles.length > 0
|
||||
? roles.map((role) => `${role.role}=${role.target_id}`).join(", ")
|
||||
: "none";
|
||||
}
|
||||
|
||||
function sourceSpanRows(evidence: ConstructionEvidence): ConstructionEvidenceSummaryRow[] {
|
||||
const rows: ConstructionEvidenceSummaryRow[] = [];
|
||||
evidence.proposals.forEach((proposal, index) => {
|
||||
proposal.evidence_spans.forEach((span, spanIndex) => {
|
||||
rows.push({
|
||||
key: `proposal ${index + 1}.${spanIndex + 1}`,
|
||||
value: spanExactness(evidence.problem_text, span),
|
||||
});
|
||||
});
|
||||
});
|
||||
evidence.mentions.forEach((mention) => {
|
||||
rows.push({
|
||||
key: `mention ${mention.mention_id}`,
|
||||
value: spanExactness(evidence.problem_text, mention.span),
|
||||
});
|
||||
});
|
||||
evidence.bindings.forEach((binding, index) => {
|
||||
binding.evidence_spans.forEach((span, spanIndex) => {
|
||||
rows.push({
|
||||
key: `binding ${index + 1}.${spanIndex + 1}`,
|
||||
value: spanExactness(evidence.problem_text, span),
|
||||
});
|
||||
});
|
||||
});
|
||||
evidence.bound_relations.forEach((relation, relationIndex) => {
|
||||
relation.evidence_spans.forEach((span, spanIndex) => {
|
||||
rows.push({
|
||||
key: `bound_relation ${relationIndex + 1}.${spanIndex + 1}`,
|
||||
value: spanExactness(evidence.problem_text, span),
|
||||
});
|
||||
});
|
||||
relation.roles.forEach((role, roleIndex) => {
|
||||
role.evidence_spans.forEach((span, spanIndex) => {
|
||||
rows.push({
|
||||
key: `bound_relation ${relationIndex + 1}.role ${roleIndex + 1}.${spanIndex + 1}`,
|
||||
value: spanExactness(evidence.problem_text, span),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
evidence.contract_assessments.forEach((assessment, index) => {
|
||||
assessment.evidence_spans.forEach((span, spanIndex) => {
|
||||
rows.push({
|
||||
key: `contract_assessment ${index + 1}.${spanIndex + 1}`,
|
||||
value: spanExactness(evidence.problem_text, span),
|
||||
});
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function detailSections(evidence: ConstructionEvidence): ConstructionEvidenceDetailSection[] {
|
||||
return [
|
||||
{
|
||||
title: "Proposals",
|
||||
emptyMessage: "No construction proposals recorded.",
|
||||
items: evidence.proposals.map((proposal, index) => ({
|
||||
title: `proposal ${index + 1}: ${proposal.candidate_organ}`,
|
||||
rows: [
|
||||
{ key: "family_id", value: proposal.family_id },
|
||||
{ key: "relation_type", value: proposal.relation_type },
|
||||
{ key: "status", value: proposal.status },
|
||||
{ key: "diagnostic_only", value: boolLabel(proposal.diagnostic_only) },
|
||||
{ key: "serving_allowed", value: boolLabel(proposal.serving_allowed) },
|
||||
{
|
||||
key: "authority",
|
||||
value: "diagnostic proposal only; contract assessment determines runnable status",
|
||||
},
|
||||
{
|
||||
key: "role_obligations",
|
||||
value:
|
||||
proposal.role_obligations.length > 0
|
||||
? proposal.role_obligations
|
||||
.map(
|
||||
(role) =>
|
||||
`${role.role} ${role.required ? "required" : "optional"} — ${role.description}`,
|
||||
)
|
||||
.join("; ")
|
||||
: "none",
|
||||
},
|
||||
{ key: "evidence_spans", value: spanList(evidence.problem_text, proposal.evidence_spans) },
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Mentions",
|
||||
emptyMessage: "No mentions recorded.",
|
||||
items: evidence.mentions.map((mention) => ({
|
||||
title: `mention ${mention.mention_id}: ${mention.surface}`,
|
||||
rows: [
|
||||
{ key: "kind", value: mention.kind },
|
||||
{ key: "surface", value: mention.surface },
|
||||
{ key: "fact_id", value: mention.fact_id ?? "none" },
|
||||
{ key: "span", value: spanExactness(evidence.problem_text, mention.span) },
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Bindings",
|
||||
emptyMessage: "No mention bindings recorded.",
|
||||
items: evidence.bindings.map((binding, index) => ({
|
||||
title: `binding ${index + 1}: ${binding.binding_type}`,
|
||||
rows: [
|
||||
{ key: "binding_type", value: binding.binding_type },
|
||||
{ key: "source_mention_id", value: binding.source_mention_id },
|
||||
{ key: "target_mention_id", value: binding.target_mention_id },
|
||||
{ key: "evidence_spans", value: spanList(evidence.problem_text, binding.evidence_spans) },
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Bound relations",
|
||||
emptyMessage: "No bound relations recorded.",
|
||||
items: evidence.bound_relations.map((relation, index) => ({
|
||||
title: `bound relation ${index + 1}: ${relation.relation_type}`,
|
||||
rows: [
|
||||
{ key: "relation_type", value: relation.relation_type },
|
||||
{ key: "roles", value: roleList(relation.roles) },
|
||||
{ key: "evidence_spans", value: spanList(evidence.problem_text, relation.evidence_spans) },
|
||||
{
|
||||
key: "role_spans",
|
||||
value:
|
||||
relation.roles.length > 0
|
||||
? relation.roles
|
||||
.map((role) => `${role.role}: ${spanList(evidence.problem_text, role.evidence_spans)}`)
|
||||
.join("; ")
|
||||
: "none",
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: "Contract assessments",
|
||||
emptyMessage: "No contract assessments recorded.",
|
||||
items: evidence.contract_assessments.map((assessment, index) => ({
|
||||
title: `assessment ${index + 1}: ${assessment.candidate_organ}`,
|
||||
rows: [
|
||||
{ key: "family_id", value: assessment.family_id ?? "unknown" },
|
||||
{ key: "disposition", value: assessmentDisposition(assessment) },
|
||||
{ key: "runnable", value: boolLabel(assessment.runnable) },
|
||||
{ key: "missing_bindings", value: noneIfEmpty(assessment.missing_bindings) },
|
||||
{ key: "unresolved_hazards", value: noneIfEmpty(assessment.unresolved_hazards) },
|
||||
{ key: "explanation", value: assessment.explanation || "none" },
|
||||
{ key: "evidence_spans", value: spanList(evidence.problem_text, assessment.evidence_spans) },
|
||||
],
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function constructionEvidencePanelModel(
|
||||
evidence: ConstructionEvidence,
|
||||
): ConstructionEvidencePanelModel {
|
||||
|
|
@ -31,8 +225,8 @@ export function constructionEvidencePanelModel(
|
|||
authorityRows: [
|
||||
{ key: "schema_version", value: evidence.schema_version },
|
||||
{ key: "status", value: evidence.status },
|
||||
{ key: "diagnostic_only", value: evidence.diagnostic_only ? "true" : "false" },
|
||||
{ key: "serving_allowed", value: evidence.serving_allowed ? "true" : "false" },
|
||||
{ key: "diagnostic_only", value: boolLabel(evidence.diagnostic_only) },
|
||||
{ key: "serving_allowed", value: boolLabel(evidence.serving_allowed) },
|
||||
{ key: "missing_reason", value: evidence.missing_reason ?? "none" },
|
||||
],
|
||||
countRows: [
|
||||
|
|
@ -46,6 +240,8 @@ export function constructionEvidencePanelModel(
|
|||
key: `${assessment.candidate_organ}:${assessment.family_id ?? "unknown"}`,
|
||||
value: `${assessmentDisposition(assessment)} — ${assessmentBlockerSummary(assessment)}`,
|
||||
})),
|
||||
detailSections: detailSections(evidence),
|
||||
sourceSpanRows: sourceSpanRows(evidence),
|
||||
showRaw: evidence.status === "recorded",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue