Skip to content
Merged
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
114 changes: 61 additions & 53 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3741,12 +3741,57 @@ function formatPurityPercent(value) {

function buildPurityObservations(summary) {
const lines = [];
const analysis = summary?.analysis;
const totalRuns = Number.isFinite(summary?.totalRuns) ? summary.totalRuns : 0;
if (!analysis || !totalRuns) {
const impureRuns = Number.isFinite(summary?.impureRuns) ? summary.impureRuns : 0;
const runs = Array.isArray(summary?.runs) ? summary.runs : [];
const checkerErrorRuns = runs.filter((run) => run && run.didError).length;
const hasCheckerError = Boolean(summary?.didCheckerError) || checkerErrorRuns > 0;
const statusLabel = hasCheckerError ? 'ERROR' : summary?.hasAnyError ? 'FAILED' : 'OK';
const detailParts = [];
if (totalRuns <= 0) {
detailParts.push('No runs analyzed.');
} else {
if (impureRuns > 0) {
detailParts.push(`${impureRuns}/${totalRuns} runs show drift between raw and share totals.`);
}
if (checkerErrorRuns > 0) {
detailParts.push(`${checkerErrorRuns}/${totalRuns} runs had checker errors.`);
}
if (detailParts.length === 0) {
detailParts.push(`All ${totalRuns} runs match raw and share totals.`);
Comment on lines +3757 to +3761
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mention checker crashes when run details are missing

If the purity checker crashes before generating per‑run results (e.g. the fallback path in computeBatchPuritySummary sets totalRuns but no runs and didCheckerError = true), this logic only looks at checkerErrorRuns derived from summary.runs. With an empty array the count is zero, so the summary logs All ${totalRuns} runs match raw and share totals. even though statusLabel is ERROR, which misleads operators and hides the crash. The detail text should also consider summary.didCheckerError so that an error is reported instead of claiming all runs matched.

Useful? React with 👍 / 👎.

}
}
lines.push(`Overall: Purity status: ${statusLabel}. ${detailParts.join(' ')}`);

const analysis = summary?.analysis;
if (!analysis || totalRuns <= 0) {
return lines;
}

const avgRaw = formatPurityMinutes(analysis.avgRawTotal);
const avgShare = formatPurityMinutes(analysis.avgShareTotal);
const avgSequence = formatPurityMinutes(analysis.avgSequenceTotal);
lines.push(
`Overall: Average total minutes per run: raw=${avgRaw}, share=${avgShare}, sequence=${avgSequence}.`
);

const shareDiff = (Number(analysis.avgShareTotal) || 0) - (Number(analysis.avgRawTotal) || 0);
const percentDiff = analysis.avgRawTotal === 0 ? null : shareDiff / analysis.avgRawTotal;
const driftMagnitudeText = formatPurityMinutes(Math.abs(shareDiff));
const percentText = formatPurityPercent(percentDiff);
const percentSuffix = percentText === 'n/a' ? '' : ` (≈ ${percentText})`;
if (shareDiff < 0) {
lines.push(
`Overall: On average, share is missing ${driftMagnitudeText} minutes per run compared to raw${percentSuffix}.`
);
} else if (shareDiff > 0) {
lines.push(
`Overall: On average, share is over-reporting ${driftMagnitudeText} minutes per run compared to raw${percentSuffix}.`
);
} else {
lines.push('Overall: On average, share matches raw totals per run.');
}

const activityThresholdMinutes = 60;
const activityThresholdPercent = 0.1;
const activityAnalyses = Array.isArray(analysis.activities) ? analysis.activities.slice() : [];
Expand All @@ -3769,13 +3814,13 @@ function buildPurityObservations(summary) {

significantActivities.forEach((activity) => {
const label = activity.label || activity.key || 'activity';
const avgRawText = formatPurityMinutes(activity.avgRawPerRun);
const avgShareText = formatPurityMinutes(activity.avgSharePerRun);
const driftMinutesText = formatPurityMinutes(activity.avgShareDriftMinutes || 0);
const driftMinutes = Number(activity.avgShareDriftMinutes) || 0;
const driftMagnitudeText = formatPurityMinutes(Math.abs(driftMinutes));
const percentText = formatPurityPercent(activity.avgShareDriftPercent);
const percentSuffix = percentText === 'n/a' ? '' : ` (≈ ${percentText})`;
const percentSuffix = percentText === 'n/a' ? '' : ` (≈ ${percentText} vs raw)`;
const descriptor = driftMinutes < 0 ? 'undercounted' : 'overcounted';
lines.push(
`ACTIVITY | ${label} | avg raw/share per run: ${avgRawText} / ${avgShareText} | drift: ${driftMinutesText} min${percentSuffix}`
`Activity '${label}' is ${descriptor} in share by an average of ${driftMagnitudeText} minutes per run${percentSuffix}.`
);
});

Expand All @@ -3800,52 +3845,13 @@ function buildPurityObservations(summary) {
}
const minMagnitudeText = formatPurityMinutes(minMagnitude);
lines.push(
`RUNS | worst total drift: ${runLabels.join(', ')} (share ${descriptor} ≥ ${minMagnitudeText} min each)`
`Runs: Largest total drift in runs ${runLabels.join(', ')} (share ${descriptor} ≥ ${minMagnitudeText} minutes each).`
);
}

return lines;
}

function buildPuritySummaryLines(summary, observations) {
const lines = [];
const totalRuns = Number.isFinite(summary?.totalRuns) ? summary.totalRuns : 0;
const pureRuns = Number.isFinite(summary?.pureRuns) ? summary.pureRuns : 0;
const impureRuns = Number.isFinite(summary?.impureRuns) ? summary.impureRuns : 0;
const runs = Array.isArray(summary?.runs) ? summary.runs : [];
const checkerErrorRuns = runs.filter((run) => run && run.didError).length;
const hasCheckerError = Boolean(summary?.didCheckerError);
const errorCount = hasCheckerError ? Math.max(1, checkerErrorRuns) : checkerErrorRuns;
const statusLabel = hasCheckerError ? 'ERROR' : summary?.hasAnyError ? 'FAILED' : 'OK';
const statusParts = [`runs: ${totalRuns} total, ${pureRuns} pure, ${impureRuns} impure`];
if (errorCount > 0) {
statusParts.push(`checker errors: ${errorCount}`);
}
lines.push(`STATUS | ${statusLabel} | ${statusParts.join(' | ')}`);

const analysis = summary?.analysis;
if (analysis && totalRuns > 0) {
const avgRaw = formatPurityMinutes(analysis.avgRawTotal);
const avgShare = formatPurityMinutes(analysis.avgShareTotal);
const avgSequence = formatPurityMinutes(analysis.avgSequenceTotal);
lines.push(`TOTALS | avg raw/share/sequence per run: ${avgRaw} / ${avgShare} / ${avgSequence}`);

const shareDiff = (Number(analysis.avgShareTotal) || 0) - (Number(analysis.avgRawTotal) || 0);
const percentDiff = analysis.avgRawTotal === 0 ? null : shareDiff / analysis.avgRawTotal;
const driftMinutesText = formatPurityMinutes(shareDiff);
const percentText = formatPurityPercent(percentDiff);
const percentSuffix = percentText === 'n/a' ? '' : ` (≈ ${percentText})`;
lines.push(`TOTALS | avg drift (share vs raw): ${driftMinutesText} min per run${percentSuffix}`);
}

if (Array.isArray(observations) && observations.length > 0) {
lines.push(...observations);
}

lines.push('NOTE | Full raw purity data continues below…');
return lines;
}

function cancelBatchFitMeasurement() {
if (
batchState.pendingFitFrame &&
Expand Down Expand Up @@ -4112,18 +4118,20 @@ function logBatchPurityReport(summary) {
const pureRuns = Number.isFinite(summary.pureRuns) ? summary.pureRuns : 0;
const impureRuns = Number.isFinite(summary.impureRuns) ? summary.impureRuns : 0;
const runs = Array.isArray(summary.runs) ? summary.runs : [];
const errorRuns = runs.filter((run) => run && run.didError).length;
const logLevel = summary.hasAnyError ? 'warn' : 'info';

appendLogEntry({
level: 'warn',
message: `[Purity] Batch analysis: ${totalRuns} runs, pure=${pureRuns}, impure=${impureRuns}`,
});

try {
const observationLines = buildPurityObservations(summary);
const summaryLines = buildPuritySummaryLines(summary, observationLines);
summaryLines.forEach((line) => {
appendLogEntry({ level: logLevel, message: `[Purity][Summary] ${line}` });
observationLines.forEach((line) => {
appendLogEntry({ level: 'warn', message: `[Purity] ${line}` });
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
appendLogEntry({ level: 'warn', message: `[Purity][Summary] ERROR building summary: ${message}` });
appendLogEntry({ level: 'warn', message: `[Purity] ERROR building summary: ${message}` });
}

try {
Expand Down