diff --git a/.agents/skills/run-behavior-diff-human-evaluation/scripts/quiz.py b/.agents/skills/run-behavior-diff-human-evaluation/scripts/quiz.py
index 1e4162f..2e87cc7 100644
--- a/.agents/skills/run-behavior-diff-human-evaluation/scripts/quiz.py
+++ b/.agents/skills/run-behavior-diff-human-evaluation/scripts/quiz.py
@@ -401,7 +401,7 @@ def _attention_shape(body):
children = _children(body)
if (
len(children) < 3
- or children[0].tag != "h3"
+ or children[0].tag != "h4"
or _plain(children[0]) != "What needs your attention"
or children[1].tag != "p"
or not children[1].has_class("attention-assessment")
@@ -531,7 +531,7 @@ def _blinded_report(source):
):
raise ValueError("Unknown summary story structure.")
steps = _children(story_children[1])
- names = ("finding", "expectation", "method", "evidence", "next")
+ names = ("evidence", "finding", "expectation", "method")
if len(steps) != len(names) or any(
step.tag != "li"
or not step.has_class("story-step")
@@ -543,34 +543,59 @@ def _blinded_report(source):
_shape(step, [("span", "story-number"), ("div", "story-body")])[1]
for step in steps
]
- # Check digest and qualification summarize the assessment; drop them but
- # accept only these exact optional siblings before the observations.
- evidence_shape = [("h3", None)]
- for tag, optional in (
- ("div", "summary-check-digest"),
- ("p", "summary-qualification"),
- ):
- if any(node.has_class(optional) for node in _children(bodies[3])):
- evidence_shape.append((tag, optional))
- evidence_shape.extend([("div", "summary-observations"), ("nav", "evidence-nav")])
- evidence_parts = _shape(bodies[3], evidence_shape)
- evidence_heading, observations = evidence_parts[0], evidence_parts[-2]
+ evidence_heading, observations, _ = _shape(
+ bodies[0],
+ [("h3", None), ("div", "summary-observations"), ("div", "summary-expected")],
+ )
children = _children(observations)
shape = [("p", "summary-provenance"), ("div", "summary-pair")]
if any(node.has_class("primary-result-context") for node in children):
shape.append(("section", "primary-result-context"))
- shape.append(
+ _shape(observations, shape)
+ scope_shape = [("h3", None)]
+ for tag, optional in (("p", "summary-qualification"), ("div", "summary-caution")):
+ if any(node.has_class(optional) for node in _children(bodies[1])):
+ scope_shape.append((tag, optional))
+ scope_shape.extend([("ul", "summary-notices"), ("div", "summary-attention")])
+ scope = _shape(bodies[1], scope_shape)
+ attention = scope[-1]
+ context = _shape(
+ bodies[3],
+ [("h3", None), ("details", "summary-context-details"), ("nav", "evidence-nav")],
+ )[1]
+ context_children = _children(context)
+ context_shape = [("summary", None), ("div", "summary-method-grid")]
+ context_shape.append(
("p", "summary-interpretation-unavailable")
if any(
- node.has_class("summary-interpretation-unavailable") for node in children
+ node.has_class("summary-interpretation-unavailable")
+ for node in context_children
)
else ("div", "summary-why")
)
- for optional in ("summary-case-context", "summary-caution"):
- if any(node.has_class(optional) for node in children):
- shape.append(("div", optional))
- shape.append(("div", "summary-attention"))
- _shape(observations, shape)
+ if any(node.has_class("summary-case-context") for node in context_children):
+ context_shape.append(("div", "summary-case-context"))
+ context_shape.append(("div", "summary-start-context"))
+ if any(node.has_class("summary-other-changes") for node in context_children):
+ context_shape.append(("p", "summary-other-changes"))
+ context_shape.append(("details", "summary-reading-help"))
+ _shape(context, context_shape)
+ retained = (
+ [
+ node
+ for node in context_children
+ if any(
+ node.has_class(name)
+ for name in (
+ "summary-why",
+ "summary-interpretation-unavailable",
+ "summary-case-context",
+ )
+ )
+ ]
+ + [node for node in scope if node.has_class("summary-caution")]
+ + [attention]
+ )
pair = children[1]
cards = _shape(
pair,
@@ -582,23 +607,30 @@ def _blinded_report(source):
)
for card in (cards[0], cards[2]):
parts = _children(card)
- start = bool(len(parts) > 1 and parts[1].has_class("summary-start"))
empty = bool(parts and parts[-1].has_class("summary-empty"))
- card_shape = [("h4", "summary-side-label")]
- if start:
- card_shape.append(("div", "summary-start"))
- card_shape.extend(
+ _shape(
+ card,
[
- ("svg", "summary-picture"),
+ ("h4", "summary-side-label"),
("p", "summary-empty") if empty else ("ul", "summary-choices"),
- ]
+ ],
+ )
+ starts = _single(
+ [node for node in context_children if node.has_class("summary-start-context")],
+ "comparison starts",
+ )
+ for start in _children(starts):
+ if start.tag != "div" or not start.has_class("summary-start"):
+ raise ValueError("Unknown comparison start.")
+ _shape(
+ start,
+ [
+ ("strong", None),
+ ("p", "summary-start-text"),
+ ("p", "summary-start-source"),
+ ],
)
- _shape(card, card_shape)
- if start:
- start_shape = [("p", "summary-start-text"), ("p", "summary-start-source")]
- if len(_children(parts[1])) == 3:
- start_shape.append(("span", "summary-start-arrow"))
- _shape(parts[1], start_shape)
+ retained.insert(0, starts)
source_intents = [
node
for node in nodes
@@ -607,12 +639,17 @@ def _blinded_report(source):
for name in ("purpose-goal-text", "edit-aim", "story-intent")
)
]
- for node in children:
+ for node in retained:
if node.has_class("summary-case-context"):
_case_context(node, source_intents)
elif node.has_class("summary-why") or node.has_class("summary-caution"):
_claim_shape(node)
- _attention_shape(children[-1])
+ _attention_shape(attention)
+ observations = _Node(
+ observations.tag,
+ observations.attrs.items(),
+ [*observations.children, *retained],
+ )
# Keep only the bounded observation section. Assessment verdicts, expected
# criteria, preparation rationale and next actions can reveal the keyed edit.
trimmed_story = _Node(
@@ -630,7 +667,7 @@ def _blinded_report(source):
[
_Node(
"li",
- steps[3].attrs.items(),
+ steps[0].attrs.items(),
[
_Node(
"div",
diff --git a/docs/architecture.md b/docs/architecture.md
index 86a5bff..f68b00a 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -49,7 +49,7 @@ scripts handle execution, evidence, and reporting.
+---------------------------------------------------------+
| Presentation |
| render.py -> consistent HTML, Markdown, structured JSON |
-| Five-step Summary; detailed tabs retain evidence |
+| Observation-first Summary; details retain evidence |
+----------------------------+----------------------------+
v
+---------------------------------------------------------+
@@ -558,19 +558,26 @@ semantic truth, causal attribution, or live extractor adherence.
| `report-data.json` | Structured, versioned report data. |
| `report-artifact.html` | Embeddable HTML body. |
-The default Summary has five numbered steps in one main reading column:
-finding; expected versus observed behavior; how tested; supporting evidence;
-and next action. A separate validated-result badge precedes a concrete heading
-from the lead extracted comparison's topic and status. An unmet requirement keeps
-the canonical finding prominent; a missing comparison uses that finding as its
-fallback. The comparison heading is not a claim that the change's goal was met.
-The first step pairs the recorded aim with observed choices and counts, then
-identifies unmet, unresolved and uncovered requirements without inventing findings.
-Full checks and original observation detail remain below; technical rejection
-reasons stay in diagnostics rather than dominating the first screen.
-A compact header keeps source/PR links, compared revisions and the model visible
-without a hover. Reading help, complete task, raw setup limits and rejected-check
-diagnostics expand inline. The other five tabs retain their navigation roles.
+The default Summary has four numbered sections in one reading column:
+observed Before/After; scope and attention; what the tests establish; and
+expandable test context and interpretation. Compact cards retain every branch
+and count, without decorative illustrations. The recorded aim follows the cards.
+A separate validated-result badge precedes the existing comparison heading;
+an unmet requirement retains canonical finding precedence. Neither a changed
+comparison nor a missing assessment is a pass of the instruction's goals.
+Material cautions, report-wide qualifications, incomplete evidence, and attention
+findings stay visible. The canonical finding, grouped check states, and every
+unmet check's full title precede one disclosure containing the full check table,
+rejection diagnostics, and unresolved or uncovered requirements. Next-action
+guidance remains visible. Setup, full task, model interpretation, and comparison
+starts expand separately. HTML and Markdown use the same hierarchy and evidence
+destinations; the other five tabs retain their content and navigation roles.
+
+The blind human-evaluation exporter validates this four-section DOM and projects
+only observations, primary-result context, comparison starts, interpretation,
+cautions, and attention evidence. Aim, assessment, setup, and next-action
+guidance remain excluded. Unknown layouts fail closed rather than using a
+legacy-layout fallback; saved already-built quiz artifacts remain unchanged.
The assessment conclusion is derived from validated target checks, not narrative prose.
An unmet After requirement remains unmet when Before made the same mistake.
@@ -648,9 +655,8 @@ Explanation validation additionally checks bounded narrative, row citations,
unchanged-row status, and exact example excerpts; it does not prove semantic
interpretation truth. Legacy comparison ranking is only a fallback when no validated
narrative selects an observed behavior.
-When target assessment is available, task-completion context stays in the detailed
-comparisons rather than adding another Summary block; mixed primary results still
-appear in the Summary's evidence-limit notices.
+When the selected lead differs from the canonical primary result, primary status
+and full distributions remain beside the cards even with a target assessment.
Changed explanations, citations, or presentation must not be described as changed
actions. When a fallback comparison lead is not the primary task result,
`content.primary_result_context` exposes the primary status and full
@@ -658,7 +664,7 @@ distribution beside those cards in both formats. Missing or incomplete evidence
cannot become an unchanged-result claim. The primary-result block remains
derived presentation, not a separate serialized report field.
The primary-result block is context, not a second navigation choice.
-**Understand the change**, under the Summary's evidence section, opens a new
+**Understand the change**, linked from the Summary's context section, opens the
top-level explanation tab. This middle layer explains the specific distinction,
its practical implications, important unchanged behavior, consistency and
minorities, and concrete limits, with links to decision rows and named trials.
@@ -673,7 +679,7 @@ instead of inferring a no-difference claim or making a new model call.
The general change explanation retains its completeness guard; incomplete
records do not erase supported target assessments of other completed trials.
Target coverage names unassessed records separately from observed failures.
-Plans are not presented as executions. The third Summary section gives concise
+Plans are not presented as executions. The second Summary section gives concise
consequences, actions, and essential evidence limits with one section-level detail link.
Findings linked to unmet After criteria come first. If no finding covers an
unmet criterion, a deterministic item retains its outcome distribution and
diff --git a/plugin/skills/behavior-diff/scripts/reporting/content.py b/plugin/skills/behavior-diff/scripts/reporting/content.py
index d33fc78..29ca3ae 100644
--- a/plugin/skills/behavior-diff/scripts/reporting/content.py
+++ b/plugin/skills/behavior-diff/scripts/reporting/content.py
@@ -364,11 +364,12 @@ def primary_result_context(report):
)
-SUMMARY_FINDING_HEADING = "What we found"
-SUMMARY_EXPECTATION_HEADING = "What was expected, and what happened"
-SUMMARY_METHOD_HEADING = "How we tested it"
-SUMMARY_EVIDENCE_HEADING = "What the evidence shows"
-SUMMARY_NEXT_HEADING = "What to do next"
+SUMMARY_FINDING_HEADING = "Scope and attention"
+SUMMARY_EXPECTATION_HEADING = "What these tests establish"
+SUMMARY_METHOD_HEADING = "Test context and interpretation"
+SUMMARY_EVIDENCE_HEADING = "Observed Before / After"
+SUMMARY_CHECK_DETAILS_LABEL = "All checks, rejection reasons, and unresolved questions"
+SUMMARY_CONTEXT_DETAILS_LABEL = "Setup, full task, and model interpretation"
SUMMARY_CONTEXT_LABEL = "Context for this comparison"
SUMMARY_MEANING_LABEL = "Model interpretation"
SUMMARY_ATTENTION_HEADING = "What needs your attention"
@@ -419,18 +420,16 @@ def required_evidence_text(check):
"The heading names the lead extracted comparison and whether it differs "
"between Before and After. A difference is not evidence that the edit's aim "
"was achieved.",
- "The badge and Step 1 give the result, decided only from the validated checks "
- "of the goals recorded before trials and from trial completeness. Step 1 also "
- "shows the edit's aim, the lead observed change, and what remains unknown or "
- "unmet.",
- "Step 2 lists each expected result with its Before and After results and says "
- "whether it was recorded before trials or written by the assessor afterward. "
- "Unclear, unassessable, and not-assessed trials stay separate from Yes and No.",
- "Step 3 shows the actual task, the scenario and test-data rationale recorded "
- "before trials, and the setup. Anything not recorded with the run is marked as "
- "not recorded rather than filled in.",
- "Step 4 shows the observed behavior; model interpretations are labeled.",
- "Step 5 suggests a next step. The report never approves or blocks a change.",
+ "Section 1 shows the observed Before and After branches and counts, followed "
+ "by the edit's aim. Section 2 keeps material uncertainty and attention visible.",
+ "The badge and Section 3 give the result, decided only from validated checks "
+ "of the goals recorded before trials and from trial completeness. Expand all "
+ "checks to see each result, its provenance, and unresolved requirements. "
+ "Unclear, unassessable, and not-assessed trials stay separate from Yes and No. "
+ "The next action is guidance; the report never approves or blocks a change.",
+ "Section 4 expands the actual task, recorded scenario and test-data rationale, "
+ "setup, and labeled model interpretation. Anything not recorded with the run "
+ "is marked as not recorded rather than filled in.",
)
@@ -449,13 +448,10 @@ def task_preview(task):
SUMMARY_RATIONALE_HEADING = "Why this test data challenges the aim"
SUMMARY_CONDITIONS_HEADING = "Test conditions"
SUMMARY_INSPECTED_HEADING = "What we inspected"
-SUMMARY_CAVEATS_HEADING = "What this does not prove"
SUMMARY_AIM_LABEL = "What the edit aimed at"
-SUMMARY_CHANGE_LABEL = "Observed change"
SUMMARY_OTHER_CHANGES_LABEL = "Other comparisons that differ"
SUMMARY_OPEN_LABEL = "Still unknown or unmet"
SUMMARY_AIM_UNCHECKED = "Whether the edit achieved the aim above"
-SUMMARY_IMPLICATION_LABEL = "What this means:"
SUMMARY_DIAGNOSTICS_LABEL = "Assessment diagnostic"
SUMMARY_DIGEST_HEADING = "Results by check"
@@ -604,7 +600,7 @@ def summary_heading(report):
def summary_result(report):
- """Return (result text, diagnostic or None) for Step 1.
+ """Return the canonical result text and any assessment diagnostic.
The canonical finding is repeated only when the heading does not already show
it. Without validated checks, the assessment's technical reason is diagnostic.
@@ -616,46 +612,14 @@ def summary_result(report):
return " ".join(parts), summary.meaning
-@dataclass(frozen=True)
-class SummaryChange:
- scope: str
- sides: Tuple[Tuple[str, str], ...]
- note: str
- others: Tuple[str, ...]
-
-
-def summary_change(report):
- """The lead extracted comparison, stated once, from extracted choices only."""
- summary = report.summary
- row = _lead_row(report)
- if row is None:
- return None
- sides = tuple(
- (
- label,
- "; ".join(
- f"{choice.choice} ({trial_count(choice.count, data.total)})"
- for choice in data.choices
- )
- or NO_EXTRACTED_CHOICE,
- )
- for label, data in (("Before", summary.before), ("After", summary.after))
- )
- others = (
- tuple(
- _row_name(other)
- for index, other in enumerate(report.decisions.rows, 1)
- if index != summary.decision and decision_status(other) == "Changed"
- )
- if complete_trial_evidence(report)
- else ()
- )
- return SummaryChange(
- f"Lead comparison, {source_label(row.anchor, report.metadata.trace_source)}:"
- f" {_row_name(row)}",
- sides,
- "Trial evidence is incomplete." if summary.status == "unavailable" else "",
- others,
+def summary_other_changes(report):
+ """Other changed comparisons, without repeating the lead's cards."""
+ if _lead_row(report) is None or not complete_trial_evidence(report):
+ return ()
+ return tuple(
+ _row_name(row)
+ for index, row in enumerate(report.decisions.rows, 1)
+ if index != report.summary.decision and decision_status(row) == "Changed"
)
@@ -736,62 +700,13 @@ def summary_check_groups(report):
)
-def _goal_gap(report):
- unchecked = unchecked_goals(report.decisions)
- if not unchecked:
- return ""
- noun = "goal" if len(unchecked) == 1 else "goals"
- return f"No check covers recorded {noun} " + ", ".join(map(str, unchecked)) + "."
-
-
-def summary_implication(report):
- """What the validated finding allows a reader to conclude, decided from states."""
- summary = report.summary
- checks = summary.checks
- state = summary.state
- if state == "unavailable":
- return (
- "No conclusion about the expected behavior is available. A difference "
- "in the observations is not evidence that the expected behavior was achieved."
- )
+def summary_check_link_label(report, state, number):
+ """Name unmet checks in the default view; other checks retain their numbers."""
if state == "not_met":
- return (
- "At least one expected behavior was not met in these tests; other "
- "supported results do not offset it."
- )
- if state == "inconclusive":
- settled = _settled(checks)
- unsettled = sum(check.state == "inconclusive" for check in checks)
- rejected = sum(check.state == "unavailable" for check in checks)
- gap = _goal_gap(report)
- parts = []
- if unsettled:
- parts.append(
- f"{unsettled} {'check is' if unsettled == 1 else 'checks are'} not settled."
- )
- if rejected:
- parts.append(
- f"{rejected} {'check was' if rejected == 1 else 'checks were'} not "
- "assessed because validation rejected them."
- )
- if gap:
- parts.append(gap)
- lead = (
- f"{settled} of {len(checks)} checks have a supported result, but this "
- "is not a pass of every expected behavior."
- if settled
- else "No check reached a supported result."
+ return next(
+ check.text for check in report.summary.checks if check.criterion == number
)
- return " ".join((lead, *parts))
- if state == "baseline_incomplete":
- return (
- "After met every check, but incomplete Before results mean no "
- "improvement is shown."
- )
- return (
- "Every check has a supported result in this scenario. This does not show "
- "reliability in other situations or that the edit caused the difference."
- )
+ return f"Check {number}"
def summary_qualification(report):
@@ -802,7 +717,7 @@ def summary_qualification(report):
"comparisons; they are retained as written, not endorsed."
)
if summary.state == "unavailable":
- # A rejection's technical reason stays in the Step 1 diagnostic.
+ # A rejection's technical reason stays in the full-checks diagnostic.
return (
f"{lead} Because no validated check of the expected behavior is "
"available, any claim of success or verification in them is unconfirmed."
diff --git a/plugin/skills/behavior-diff/scripts/reporting/render_html.py b/plugin/skills/behavior-diff/scripts/reporting/render_html.py
index fda4ed7..b682ad3 100644
--- a/plugin/skills/behavior-diff/scripts/reporting/render_html.py
+++ b/plugin/skills/behavior-diff/scripts/reporting/render_html.py
@@ -766,16 +766,12 @@ def _summary_start(report, side: str) -> str:
start = content.summary_start(report, side)
if start is None:
return ""
- arrow = (
- '↓'
- if start.ordered
- else ""
- )
return (
'
'
- f"{_summary_start(report, side)}{illustration(data.icon)}{body}"
+ f"{body}"
)
def _primary_result_context(report) -> str:
context = content.primary_result_context(report)
- if context is None or report.decisions.target_assessment is not None:
+ if context is None:
return ""
sides = "".join(
f"