Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,17 @@ export default function App() {
},
},
);
} catch (rawError) {
// The monitor's onError handles errors surfaced through polling; this
// catch covers the awaited create*Attempt POST that runs before the
// monitor (backend down / 4xx). Mirror onError's shouldIgnoreRun guard so
// an ignored/cancelled run isn't re-surfaced here.
if (shouldIgnoreRun(currentRunIdRef.current)) return;
const err = rawError instanceof Error ? rawError : new Error(String(rawError));
if (!(err instanceof BackendClientError && err.code === 'USER_CANCELLED')) {
setError(err.message);
setErrorRetryable(err instanceof BackendClientError && err.details?.retryable === true);
}
} finally {
setIsAnalyzing(false);
abortControllerRef.current = null;
Expand Down Expand Up @@ -917,6 +928,17 @@ export default function App() {
},
},
);
} catch (rawError) {
// The monitor's onError handles errors surfaced through polling; this
// catch covers the awaited create*Attempt POST that runs before the
// monitor (backend down / 4xx). Mirror onError's shouldIgnoreRun guard so
// an ignored/cancelled run isn't re-surfaced here.
if (shouldIgnoreRun(currentRunIdRef.current)) return;
const err = rawError instanceof Error ? rawError : new Error(String(rawError));
if (!(err instanceof BackendClientError && err.code === 'USER_CANCELLED')) {
setError(err.message);
setErrorRetryable(err instanceof BackendClientError && err.details?.retryable === true);
}
} finally {
setIsAnalyzing(false);
abortControllerRef.current = null;
Expand Down
3 changes: 3 additions & 0 deletions apps/ui/src/components/AnalysisResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Phase2ConsistencyReport } from './Phase2ConsistencyReport';
import { isBrowserLoudnessConfigEnabled } from '../config';
import { BrowserLoudnessPanel } from './BrowserLoudnessPanel';
import { MeasurementDashboard } from './MeasurementDashboard';
import { ReconstructionContractPanel } from './ReconstructionContractPanel';
import { PatchSmithPanel } from './PatchSmithPanel';
import { SamplePlayback } from './SamplePlayback';
import { SessionMusicianPanel } from './SessionMusicianPanel';
Expand Down Expand Up @@ -1815,6 +1816,8 @@ export function AnalysisResults({
<SecretSauceSection secretSauce={phase2.secretSauce} isPhase2V2={isPhase2V2} />
)}

{isPhase2V2 && <ReconstructionContractPanel contract={phase2?.recommendations} />}

{/* Audit Finding #1: measurements section moved to the end of the scroll.
Wrapped in a single anchorable <section> so the StickyNav can target
it with one pill ("Measurements") instead of nine pills. The internal
Expand Down
84 changes: 84 additions & 0 deletions apps/ui/src/components/ReconstructionContractPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Reconstruction Contract — the verbatim recommendations.v1 envelope (ADR 0003).
*
* The recommendation cards pair each device with its contract entry inline
* (ContractValidatedBadge / ContractEntriesBlock). This panel is the
* complementary single machine-view: the flat, schema-validated, citation-gated
* set exactly as it is exported to the asa-ableton .als generator and the
* phase2-export envelope. Every row carries ≥1 cited measurement by
* construction. Renders nothing when the envelope is absent (older runs,
* stem_summary) or empty.
*/
import React, { useState } from 'react';

import type { RecommendationsContract, RecommendationContractEntry } from '../types';
import { formatContractValue, formatContractRange } from '../services/recommendationsContract';
import { CollapsibleCard, DataTable, type DataTableColumn } from './ui';

interface ReconstructionContractPanelProps {
contract?: RecommendationsContract | null;
className?: string;
}

const COLUMNS: DataTableColumn<RecommendationContractEntry>[] = [
{ key: 'device', label: 'Device' },
{ key: 'parameter', label: 'Parameter' },
{
key: 'value',
label: 'Value',
render: (entry) => (
<span className="font-mono tabular-nums text-text-primary">
{formatContractValue(entry)}
</span>
),
},
{
key: 'range',
label: 'Working range',
render: (entry) => {
const range = formatContractRange(entry);
return (
<span className="font-mono tabular-nums text-text-secondary">{range ?? '—'}</span>
);
},
},
{
key: 'cited_measurements',
label: 'Cited measurements',
render: (entry) => (
<div className="flex flex-col gap-0.5">
{entry.cited_measurements.map((path, index) => (
<span key={index} className="font-mono text-micro text-text-secondary">
{path}
</span>
))}
</div>
),
},
];

export function ReconstructionContractPanel({
contract,
className,
}: ReconstructionContractPanelProps) {
const [open, setOpen] = useState(false);
const entries = contract?.recommendations ?? [];
if (entries.length === 0) {
return null;
}

return (
<CollapsibleCard
open={open}
onToggle={() => setOpen((prev) => !prev)}
tone="success"
className={className}
eyebrow="recommendations.v1 · exported to .als"
title={`Reconstruction Contract · ${entries.length} validated recommendation${
entries.length === 1 ? '' : 's'
}`}
>
<DataTable data={entries} columns={COLUMNS} />
</CollapsibleCard>
);
}
Loading