Skip to content

Commit baa2810

Browse files
authored
Merge pull request #182 from LennartvdM/codex/add-elaborate-narrative-for-purity-diagnostics
Add purity narrative toggle and explanation
2 parents d6451ca + 9323189 commit baa2810

3 files changed

Lines changed: 230 additions & 3 deletions

File tree

web/app.js

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ const batchState = {
174174
purityPanelHeader: null,
175175
purityPanelList: null,
176176
latestPurityReport: null,
177+
purityNarrativeButton: null,
178+
purityNarrativeContainer: null,
179+
showPurityNarrative: false,
177180
};
178181

179182
const RANDOMIZE_SEED_STORAGE_KEY = 'cfg.calendar.randomizeSeed';
@@ -4115,6 +4118,17 @@ function updateBatchPurityIndicator(summaryOverride) {
41154118
}
41164119
}
41174120

4121+
function updatePurityNarrativeToggle(hasReport) {
4122+
const button = batchState.purityNarrativeButton;
4123+
if (!button) {
4124+
return;
4125+
}
4126+
const isExpanded = Boolean(batchState.showPurityNarrative && hasReport);
4127+
button.textContent = isExpanded ? 'Collapse' : 'Elaborate';
4128+
button.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
4129+
button.disabled = !hasReport;
4130+
}
4131+
41184132
function renderBatchPurityPanel(reportOverride) {
41194133
const panel = batchState.purityPanel;
41204134
if (!panel) {
@@ -4123,8 +4137,15 @@ function renderBatchPurityPanel(reportOverride) {
41234137

41244138
const report =
41254139
typeof reportOverride === 'undefined' ? batchState.latestPurityReport : reportOverride;
4140+
const hasReport = Boolean(report && report.human);
4141+
updatePurityNarrativeToggle(hasReport);
41264142

4127-
if (!report || !report.human) {
4143+
const narrativeContainer = batchState.purityNarrativeContainer;
4144+
if (!hasReport) {
4145+
if (narrativeContainer) {
4146+
narrativeContainer.hidden = true;
4147+
narrativeContainer.innerHTML = '';
4148+
}
41284149
panel.hidden = true;
41294150
return;
41304151
}
@@ -4164,9 +4185,145 @@ function renderBatchPurityPanel(reportOverride) {
41644185
}
41654186
}
41664187

4188+
if (narrativeContainer) {
4189+
const shouldShowNarrative = Boolean(batchState.showPurityNarrative && report);
4190+
if (!shouldShowNarrative) {
4191+
narrativeContainer.hidden = true;
4192+
narrativeContainer.innerHTML = '';
4193+
} else {
4194+
const paragraphs = buildPurityNarrative(report);
4195+
if (!paragraphs.length) {
4196+
narrativeContainer.hidden = true;
4197+
narrativeContainer.innerHTML = '';
4198+
} else {
4199+
narrativeContainer.hidden = false;
4200+
narrativeContainer.innerHTML = '';
4201+
const fragment = document.createDocumentFragment();
4202+
paragraphs.forEach((text) => {
4203+
if (typeof text !== 'string' || !text.trim()) {
4204+
return;
4205+
}
4206+
const paragraph = document.createElement('p');
4207+
paragraph.textContent = text;
4208+
fragment.append(paragraph);
4209+
});
4210+
narrativeContainer.append(fragment);
4211+
}
4212+
}
4213+
}
4214+
41674215
panel.hidden = false;
41684216
}
41694217

4218+
function formatPurityActivityLabel(label) {
4219+
if (typeof label !== 'string') {
4220+
return 'This activity';
4221+
}
4222+
const cleaned = label.trim().replace(/[_-]+/g, ' ');
4223+
if (!cleaned) {
4224+
return 'This activity';
4225+
}
4226+
return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
4227+
}
4228+
4229+
function buildPurityNarrative(report) {
4230+
if (!report) {
4231+
return [];
4232+
}
4233+
4234+
const paragraphs = [];
4235+
const totalRuns = Number.isFinite(report.batchSize) ? report.batchSize : Number(report.totalRuns) || 0;
4236+
const pureRuns = Number.isFinite(report.pureRuns) ? report.pureRuns : 0;
4237+
const impureRuns = Number.isFinite(report.impureRuns) ? report.impureRuns : Math.max(totalRuns - pureRuns, 0);
4238+
const totals = report.totals || {};
4239+
const avgRaw = Number.isFinite(totals.avgRaw) ? totals.avgRaw : 0;
4240+
const avgShare = Number.isFinite(totals.avgShare) ? totals.avgShare : 0;
4241+
const avgSequence = Number.isFinite(totals.avgSequence) ? totals.avgSequence : 0;
4242+
const shareDriftMinutes = Number.isFinite(totals.avgShareDriftMinutes)
4243+
? totals.avgShareDriftMinutes
4244+
: avgShare - avgRaw;
4245+
const shareDriftPercent = Number.isFinite(totals.avgShareDriftPercent)
4246+
? totals.avgShareDriftPercent
4247+
: avgRaw === 0
4248+
? 0
4249+
: shareDriftMinutes / avgRaw;
4250+
const driftMagnitudeText = formatPurityMinutes(Math.abs(shareDriftMinutes));
4251+
const driftPercentText = formatPurityPercent(shareDriftPercent);
4252+
const severity = Math.abs(shareDriftPercent) < 0.05
4253+
? 'very close'
4254+
: Math.abs(shareDriftPercent) < 0.15
4255+
? 'moderately off'
4256+
: 'significantly misaligned';
4257+
4258+
if (totalRuns <= 0) {
4259+
paragraphs.push('No batch runs have been analyzed yet, so the purity checker does not have a verdict.');
4260+
} else {
4261+
const statusWord = impureRuns > 0 ? 'failing' : 'passing';
4262+
let driftSentence = 'matching the raw totals almost exactly.';
4263+
if (shareDriftMinutes !== 0) {
4264+
const direction = shareDriftMinutes < 0 ? 'undercounting' : 'overstating';
4265+
const percentSuffix = driftPercentText === 'n/a' ? '' : ` (${driftPercentText})`;
4266+
driftSentence = `${direction} about ${driftMagnitudeText} minutes per run${percentSuffix}.`;
4267+
}
4268+
paragraphs.push(
4269+
`This batch of ${totalRuns} runs is ${statusWord} the purity check, with ${pureRuns} marked pure and ${impureRuns} flagged impure. On average, the Share view is ${severity} versus the raw schedules, ${driftSentence}`
4270+
);
4271+
}
4272+
4273+
if (avgRaw > 0 || avgShare > 0 || avgSequence > 0) {
4274+
const avgRawText = formatPurityMinutes(avgRaw);
4275+
const avgShareText = formatPurityMinutes(avgShare);
4276+
const avgSequenceText = formatPurityMinutes(avgSequence);
4277+
const gapText = formatPurityMinutes(Math.abs(avgShare - avgRaw));
4278+
paragraphs.push(
4279+
`Across the batch, raw and Sequence timelines stay aligned at roughly ${avgRawText} and ${avgSequenceText} minutes per run, while Share only captures ${avgShareText}. That shortfall of about ${gapText} minutes per run is what the diagnostics are highlighting.`
4280+
);
4281+
}
4282+
4283+
const activities = Array.isArray(report.activities) ? report.activities : [];
4284+
const rankedActivities = activities
4285+
.filter((activity) => {
4286+
const driftValue = Number(activity?.driftMinutes);
4287+
return Number.isFinite(driftValue) && Math.abs(driftValue) >= 1;
4288+
})
4289+
.sort((a, b) => Math.abs((b?.driftMinutes) || 0) - Math.abs((a?.driftMinutes) || 0))
4290+
.slice(0, 2);
4291+
if (rankedActivities.length) {
4292+
const activitySentences = rankedActivities.map((activity) => {
4293+
const driftMinutes = Number(activity?.driftMinutes) || 0;
4294+
const descriptor = driftMinutes < 0 ? 'undercounted' : 'overcounted';
4295+
const deltaWord = driftMinutes < 0 ? 'missing' : 'adding';
4296+
const driftText = formatPurityMinutes(Math.abs(driftMinutes));
4297+
const percentText = formatPurityPercent(activity?.driftPercent);
4298+
const percentSuffix = percentText === 'n/a' ? '' : ` (${percentText})`;
4299+
const rawText = formatPurityMinutes(activity?.avgRaw);
4300+
const shareText = formatPurityMinutes(activity?.avgShare);
4301+
const label = formatPurityActivityLabel(activity?.id);
4302+
return `${label} is ${descriptor}: raw schedules average about ${rawText} minutes per run while Share shows ${shareText}, ${deltaWord} roughly ${driftText} minutes${percentSuffix}.`;
4303+
});
4304+
paragraphs.push(`Distortion concentrates in a few activities. ${activitySentences.join(' ')}`);
4305+
}
4306+
4307+
const worstRuns = Array.isArray(report.worstRuns) ? report.worstRuns : [];
4308+
const notableRuns = worstRuns
4309+
.filter((run) => Number.isFinite(run?.driftMinutes))
4310+
.slice(0, 3);
4311+
if (notableRuns.length) {
4312+
const runText = notableRuns
4313+
.map((run) => {
4314+
const label = Number.isFinite(run?.runIndex) && run.runIndex > 0 ? `#${run.runIndex}` : 'one run';
4315+
const magnitude = formatPurityMinutes(Math.abs(Number(run?.driftMinutes) || 0));
4316+
return `${label} (~${magnitude} min)`;
4317+
})
4318+
.join(', ');
4319+
paragraphs.push(
4320+
`The discrepancy is not uniform: runs ${runText} show the steepest gaps, so their Share visualisations should be treated as sketches rather than precise ledgers.`
4321+
);
4322+
}
4323+
4324+
return paragraphs.slice(0, 4);
4325+
}
4326+
41704327
function logBatchPurityReport(summary) {
41714328
if (!summary) {
41724329
return;
@@ -4879,6 +5036,18 @@ function hydrateBatchPanel() {
48795036
batchState.purityPanelStatus = panel.querySelector('[data-batch-purity-status]');
48805037
batchState.purityPanelHeader = panel.querySelector('[data-batch-purity-header]');
48815038
batchState.purityPanelList = panel.querySelector('[data-batch-purity-list]');
5039+
batchState.purityNarrativeButton = panel.querySelector('[data-purity-narrative-toggle]');
5040+
batchState.purityNarrativeContainer = panel.querySelector('[data-batch-purity-narrative]');
5041+
if (batchState.purityNarrativeButton instanceof HTMLButtonElement) {
5042+
batchState.purityNarrativeButton.type = 'button';
5043+
batchState.purityNarrativeButton.addEventListener('click', () => {
5044+
if (!batchState.latestPurityReport) {
5045+
return;
5046+
}
5047+
batchState.showPurityNarrative = !batchState.showPurityNarrative;
5048+
renderBatchPurityPanel(batchState.latestPurityReport);
5049+
});
5050+
}
48825051

48835052
batchState.sizeButtons = new Map();
48845053
const sizeButtons = panel.querySelectorAll('[data-batch-size]');

web/index.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,23 @@ <h3 class="batch-results__title">Batch results</h3>
223223
</div>
224224
<section class="batch-purity-panel" data-batch-purity-panel hidden>
225225
<div class="batch-purity-header">
226-
<h3 class="batch-purity-title">Purity diagnostics</h3>
227-
<span class="batch-purity-status" data-batch-purity-status>Purity status</span>
226+
<div class="batch-purity-heading">
227+
<h3 class="batch-purity-title">Purity diagnostics</h3>
228+
<span class="batch-purity-status" data-batch-purity-status>Purity status</span>
229+
</div>
230+
<button
231+
type="button"
232+
class="mini-button batch-purity-toggle"
233+
data-purity-narrative-toggle
234+
aria-expanded="false"
235+
disabled
236+
>
237+
Elaborate
238+
</button>
228239
</div>
229240
<p class="batch-purity-subheader" data-batch-purity-header></p>
230241
<ul class="batch-purity-list" data-batch-purity-list></ul>
242+
<div class="batch-purity-narrative" data-batch-purity-narrative hidden></div>
231243
</section>
232244
<div class="batch-results__legend" data-batch-legend>
233245
<span class="batch-results__legend-title">Activities</span>

web/style.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,14 @@ body {
796796
flex-wrap: wrap;
797797
}
798798

799+
.batch-purity-heading {
800+
display: inline-flex;
801+
align-items: center;
802+
gap: 8px;
803+
flex: 1;
804+
min-width: 0;
805+
}
806+
799807
.batch-purity-title {
800808
margin: 0;
801809
font-size: 14px;
@@ -813,6 +821,28 @@ body {
813821
color: rgba(248, 250, 252, 0.9);
814822
}
815823

824+
.batch-purity-toggle {
825+
text-transform: uppercase;
826+
font-size: 11px;
827+
letter-spacing: 0.05em;
828+
padding: 4px 10px;
829+
border-color: rgba(148, 163, 184, 0.35);
830+
background: rgba(15, 23, 42, 0.6);
831+
color: #e2e8f0;
832+
}
833+
834+
.batch-purity-toggle:hover:not(:disabled) {
835+
background: rgba(71, 85, 105, 0.6);
836+
color: #ffffff;
837+
}
838+
839+
.batch-purity-toggle:disabled {
840+
opacity: 0.55;
841+
cursor: not-allowed;
842+
background: rgba(15, 23, 42, 0.45);
843+
color: rgba(226, 232, 240, 0.6);
844+
}
845+
816846
.batch-purity-subheader {
817847
margin: 0;
818848
font-size: 13px;
@@ -829,6 +859,22 @@ body {
829859
color: rgba(203, 213, 225, 0.85);
830860
}
831861

862+
.batch-purity-narrative {
863+
margin: 0;
864+
padding-top: 8px;
865+
border-top: 1px solid rgba(148, 163, 184, 0.2);
866+
display: flex;
867+
flex-direction: column;
868+
gap: 8px;
869+
font-size: 12px;
870+
line-height: 1.5;
871+
color: rgba(203, 213, 225, 0.75);
872+
}
873+
874+
.batch-purity-narrative p {
875+
margin: 0;
876+
}
877+
832878
.batch-controls__heading-row {
833879
display: flex;
834880
align-items: center;

0 commit comments

Comments
 (0)