From 10fdac4860035fd5c845a621752e93688e2e674e Mon Sep 17 00:00:00 2001 From: codex-scheduled Date: Tue, 11 Aug 2026 08:08:13 -0700 Subject: [PATCH] feat(rate-of-closure): complete regional result readback --- AGENT_HANDOFF.md | 23 +++++++ SPEC.md | 21 +++++- .../RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md | 23 +++++++ docs/release/rate_of_closure_campaign.v1.json | 17 ++++- src/rate_of_closure/AGENT_HANDOFF.md | 22 +++++++ .../regional_execution_readback.py | 65 +++++++++++++++++++ .../ui/pyqt6/regional_execution_evidence.py | 62 ++++++++++++++---- .../RegionalExecutionEvidencePanel.test.tsx | 8 ++- .../RegionalExecutionEvidencePanel.tsx | 62 +++++++++++++++--- .../model/regionalExecutionReadback.test.ts | 37 +++++++++++ .../src/model/regionalExecutionReadback.ts | 47 ++++++++++++++ .../test_regional_execution_readback.py | 48 ++++++++++++-- 12 files changed, 403 insertions(+), 32 deletions(-) diff --git a/AGENT_HANDOFF.md b/AGENT_HANDOFF.md index ec690d148..9cbb228b3 100644 --- a/AGENT_HANDOFF.md +++ b/AGENT_HANDOFF.md @@ -968,3 +968,26 @@ independent review is complete. UI construction of a qualified ground request and settled bounce prefix, executor invocation, playback, measured calibration, compiled regional physics, downstream parity, protected evidence, release, and issue completion remain open. + +## 2026-08-11 complete regional result readback continuation + +Branch `feat/4271-regional-result-readback` is a local unpublished child of +exact draft PR #4351 head +`fe463b5503a8c7b599a329da18bb690d008871cd`. It extends the matched import-only +PyQt6/React readback to every qualified summary/result field required for +honest user inspection: distinct carry/bounce/skid/roll/surface-path/total, +final downrange/offline, bounce count, ground time, terminal completion, model +and surface provider IDs/versions, calibration evidence, observed phases, +typed warnings, executor provenance, and qualification limits. + +Null-result cancellation/failure keeps ground-only values unavailable. Partial +evidence retains the censored-endpoint warning and is not relabeled as rest. +No physics, executor invocation, trajectory/event tables, playback, compiled +parity, calibration workflow, or downstream integration is added. + +Exact local gates are green: 208 expanded Python ground/plan/PyQt/layout tests, +111 React files / 691 tests, strict MyPy, Ruff/format, TypeScript type-check, +zero-warning ESLint, the 202-module production build, campaign-manifest +validation plus eight manifest tests, documentation governance, module-size +budget, placeholder scan, and diff checks. The build retains the inherited +500 kB chunk advisory. Independently review before any GitHub write. diff --git a/SPEC.md b/SPEC.md index f0392bb61..8053f551a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -26,8 +26,8 @@ | **Owner** | D-sorganization | | **Primary Language(s)** | Python 3.11+, Rust, JavaScript, TypeScript | | **License** | MIT | -| **Current Version** | 1.14.35 | -| **Spec Version** | 1.14.35 | +| **Current Version** | 1.14.36 | +| **Spec Version** | 1.14.36 | | **Last Spec Update** | 2026-08-11 | ## 2. Purpose & Mission @@ -46,6 +46,22 @@ Comprehensive monorepo housing 45+ utility tools for data processing, scientific ## 3. Goals & Non-Goals +### 2026-08-11 Complete Regional Execution Result Readback + +- The matched PyQt6 and React evidence readers project every already-qualified + ground summary field without recomputation: carry, bounce-air, skid, roll, + surface-path, total, final downrange/offline, bounce count, ground time, + terminal completion, model and base-surface provider identities, calibration + evidence, ordered observed phases, typed warnings, executor provenance, and + qualification limitations. +- Null-result cancellation/failure evidence keeps all ground-only values + unavailable and emits no fabricated phases, warnings, calibration, times, or + distances. Partial results retain their censored-endpoint warning and are not + presented as final-rest outcomes. +- PyQt6 uses a readable scrollable readback while React uses an accessible + definition list and warning list. Both remain import-only consumers of the + frozen Python executor envelope; this slice does not add playback or physics. + ### 2026-08-11 Regional Execution Evidence Readback - Matched PyQt6 and React regional-plan surfaces can import a bounded strict @@ -2978,6 +2994,7 @@ Active development with stable core, continuous tool expansion, and web API in p | Date | Version | Changes | | ---- | ------- | ------- | +| 2026-08-11 | 1.14.36 | feat(ground-roll, #4267 #4271): complete the matched PyQt6/React import-only result projection with carry, bounce-air, skid, roll, surface-path, total, final position/offline, bounce count, ground time, terminal completion, model and surface-provider identity/version, calibration evidence, ordered observed phases, typed warnings, executor provenance, and qualification limits; preserve unavailable ground-only values for null-result cancellation/failure and keep partial censored endpoints explicit; retain UI executor invocation, trajectory/event tables, playback, calibration workflows, compiled parity, downstream parity, protected evidence, release, and issue completion as open work. | | 2026-08-11 | 1.14.35 | feat(ground-roll, #4267 #4271): add matched PyQt6 and React bounded strict import/readback for Python-produced regional execution evidence; require its embedded plan to exactly match the currently valid visible plan, preserve prior accepted evidence after failed imports, invalidate stale evidence after plan edits, expose status/termination/model/skid/roll/total/transitions/provenance, and state explicitly that React executes no regional physics; retain UI executor invocation, playback, measured calibration, compiled physics, downstream parity, protected evidence, release, and issue completion as open work. | | 2026-08-11 | 1.14.34 | chore(ground-roll, #4271): normally reconcile exact reviewed regional-execution child `012cdfc33ad1590f31a1cbb109f0b8bee8eee700` with exact newly published PR #4342 parent `c1f47f2ef68b3db102da5416aaac17a40f675207` without changing the intended `feat/4274-regional-plan-io` base or rewriting either branch; retain the remediated embedded-plan execution/provenance contract and executor-produced cross-runtime evidence together with canonical request I/O, the bounded engineering-input helper, and verbatim parent/child handoff and SPEC history; keep UI execution/playback, compiled regional physics, downstream parity, protected evidence, PR publication, release, and issue completion open. | | 2026-08-11 | 1.14.33 | fix(ground-roll, #4271): remediate independent review of the regional execution envelope by embedding and hashing the exact plan, enforcing fixed executor producer/version with variable source revision, binding ordered transition identities and coordinates to both ground events and real plan crossings, aligning Python wire safe-number/integral-number/nonblank-text/vector validation with TypeScript, rejecting same-surface and fabricated mappings or unsubstantiated null-result ledgers, replacing synthetic evidence with executor-produced representable/cancelled/failed fixtures, and adding a shared adversarial parity corpus; preserve frozen base-result v1 and retain UI, compiled physics, downstream parity, protected evidence, release, and issue completion as open work. | diff --git a/docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md b/docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md index d2b6cd851..2e395cdc8 100644 --- a/docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md +++ b/docs/development/RATE_OF_CLOSURE_CAMPAIGN_HANDOFF.md @@ -1762,3 +1762,26 @@ production build, manifest and eight manifest tests, docs governance, structural budgets, and diff checks. The inherited 500 kB build advisory remains. Complete an independent audit before publication; then create a normal stacked draft child without retargeting or rewriting PR #4350. + +## 2026-08-11 Complete regional result readback continuation + +Local branch `feat/4271-regional-result-readback` starts exactly at draft PR +#4351 head `fe463b5503a8c7b599a329da18bb690d008871cd`. The next bounded result- +usability slice makes matched PyQt6/React evidence inspection complete for the +existing frozen result: carry, bounce-air, skid, roll, surface path, total, +final downrange/offline, bounce count, ground time, completion, model and +surface-provider identities/versions, calibration evidence, ordered observed +phases, typed warnings, executor provenance, and qualification limits. + +Cancellation/failure cannot fabricate ground-only values, and partial results +remain visibly censored. React still executes no ground physics. Trajectory and +event tables, export/playback, executor input construction/invocation, measured +calibration, compiled parity, Upstream consumers, protected evidence, release, +and issue completion remain open. + +Exact local gates are green: 208 expanded Python ground/plan/PyQt/layout tests, +111 React files / 691 tests, strict MyPy, Ruff/format, TypeScript type-check, +zero-warning ESLint, the 202-module production build, campaign-manifest +validation plus eight manifest tests, documentation governance, module-size +budget, placeholder scan, and diff checks. The inherited 500 kB build advisory +remains. Independent review is required before publication. diff --git a/docs/release/rate_of_closure_campaign.v1.json b/docs/release/rate_of_closure_campaign.v1.json index 121b79c0d..a707ddfb1 100644 --- a/docs/release/rate_of_closure_campaign.v1.json +++ b/docs/release/rate_of_closure_campaign.v1.json @@ -258,6 +258,16 @@ "state": "open", "merge_commit_sha": null, "protection_evidence": "not_recorded" + }, + { + "id": "pr-4351", + "pr": 4351, + "branch": "feat/4271-regional-execution-ui", + "base_branch": "feat/4271-regional-execution-binding", + "head_sha": "fe463b5503a8c7b599a329da18bb690d008871cd", + "state": "open", + "merge_commit_sha": null, + "protection_evidence": "not_recorded" } ], "test_evidence": [ @@ -1262,7 +1272,8 @@ "pr-4335", "pr-4339", "pr-4342", - "pr-4350" + "pr-4350", + "pr-4351" ], "supported_surfaces": { "tools.pyqt6": "implemented_unverified", @@ -1279,12 +1290,12 @@ "ground-regional-ui-helper-local-d217", "ground-regional-io-local-8e1c" ], - "evidence_gap": "The contract, flight-transfer, impact/bounce, static-plane skid/roll, regional-surface, regional-plan wire, regional-editor, regional request-I/O, and regional execution carriers remain open on draft PRs 4285, 4288, 4302, 4304, 4332, 4335, 4339, 4342, and 4350. A local unpublished child adds matched PyQt6/React import-only readback of exact plan-bound Python execution evidence. Fresh protected evidence for the reconciled heads, UI executor invocation/playback, workspace model-input persistence, measured calibration, changing geometry, compiled-runtime physics, and downstream parity remain absent.", + "evidence_gap": "The contract, flight-transfer, impact/bounce, static-plane skid/roll, regional-surface, regional-plan wire, regional-editor, regional request-I/O, regional execution, and evidence-readback carriers remain open on draft PRs 4285, 4288, 4302, 4304, 4332, 4335, 4339, 4342, 4350, and 4351. A local unpublished child completes matched summary/result inspection, warnings, phase presence, calibration evidence, and provider identity without browser physics. Fresh protected evidence for the reconciled heads, UI executor invocation, trajectory/event tables, export/playback, workspace model-input persistence, measured calibration, changing geometry, compiled-runtime physics, and downstream parity remain absent.", "limitations": [ "Airborne flight must first terminate against physical terrain and preserve terminal angular velocity.", "Current UpstreamDrift terrain and putting implementations are reference inputs rather than the qualified authority.", "The #4270 rigid impact law does not consume firmness, grass, compressibility, moisture, or rolling-resistance fields.", - "Published open PR #4304 qualifies one immutable planar profile and optional finite tangent-axis edge; open PR #4332 adds bounded coplanar material overlays; open PR #4335 adds a strict Python/TypeScript plan wire boundary and Python resolver; open PR #4339 adds matched PyQt6/React request editors; open PR #4342 adds editor-qualified canonical request import/export with native atomic semantics and explicit browser limitations; and open PR #4350 adds UI-neutral Python execution plus an embedded-plan-bound result/provenance envelope with validated transition crossings. A local child adds matched import-only execution evidence readback. Changing normals, height or velocity discontinuities, terrain deformation, torsional-spin damping, roll-to-skid transitions, UI executor invocation/playback, measured calibration, workspace model-input persistence, compiled or TypeScript regional physics, and downstream adapters remain unimplemented." + "Published open PR #4304 qualifies one immutable planar profile and optional finite tangent-axis edge; open PR #4332 adds bounded coplanar material overlays; open PR #4335 adds a strict Python/TypeScript plan wire boundary and Python resolver; open PR #4339 adds matched PyQt6/React request editors; open PR #4342 adds editor-qualified canonical request import/export with native atomic semantics and explicit browser limitations; open PR #4350 adds UI-neutral Python execution plus an embedded-plan-bound result/provenance envelope with validated transition crossings; and open PR #4351 adds matched strict plan-bound evidence import/readback. A local child completes all frozen summary/result fields, warnings, calibration, provider identity, and observed-phase readback. Changing normals, height or velocity discontinuities, terrain deformation, torsional-spin damping, roll-to-skid transitions, UI executor invocation, trajectory/event tables, export/playback, measured calibration, workspace model-input persistence, compiled or TypeScript regional physics, and downstream adapters remain unimplemented." ], "depends_on_issues": [ 4191 diff --git a/src/rate_of_closure/AGENT_HANDOFF.md b/src/rate_of_closure/AGENT_HANDOFF.md index 9597632a6..eccc2e835 100644 --- a/src/rate_of_closure/AGENT_HANDOFF.md +++ b/src/rate_of_closure/AGENT_HANDOFF.md @@ -1447,3 +1447,25 @@ remains. Independent review is still required before any GitHub write. Do not claim #4267/#4271 complete; UI executor inputs/invocation, playback, measured calibration, compiled regional physics, downstream parity, protected evidence, and release remain open. + +## 2026-08-11 Complete regional result readback + +Local unpublished `feat/4271-regional-result-readback` is an exact child of PR +#4351 head `fe463b5503a8c7b599a329da18bb690d008871cd`. Its matched PyQt6 and React +evidence presenters now expose every qualified result summary field, final +position, ground time, completion, bounce count, model/surface authority, +calibration evidence, ordered phases, warnings, executor provenance, and +limitations. Null-result states remain unavailable rather than fabricated; +partial endpoints retain their warning. + +This remains import/readback only. Complete local gates and independent review +before any GitHub write. Executor invocation, trajectory/event tables, export, +playback, calibration workflows, compiled parity, downstream parity, protected +evidence, release, and #4267/#4271 completion remain open. + +Exact local gates are green: 208 expanded Python ground/plan/PyQt/layout tests, +111 React files / 691 tests, strict MyPy, Ruff/format, TypeScript type-check, +zero-warning ESLint, the 202-module production build, campaign-manifest +validation plus eight manifest tests, documentation governance, module-size +budget, placeholder scan, and diff checks. The inherited 500 kB build advisory +remains. diff --git a/src/rate_of_closure/application/regional_execution_readback.py b/src/rate_of_closure/application/regional_execution_readback.py index 4265cda45..c9eb74e20 100644 --- a/src/rate_of_closure/application/regional_execution_readback.py +++ b/src/rate_of_closure/application/regional_execution_readback.py @@ -15,6 +15,15 @@ from .bounded_text_files import read_bounded_utf8 +@dataclass(frozen=True) +class RegionalExecutionWarningReadback: + """Typed warning projection retained for matched client presentation.""" + + code: str + severity: str + message: str + + @dataclass(frozen=True) class RegionalExecutionReadback: """Small UI-neutral projection of one frozen execution envelope.""" @@ -23,13 +32,29 @@ class RegionalExecutionReadback: failure_reason: str | None plan_id: str surface_id: str + surface_provider_id: str + surface_provider_version: str model_id: str model_version: str termination_reason: str | None + ground_time_s: float | None + completed: bool | None transition_count: int + carry_distance_m: float | None + bounce_air_distance_m: float | None skid_distance_m: float | None roll_distance_m: float | None + surface_path_distance_m: float | None total_distance_m: float | None + final_downrange_m: float | None + final_offline_m: float | None + bounce_count: int | None + calibration_id: str | None + calibration_kind: str | None + calibration_source: str | None + calibration_confidence: float | None + observed_phases: tuple[str, ...] + warnings: tuple[RegionalExecutionWarningReadback, ...] executor_source_revision: str executor_input_sha256: str limitations: tuple[str, ...] @@ -58,6 +83,23 @@ def regional_execution_readback( raise ValueError("execution evidence does not match the current regional plan") ground = result.ground_result summary = None if ground is None else ground.summary + phases = ( + () + if ground is None + else tuple(dict.fromkeys(point.phase.value for point in ground.trajectory)) + ) + warnings = ( + () + if ground is None + else tuple( + RegionalExecutionWarningReadback( + code=item.code, + severity=item.severity.value, + message=item.message, + ) + for item in ground.warnings + ) + ) return RegionalExecutionReadback( status=result.status.value, failure_reason=None @@ -65,13 +107,35 @@ def regional_execution_readback( else result.failure_reason.value, plan_id=result.plan_id, surface_id=result.surface_id, + surface_provider_id=result.regional_plan.base_surface.provider_id, + surface_provider_version=result.regional_plan.base_surface.provider_version, model_id=result.model_id, model_version=result.model_version, termination_reason=None if ground is None else ground.termination.reason.value, + ground_time_s=None if ground is None else ground.termination.time_s, + completed=None if ground is None else ground.termination.completed, transition_count=len(result.transitions), + carry_distance_m=None if summary is None else summary.carry_distance_m, + bounce_air_distance_m=( + None if summary is None else summary.bounce_air_distance_m + ), skid_distance_m=None if summary is None else summary.skid_distance_m, roll_distance_m=None if summary is None else summary.roll_distance_m, + surface_path_distance_m=( + None if summary is None else summary.surface_path_distance_m + ), total_distance_m=None if summary is None else summary.total_distance_m, + final_downrange_m=None if summary is None else summary.final_downrange_m, + final_offline_m=None if summary is None else summary.final_offline_m, + bounce_count=None if summary is None else summary.bounce_count, + calibration_id=None if ground is None else ground.calibration.calibration_id, + calibration_kind=None if ground is None else ground.calibration.kind.value, + calibration_source=None if ground is None else ground.calibration.source, + calibration_confidence=( + None if ground is None else ground.calibration.confidence + ), + observed_phases=phases, + warnings=warnings, executor_source_revision=result.executor_provenance.source_revision, executor_input_sha256=result.executor_provenance.input_sha256, limitations=result.limitations, @@ -101,6 +165,7 @@ def read_regional_execution_evidence( __all__ = [ "RegionalExecutionEvidence", "RegionalExecutionReadback", + "RegionalExecutionWarningReadback", "read_regional_execution_evidence", "regional_execution_readback", ] diff --git a/src/rate_of_closure/ui/pyqt6/regional_execution_evidence.py b/src/rate_of_closure/ui/pyqt6/regional_execution_evidence.py index 5a6df23b9..40936431f 100644 --- a/src/rate_of_closure/ui/pyqt6/regional_execution_evidence.py +++ b/src/rate_of_closure/ui/pyqt6/regional_execution_evidence.py @@ -10,6 +10,7 @@ QFormLayout, QGroupBox, QLabel, + QPlainTextEdit, QPushButton, QWidget, ) @@ -49,8 +50,9 @@ def __init__(self, host: RegionalExecutionEvidenceHost, parent: QWidget) -> None self.status_label = QLabel("No execution evidence loaded.") self.status_label.setWordWrap(True) self.status_label.setAccessibleName("Regional execution evidence status") - self.readback_label = QLabel("No accepted evidence") - self.readback_label.setWordWrap(True) + self.readback_label = QPlainTextEdit("No accepted evidence") + self.readback_label.setReadOnly(True) + self.readback_label.setMinimumHeight(180) self.readback_label.setAccessibleName("Regional execution evidence readback") layout = QFormLayout(self) layout.addRow(self.description) @@ -60,7 +62,7 @@ def __init__(self, host: RegionalExecutionEvidenceHost, parent: QWidget) -> None def clear(self) -> None: """Remove evidence made stale by a visible plan edit.""" - self.readback_label.setText("No accepted evidence") + self.readback_label.setPlainText("No accepted evidence") self.status_label.setText("Plan changed; execution evidence must be reloaded.") def open(self) -> None: @@ -85,7 +87,7 @@ def open(self) -> None: self.status_label.setAccessibleName("Regional execution evidence error") return self.recent_path = path - self.readback_label.setText(_format_readback(evidence.readback)) + self.readback_label.setPlainText(_format_readback(evidence.readback)) self.status_label.setText(f"Loaded {path.name}. No physics executed.") self.status_label.setAccessibleName("Regional execution evidence success") @@ -96,16 +98,52 @@ def _metric(value: float | None) -> str: def _format_readback(value: RegionalExecutionReadback) -> str: terminal = value.termination_reason or value.failure_reason or "unavailable" - return ( - f"{value.status} · {terminal} · plan {value.plan_id} · surface " - f"{value.surface_id} · model {value.model_id} {value.model_version} · " + lines = [ + f"Status: {value.status} · termination: {terminal} · " + f"ground time: {_seconds(value.ground_time_s)}", + f"Plan: {value.plan_id} · surface: {value.surface_id} · " + f"provider: {value.surface_provider_id} {value.surface_provider_version} · " + f"model: {value.model_id} {value.model_version}", + f"Carry {_metric(value.carry_distance_m)} · " + f"bounce {_metric(value.bounce_air_distance_m)} · " f"skid {_metric(value.skid_distance_m)} · " - f"roll {_metric(value.roll_distance_m)} · " + f"roll {_metric(value.roll_distance_m)}", + f"Surface path {_metric(value.surface_path_distance_m)} · " f"total {_metric(value.total_distance_m)} · " - f"{value.transition_count} transition(s) · " - f"source {value.executor_source_revision} · " - f"input {value.executor_input_sha256} · " - f"limits {', '.join(value.limitations)}" + f"final downrange {_metric(value.final_downrange_m)} · " + f"final offline {_metric(value.final_offline_m)}", + f"Bounces: {_count(value.bounce_count)} · transitions: " + f"{value.transition_count} · phases: {_phases(value.observed_phases)}", + f"Calibration: {_calibration(value)}", + f"Executor source: {value.executor_source_revision} · " + f"input: {value.executor_input_sha256}", + f"Qualification limits: {', '.join(value.limitations)}", + ] + lines.extend( + f"Warning {item.code} [{item.severity}]: {item.message}" + for item in value.warnings + ) + return "\n".join(lines) + + +def _seconds(value: float | None) -> str: + return "unavailable" if value is None else f"{value:.3f} s" + + +def _count(value: int | None) -> str: + return "unavailable" if value is None else str(value) + + +def _phases(values: tuple[str, ...]) -> str: + return "unavailable" if not values else " -> ".join(values) + + +def _calibration(value: RegionalExecutionReadback) -> str: + if value.calibration_kind is None: + return "unavailable" + return ( + f"{value.calibration_kind} · {value.calibration_id} · " + f"{value.calibration_source} · confidence {value.calibration_confidence}" ) diff --git a/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.test.tsx b/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.test.tsx index 13ad2a00b..2220f8c0b 100644 --- a/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.test.tsx +++ b/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.test.tsx @@ -24,7 +24,11 @@ describe("RegionalExecutionEvidencePanel", () => { "Regional execution evidence readback", )).toBeVisible()); expect(screen.getByText("partial")).toBeVisible(); - expect(screen.getByText("0.254 m")).toBeVisible(); + expect(screen.getAllByText("0.254 m")).toHaveLength(2); + expect(screen.getByText("0.040 m")).toBeVisible(); + expect(screen.getByText("1.155 s")).toBeVisible(); + expect(screen.getByText("impact → skid → roll")).toBeVisible(); + expect(screen.getByText(/CENSORED_ENDPOINT/)).toBeVisible(); expect(screen.getByText(/no browser physics executed/i)).toBeVisible(); fireEvent.change(input!, { target: { files: [{ @@ -33,7 +37,7 @@ describe("RegionalExecutionEvidencePanel", () => { text: async () => "{}", }] } }); await waitFor(() => expect(screen.getByRole("alert")).toBeVisible()); - expect(screen.getByText("0.254 m")).toBeVisible(); + expect(screen.getAllByText("0.254 m")).toHaveLength(2); expect(screen.getByText(/prior accepted execution evidence was preserved/i)).toBeVisible(); }); }); diff --git a/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.tsx b/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.tsx index 868af08bb..98ef1b4a5 100644 --- a/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.tsx +++ b/src/rate_of_closure/web/src/components/RegionalExecutionEvidencePanel.tsx @@ -1,4 +1,4 @@ -import { type ChangeEvent, useRef, useState } from "react"; +import { type ChangeEvent, type ReactNode, useRef, useState } from "react"; import type { GroundRegionalMaterialPlanRequest } from "../model/groundRegionalPlan"; import { @@ -8,6 +8,12 @@ import { const metric = (value: number | null): string => value === null ? "Unavailable" : `${value.toFixed(3)} m`; +const seconds = (value: number | null): string => + value === null ? "Unavailable" : `${value.toFixed(3)} s`; + +function ReadbackItem(props: { readonly label: string; readonly children: ReactNode }) { + return
{props.label}
{props.children}
; +} export function RegionalExecutionEvidencePanel(props: { readonly currentPlan: () => GroundRegionalMaterialPlanRequest; @@ -52,14 +58,44 @@ export function RegionalExecutionEvidencePanel(props: { className="mt-3 text-xs text-slate-400">{status}

{readback !== null &&
-
Status
{readback.status}
-
Termination
{readback.terminationReason ?? readback.failureReason ?? "Unavailable"}
-
Plan / surface
{readback.planId} / {readback.surfaceId}
-
Model
{readback.modelId} {readback.modelVersion}
-
Skid
{metric(readback.skidDistanceM)}
-
Roll
{metric(readback.rollDistanceM)}
-
Total
{metric(readback.totalDistanceM)}
-
Surface transitions
{readback.transitionCount}
+ {readback.status} + + {readback.terminationReason ?? readback.failureReason ?? "Unavailable"} + + {seconds(readback.groundTimeS)} + + {readback.completed === null ? "Unavailable" : readback.completed + ? "Completed" : "Observed endpoint"} + + + {readback.planId} / {readback.surfaceId} + + + {readback.surfaceProviderId} {readback.surfaceProviderVersion} + + + {readback.modelId} {readback.modelVersion} + + {metric(readback.carryDistanceM)} + {metric(readback.bounceAirDistanceM)} + {metric(readback.skidDistanceM)} + {metric(readback.rollDistanceM)} + {metric(readback.surfacePathDistanceM)} + {metric(readback.totalDistanceM)} + {metric(readback.finalDownrangeM)} + {metric(readback.finalOfflineM)} + {readback.bounceCount ?? "Unavailable"} + {readback.transitionCount} + + {readback.calibrationKind === null ? "Unavailable" + : `${readback.calibrationKind} · ${readback.calibrationId} · ` + + `${readback.calibrationSource} · confidence ` + + `${readback.calibrationConfidence}`} + + + {readback.observedPhases.length === 0 + ? "Unavailable" : readback.observedPhases.join(" → ")} +
Executor provenance
{readback.executorSourceRevision} · {readback.executorInputSha256}
@@ -68,6 +104,14 @@ export function RegionalExecutionEvidencePanel(props: {
Qualification limits
{readback.limitations.join(" · ")}
+ {readback.warnings.length > 0 &&
+
Warnings
+
    + {readback.warnings.map((warning, index) =>
  • + {warning.code} [{warning.severity}] — {warning.message} +
  • )} +
+
}
} ); diff --git a/src/rate_of_closure/web/src/model/regionalExecutionReadback.test.ts b/src/rate_of_closure/web/src/model/regionalExecutionReadback.test.ts index 83c85dd2e..12797d0a2 100644 --- a/src/rate_of_closure/web/src/model/regionalExecutionReadback.test.ts +++ b/src/rate_of_closure/web/src/model/regionalExecutionReadback.test.ts @@ -16,10 +16,47 @@ describe("regional execution evidence readback", () => { expect(readback.status).toBe("partial"); expect(readback.planId).toBe("regional-execution-plan-001"); expect(readback.terminationReason).toBe("time_limit"); + expect(readback.groundTimeS).toBeCloseTo(1.155); + expect(readback.completed).toBe(false); expect(readback.transitionCount).toBe(1); + expect(readback.carryDistanceM).toBeCloseTo(0); + expect(readback.bounceAirDistanceM).toBeCloseTo(0.04); expect(readback.skidDistanceM).toBeCloseTo(0); expect(readback.rollDistanceM).toBeCloseTo(0.25374857896); + expect(readback.surfacePathDistanceM).toBeCloseTo(0.25374857896); expect(readback.totalDistanceM).toBeCloseTo(0.2937485791); + expect(readback.finalDownrangeM).toBeCloseTo(0.2937485791); + expect(readback.finalOfflineM).toBeCloseTo(0); + expect(readback.bounceCount).toBe(1); + expect(readback.surfaceProviderId).toBe("tools.planar-surface"); + expect(readback.surfaceProviderVersion).toBe("1.0.0"); + expect(readback.calibrationId).toBe("literature-default-2026-08"); + expect(readback.calibrationKind).toBe("literature"); + expect(readback.calibrationSource).toBe("documented literature basis"); + expect(readback.calibrationConfidence).toBeCloseTo(0.6); + expect(readback.observedPhases).toEqual(["impact", "skid", "roll"]); + expect(readback.warnings[readback.warnings.length - 1]).toEqual({ + code: "CENSORED_ENDPOINT", + severity: "warning", + message: "Distance totals describe only the observed endpoint and are not projected final-rest metrics.", + }); + }); + + it("does not fabricate metrics for null-result cancellation evidence", () => { + const cancelled = parseGroundRegionalExecutionResult(fixture.cancelled.result); + const readback = regionalExecutionReadback(cancelled, cancelled.regional_plan); + + expect(readback.failureReason).toBe("cancelled"); + expect(readback.groundTimeS).toBeNull(); + expect(readback.completed).toBeNull(); + expect(readback.carryDistanceM).toBeNull(); + expect(readback.bounceCount).toBeNull(); + expect(readback.calibrationKind).toBeNull(); + expect(readback.calibrationId).toBeNull(); + expect(readback.calibrationSource).toBeNull(); + expect(readback.calibrationConfidence).toBeNull(); + expect(readback.observedPhases).toEqual([]); + expect(readback.warnings).toEqual([]); }); it("rejects evidence for a different plan and oversize browser files", async () => { diff --git a/src/rate_of_closure/web/src/model/regionalExecutionReadback.ts b/src/rate_of_closure/web/src/model/regionalExecutionReadback.ts index 4a8fb1586..4adb59041 100644 --- a/src/rate_of_closure/web/src/model/regionalExecutionReadback.ts +++ b/src/rate_of_closure/web/src/model/regionalExecutionReadback.ts @@ -13,18 +13,40 @@ export interface RegionalExecutionReadback { readonly failureReason: string | null; readonly planId: string; readonly surfaceId: string; + readonly surfaceProviderId: string; + readonly surfaceProviderVersion: string; readonly modelId: string; readonly modelVersion: string; readonly terminationReason: string | null; + readonly groundTimeS: number | null; + readonly completed: boolean | null; readonly transitionCount: number; + readonly carryDistanceM: number | null; + readonly bounceAirDistanceM: number | null; readonly skidDistanceM: number | null; readonly rollDistanceM: number | null; + readonly surfacePathDistanceM: number | null; readonly totalDistanceM: number | null; + readonly finalDownrangeM: number | null; + readonly finalOfflineM: number | null; + readonly bounceCount: number | null; + readonly calibrationId: string | null; + readonly calibrationKind: string | null; + readonly calibrationSource: string | null; + readonly calibrationConfidence: number | null; + readonly observedPhases: readonly string[]; + readonly warnings: readonly RegionalExecutionWarningReadback[]; readonly executorSourceRevision: string; readonly executorInputSha256: string; readonly limitations: readonly string[]; } +export interface RegionalExecutionWarningReadback { + readonly code: string; + readonly severity: string; + readonly message: string; +} + export interface RegionalExecutionEvidence { readonly result: GroundRegionalExecutionResult; readonly readback: RegionalExecutionReadback; @@ -45,18 +67,43 @@ export const regionalExecutionReadback = ( } const ground = result.ground_result; const summary = ground?.summary ?? null; + const phases = Object.freeze(Array.from(new Set( + ground?.trajectory.map((point) => point.phase) ?? [], + ))); + const warnings = Object.freeze((ground?.warnings ?? []).map((warning) => + Object.freeze({ + code: warning.code, + severity: warning.severity, + message: warning.message, + }))); return Object.freeze({ status: result.status, failureReason: result.failure_reason, planId: result.plan_id, surfaceId: result.surface_id, + surfaceProviderId: result.regional_plan.base_surface.provider_id, + surfaceProviderVersion: result.regional_plan.base_surface.provider_version, modelId: result.model_id, modelVersion: result.model_version, terminationReason: ground?.termination.reason ?? null, + groundTimeS: ground?.termination.time_s ?? null, + completed: ground?.termination.completed ?? null, transitionCount: result.transitions.length, + carryDistanceM: summary?.carry_distance_m ?? null, + bounceAirDistanceM: summary?.bounce_air_distance_m ?? null, skidDistanceM: summary?.skid_distance_m ?? null, rollDistanceM: summary?.roll_distance_m ?? null, + surfacePathDistanceM: summary?.surface_path_distance_m ?? null, totalDistanceM: summary?.total_distance_m ?? null, + finalDownrangeM: summary?.final_downrange_m ?? null, + finalOfflineM: summary?.final_offline_m ?? null, + bounceCount: summary?.bounce_count ?? null, + calibrationId: ground?.calibration.calibration_id ?? null, + calibrationKind: ground?.calibration.kind ?? null, + calibrationSource: ground?.calibration.source ?? null, + calibrationConfidence: ground?.calibration.confidence ?? null, + observedPhases: phases, + warnings, executorSourceRevision: result.executor_provenance.source_revision, executorInputSha256: result.executor_provenance.input_sha256, limitations: result.limitations, diff --git a/tests/rate_of_closure/test_regional_execution_readback.py b/tests/rate_of_closure/test_regional_execution_readback.py index 4072af93d..0c4e387d9 100644 --- a/tests/rate_of_closure/test_regional_execution_readback.py +++ b/tests/rate_of_closure/test_regional_execution_readback.py @@ -39,14 +39,51 @@ def test_readback_reports_frozen_executor_evidence_without_running_physics() -> assert readback.status == "partial" assert readback.plan_id == "regional-execution-plan-001" assert readback.surface_id == "firm-fairway" + assert readback.surface_provider_id == "tools.planar-surface" + assert readback.surface_provider_version == "1.0.0" assert readback.termination_reason == "time_limit" + assert readback.ground_time_s == pytest.approx(1.155) + assert readback.completed is False assert readback.transition_count == 1 + assert readback.carry_distance_m == pytest.approx(0.0) + assert readback.bounce_air_distance_m == pytest.approx(0.04) assert readback.skid_distance_m == pytest.approx(0.0) assert readback.roll_distance_m == pytest.approx(0.25374857896) + assert readback.surface_path_distance_m == pytest.approx(0.25374857896) assert readback.total_distance_m == pytest.approx(0.2937485791) + assert readback.final_downrange_m == pytest.approx(0.2937485791) + assert readback.final_offline_m == pytest.approx(0.0) + assert readback.bounce_count == 1 + assert readback.calibration_kind == "literature" + assert readback.calibration_id == "literature-default-2026-08" + assert readback.calibration_source == "documented literature basis" + assert readback.calibration_confidence == pytest.approx(0.6) + assert readback.observed_phases == ("impact", "skid", "roll") + assert len(readback.warnings) == 4 + assert readback.warnings[-1].code == "CENSORED_ENDPOINT" + assert readback.warnings[-1].severity == "warning" assert readback.executor_source_revision == "ground-regional-execution-v1" +def test_null_result_readback_does_not_fabricate_ground_metrics() -> None: + result = _result("cancelled") + + readback = regional_execution_readback(result, result.regional_plan) + + assert readback.status == "cancelled" + assert readback.failure_reason == "cancelled" + assert readback.ground_time_s is None + assert readback.completed is None + assert readback.carry_distance_m is None + assert readback.bounce_count is None + assert readback.calibration_kind is None + assert readback.calibration_id is None + assert readback.calibration_source is None + assert readback.calibration_confidence is None + assert readback.observed_phases == () + assert readback.warnings == () + + def test_readback_rejects_evidence_for_a_different_visible_plan() -> None: result = _result() different = replace(result.regional_plan, request_id="different-plan") @@ -104,12 +141,15 @@ def current_request(self): # type: ignore[no-untyped-def] box.open_button.click() - assert "partial" in box.readback_label.text() + assert "partial" in box.readback_label.toPlainText() + assert "bounce 0.040 m" in box.readback_label.toPlainText() + assert "final offline 0.000 m" in box.readback_label.toPlainText() + assert "CENSORED_ENDPOINT" in box.readback_label.toPlainText() assert "No physics executed" in box.status_label.text() - accepted = box.readback_label.text() + accepted = box.readback_label.toPlainText() target.write_text('{"request_id":"one","request_id":"two"}', encoding="utf-8") box.open_button.click() - assert box.readback_label.text() == accepted + assert box.readback_label.toPlainText() == accepted assert "Prior accepted evidence was preserved" in box.status_label.text() box.clear() - assert box.readback_label.text() == "No accepted evidence" + assert box.readback_label.toPlainText() == "No accepted evidence"