wowbagger 0.5.0-beta.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  import { markdownBrowserSource } from './report-markdown.js';
2
2
  import { READINESS_REASON_LABELS } from './report.js';
3
+ import { reportFieldValues } from './report-view.js';
4
+ import { reportSelectionBrowserSource } from './report-selection.js';
5
+ import { reportEvidenceBrowserSource } from './report-evidence.js';
3
6
  import {
4
7
  buildGraphModel,
5
8
  graphClientSource,
@@ -13,6 +16,7 @@ import {
13
16
  forecastChart,
14
17
  throughputChart,
15
18
  weeklyFlowChart,
19
+ reportSvgBrowserSource,
16
20
  } from './report-svg.js';
17
21
 
18
22
  const READINESS_LABELS = {
@@ -49,7 +53,13 @@ function renderBadges(item) {
49
53
  badges.push(`<span class="badge">Priority ${escapeHtml(item.priority)}</span>`);
50
54
  }
51
55
  for (const [name, value] of Object.entries(item.fields)) {
52
- badges.push(`<span class="badge mapped">${escapeHtml(fieldLabel(name))}: ${escapeHtml(value)}</span>`);
56
+ if (Array.isArray(value)) {
57
+ for (const entry of value) {
58
+ badges.push(`<span class="badge mapped">${escapeHtml(fieldLabel(name))}: ${escapeHtml(entry)}</span>`);
59
+ }
60
+ } else {
61
+ badges.push(`<span class="badge mapped">${escapeHtml(fieldLabel(name))}: ${escapeHtml(value)}</span>`);
62
+ }
53
63
  }
54
64
  return badges.join('');
55
65
  }
@@ -75,7 +85,7 @@ function handleFor(id, itemNumbers) {
75
85
  return number === null ? id : `#${number}`;
76
86
  }
77
87
 
78
- function renderRelations(item, itemNumbers) {
88
+ function renderRelations(item, itemNumbers, retainedIds) {
79
89
  const rows = [
80
90
  ['Parent', item.parent === null ? [] : [item.parent]],
81
91
  ['Depends on', item.dependsOn],
@@ -84,7 +94,30 @@ function renderRelations(item, itemNumbers) {
84
94
  if (rows.length === 0) {
85
95
  return '<span class="muted">No relations.</span>';
86
96
  }
87
- return `<dl class="relations">${rows.map(([label, values]) => `<dt>${label}</dt><dd>${values.map((value) => escapeHtml(handleFor(value, itemNumbers))).join(', ')}</dd>`).join('')}</dl>`;
97
+ return `<dl class="relations">${rows.map(([label, values]) => `<dt>${label}</dt><dd>${values.map((value) => relationCell(value, itemNumbers, retainedIds)).join(', ')}</dd>`).join('')}</dl>`;
98
+ }
99
+
100
+ // A relation inside the artifact is a way into its canonical detail. A value
101
+ // outside the artifact names what the complete ledger knows and states
102
+ // explicitly that this report does not include it.
103
+ function relationCell(id, itemNumbers, retainedIds) {
104
+ const handle = handleFor(id, itemNumbers);
105
+ if (retainedIds !== undefined && !retainedIds.has(id)) {
106
+ return `${escapeHtml(handle)} (not included in this report)`;
107
+ }
108
+ if (retainedIds === undefined && !Object.prototype.hasOwnProperty.call(itemNumbers, id)) {
109
+ return `${escapeHtml(handle)} (not included in this report)`;
110
+ }
111
+ if (retainedIds !== undefined) {
112
+ const anchor = anchorForId(id, itemNumbers);
113
+ return `<a href="#${escapeHtml(anchor)}" data-reveal="${escapeHtml(anchor)}" data-inspect="${escapeHtml(id)}">${escapeHtml(handle)}</a>`;
114
+ }
115
+ return escapeHtml(handle);
116
+ }
117
+
118
+ function anchorForId(id, itemNumbers) {
119
+ const number = Object.prototype.hasOwnProperty.call(itemNumbers, id) ? itemNumbers[id] : null;
120
+ return `item-${number === null ? id : number}`;
88
121
  }
89
122
 
90
123
  function renderDecisions(item) {
@@ -121,18 +154,36 @@ function itemAnchor(item) {
121
154
  return escapeHtml(`item-${item.number === null ? item.id : item.number}`);
122
155
  }
123
156
 
124
- function renderCard(item, index, itemNumbers) {
157
+ function renderImpact(item, impactById, itemNumbers) {
158
+ const impact = impactById?.[item.id] ?? { downstreamIds: [], readyIfDoneIds: [] };
159
+ const downstreamIds = Array.isArray(impact.downstreamIds) ? impact.downstreamIds : [];
160
+ const readyIds = Array.isArray(impact.readyIfDoneIds) ? impact.readyIfDoneIds : [];
161
+ const downstreamNames = downstreamIds.map((id) => escapeHtml(handleFor(id, itemNumbers))).join(', ');
162
+ const readyNames = readyIds.map((id) => escapeHtml(handleFor(id, itemNumbers))).join(', ');
163
+ const downstreamAction = downstreamIds.length === 0 ? '' : ` <button type="button" data-show-items="${escapeHtml(downstreamIds.join(','))}" data-label="${escapeHtml(`Downstream of ${handleFor(item.id, itemNumbers)}`)}">Show downstream</button>`;
164
+ const readyAction = readyIds.length === 0 ? '' : ` <button type="button" data-show-items="${escapeHtml(readyIds.join(','))}" data-label="${escapeHtml(`Ready if ${handleFor(item.id, itemNumbers)} is done`)}">Show ready if done</button>`;
165
+ return `<section class="standard-only"><h3>Dependency impact</h3><p>Downstream reach: ${downstreamIds.length} item${downstreamIds.length === 1 ? '' : 's'}${downstreamIds.length === 0 ? '.' : ` (${downstreamNames}).`}${downstreamAction}</p><p>Ready if done: ${readyIds.length} item${readyIds.length === 1 ? '' : 's'}${readyIds.length === 0 ? '.' : ` (${readyNames}).`}${readyAction}</p></section>`;
166
+ }
167
+
168
+ function renderCard(item, index, itemNumbers, retainedIds, impactById = {}) {
125
169
  const handle = item.number === null ? item.id : `#${item.number}`;
126
- const searchText = [handle, item.id, item.title, item.kind, item.status, ...Object.values(item.fields)]
127
- .join(' ')
128
- .toLocaleLowerCase('en-US');
170
+ // Shared search contract: number, immutable ID, title, and normalized mapped
171
+ // values only. Lifecycle status, readiness, and kind filter; they do not
172
+ // search.
173
+ const searchText = [
174
+ item.number === null ? '' : String(item.number),
175
+ item.id,
176
+ item.title,
177
+ ...Object.values(item.fields).flatMap((value) => (Array.isArray(value) ? value : [value])),
178
+ ].filter((value) => value !== null && value !== undefined).join(' ').toLocaleLowerCase('en-US');
129
179
  const latestDecision = item.decisions.at(-1);
130
180
  const summaryPreview = bodyExcerpt(item.body, 120);
131
- return `<details class="card" id="${itemAnchor(item)}" data-item data-order="${index}" data-state="${escapeHtml(item.readiness.state)}" data-status="${escapeHtml(item.status)}" data-kind="${escapeHtml(item.kind)}" data-priority="${item.priority ?? ''}" data-created="${escapeHtml(item.created)}" data-title="${escapeHtml(item.title.toLocaleLowerCase('en-US'))}" data-fields="${escapeHtml(JSON.stringify(item.fields))}" data-search="${escapeHtml(searchText)}">
181
+ return `<details class="card" id="${itemAnchor(item)}" data-item data-item-id="${escapeHtml(item.id)}" data-order="${index}" data-state="${escapeHtml(item.readiness.state)}" data-status="${escapeHtml(item.status)}" data-kind="${escapeHtml(item.kind)}" data-priority="${item.priority ?? ''}" data-created="${escapeHtml(item.created)}" data-title="${escapeHtml(item.title.toLocaleLowerCase('en-US'))}" data-fields="${escapeHtml(JSON.stringify(item.fields))}" data-search="${escapeHtml(searchText)}">
132
182
  <summary><span class="summary-main"><span class="handle">${escapeHtml(handle)}</span><span class="title">${escapeHtml(item.title)}</span></span><span class="badges">${renderBadges(item)}</span><span class="summary-context standard-only">${summaryContext(item)}</span>${summaryPreview === '' ? '' : `<span class="summary-preview detailed-only">${escapeHtml(summaryPreview)}</span>`}</summary>
133
183
  <div class="card-detail">
184
+ <p class="outside-notice" hidden>Outside current filters. This detail stays open while the list shows the current scope.</p>
134
185
  <div class="basic-grid"><section><h3>Readiness</h3>${renderReasons(item, itemNumbers)}</section><section><h3>Dates</h3><p>Created ${escapeHtml(item.created)}<br>Updated ${escapeHtml(item.updated)}</p></section></div>
135
- <section class="standard-only"><h3>Relations</h3>${renderRelations(item, itemNumbers)}</section>
186
+ <section class="standard-only"><h3>Relations</h3>${renderRelations(item, itemNumbers, retainedIds)}</section>${renderImpact(item, impactById, itemNumbers)}
136
187
  <section class="standard-only"><h3>Latest decision</h3>${latestDecision === undefined ? '<span class="muted">No decisions.</span>' : renderDecisions({ decisions: [latestDecision] })}</section>
137
188
  <section class="body-excerpt standard-only"><h3>Body excerpt</h3><p>${escapeHtml(bodyExcerpt(item.body))}</p></section>
138
189
  <section class="detailed-only"><h3>Decision history</h3>${renderDecisions(item)}</section>
@@ -141,65 +192,6 @@ function renderCard(item, index, itemNumbers) {
141
192
  </details>`;
142
193
  }
143
194
 
144
- function numberHandle(item) {
145
- return item.number === null ? item.id : `#${item.number}`;
146
- }
147
-
148
- // An item named above the drill-down is a way into it. The whole row is the
149
- // link, so the pointer target is the reading target, and the href alone
150
- // reaches the canonical card when scripting is off.
151
- //
152
- // A free-text aria-label would replace every node inside the anchor, costing
153
- // the reader the reasons and facts the row exists to state, so the row names
154
- // itself with its own handle and title and describes itself with its own detail
155
- // line. The collection scope keeps the ids unique where one item is on two
156
- // lists.
157
- function rowLink(entry, scope, content) {
158
- const anchor = itemAnchor(entry);
159
- const nameId = `row-${scope}-${anchor}-name`;
160
- const detailId = `row-${scope}-${anchor}-detail`;
161
- return `<a class="row-link" href="#${anchor}" data-reveal="${anchor}" aria-labelledby="${nameId}" aria-describedby="${detailId}">${content(nameId, detailId)}</a>`;
162
- }
163
-
164
- // The ranked list is the report. Each entry states the factors that placed it,
165
- // in the order the comparison steps ran, so the reader can disagree with a
166
- // position by pointing at a reason instead of at a score.
167
- function renderWorkNext(entries, unknownClasses) {
168
- const notice = unknownClasses.length === 0
169
- ? ''
170
- : `<p class="notice">Unrecognised class values, ranked as standard: ${unknownClasses.map((entry) => `${escapeHtml(entry.value)} (${entry.numbers.map((number) => `#${number}`).join(', ')})`).join('; ')}</p>`;
171
- const body = entries.length === 0
172
- ? '<p class="muted">No ready items.</p>'
173
- : `<ol class="ranked">${entries.map((entry) => `<li>${rowLink(entry, 'work-next', (nameId, detailId) => `<span class="summary-main" id="${nameId}"><span class="handle">${escapeHtml(numberHandle(entry))}</span><span class="title">${escapeHtml(entry.title)}</span></span><p class="why" id="${detailId}">${entry.reasons.map((reason) => `<span class="reason reason-${escapeHtml(reason.code)}">${escapeHtml(reason.label)}</span>`).join('')}</p>`)}</li>`).join('')}</ol>`;
174
- return `<section id="work-next" class="panel"><div class="section-heading"><div><p class="eyebrow">Recommended order</p><h2>Work next</h2></div><p class="muted">Ready items only. Derived at render time; the deterministic <code>ready</code> queue is unchanged.</p></div>${notice}${body}</section>`;
175
- }
176
-
177
- // Every attention row is the same shape: the handle and title as the row's
178
- // name, one muted line of the facts that put the item on this list, and the
179
- // whole row as the way into the item.
180
- function attentionList(entries, scope, emptyMessage, detail) {
181
- if (entries.length === 0) {
182
- return `<p class="muted">${emptyMessage}</p>`;
183
- }
184
- return `<ul class="plain">${entries.map((entry) => `<li>${rowLink(entry, scope, (nameId, detailId) => `<span class="row-name" id="${nameId}"><span class="handle">${escapeHtml(numberHandle(entry))}</span> ${escapeHtml(entry.title)}</span><br><span class="muted" id="${detailId}">${detail(entry)}</span>`)}</li>`).join('')}</ul>`;
185
- }
186
-
187
- function renderAttention(attention, itemNumbers) {
188
- const blocked = attentionList(attention.blocked, 'blocked', 'Nothing blocked.', (entry) => `blocked by ${entry.blockers.map((blocker) => `${escapeHtml(handleFor(blocker.id, itemNumbers))}${blocker.status === null ? '' : ` (${escapeHtml(blocker.status)})`}`).join(', ')} · age ${entry.ageDays}d`);
189
- const aging = attentionList(attention.aging, 'aging', 'No open items.', (entry) => `age ${entry.ageDays}d · ${escapeHtml(entry.status)} · ${escapeHtml(entry.state)}`);
190
- const stuck = attentionList(attention.stuck, 'stuck', 'Nothing started is past the historical 85th percentile.', (entry) => `${entry.elapsedDays}d since accept ${escapeHtml(entry.startedOn)} · p85 ${entry.thresholdDays}d`);
191
-
192
- return `<section id="attention" class="panel"><div class="section-heading"><div><p class="eyebrow">Needs a decision</p><h2>Attention</h2></div><p class="muted">Blocked work, the oldest open work, and started work past this ledger's own 85th-percentile cycle time.</p></div><div class="attention-grid"><section><h3>Blocked</h3>${blocked}${renderTruncation(attention.blocked, attention.blockedTotal)}</section><section><h3>Aging</h3>${aging}${renderTruncation(attention.aging, attention.agingTotal)}</section><section><h3>Past p85</h3>${stuck}${renderTruncation(attention.stuck, attention.stuckTotal)}</section></div></section>`;
193
- }
194
-
195
- // A shortened list has to say so. Silent truncation reads as "that is all of
196
- // it", which is the one thing an attention list must never imply.
197
- function renderTruncation(shown, total) {
198
- return shown.length >= total
199
- ? ''
200
- : `<p class="muted truncation">Showing ${shown.length} of ${total}. The rest are in the drill-down.</p>`;
201
- }
202
-
203
195
  // A chart row is only worth a heading when there is a chart in it. Every row
204
196
  // that has a table keeps the table whether or not the chart drew, because the
205
197
  // table is what a reader without SVG is left with.
@@ -212,16 +204,24 @@ function chartRow(heading, chart, caption = '', fallback = '') {
212
204
  : `<figure class="chart-figure">${chart}${caption === '' ? '' : `<figcaption>${caption}</figcaption>`}</figure>`;
213
205
  return `<section class="chart-row"><h3>${heading}</h3>${figure}${fallback}</section>`;
214
206
  }
207
+ // The range controls open on the window the fixed snapshot already shows: the
208
+ // first week the evidence layer measured through the report date.
209
+ function flowDefaultFrom(model) {
210
+ const first = model.evidence && Array.isArray(model.evidence.weeks) && model.evidence.weeks.length > 0
211
+ ? model.evidence.weeks[0].weekStart
212
+ : null;
213
+ return typeof first === 'string' && first !== '' ? first : model.asOf;
214
+ }
215
215
 
216
216
  function renderEvidence(evidence, asOf) {
217
217
  const buckets = `<table><thead><tr><th>Age</th><th>Open items</th></tr></thead><tbody>${evidence.agingBuckets.map((bucket) => `<tr><td>${escapeHtml(bucket.label)}</td><td>${bucket.count}</td></tr>`).join('')}</tbody></table>`;
218
- const flow = `<div class="table-scroll"><table><thead><tr><th>Week</th><th>Arrivals</th><th>Completions</th><th>Net</th><th>4-week mean</th></tr></thead><tbody>${evidence.weeks.map((week) => `<tr><td>${escapeHtml(week.weekStart)}</td><td>${week.arrivals}</td><td>${week.completions}</td><td>${week.arrivals - week.completions > 0 ? '+' : ''}${week.arrivals - week.completions}</td><td>${week.rolling === null ? '—' : week.rolling}</td></tr>`).join('')}</tbody></table></div>`;
218
+ const flow = `<div class="table-scroll"><table><thead><tr><th>Week</th><th>Arrivals</th><th>Closures</th><th>Done</th><th>Net</th><th>4-week mean</th></tr></thead><tbody>${evidence.weeks.map((week) => `<tr><td>${escapeHtml(week.weekStart)}${week.partial === true ? ' (partial)' : ''}</td><td>${week.arrivals}</td><td>${week.closures}</td><td>${week.done ?? 0}</td><td>${week.arrivals - week.closures > 0 ? '+' : ''}${week.arrivals - week.closures}</td><td>${week.rolling === null ? '—' : week.rolling}</td></tr>`).join('')}</tbody></table></div>`;
219
219
  const cycle = evidence.cycleTime.sampleCount === 0
220
220
  ? '<p class="muted">No accept-to-complete history yet.</p>'
221
- : `<p>Median <strong>${evidence.cycleTime.medianDays}d</strong> · p85 <strong>${evidence.cycleTime.p85Days}d</strong><br><span class="muted">${evidence.cycleTime.sampleCount} completed items, accept to complete.</span></p>`;
221
+ : `<p>Median <strong>${evidence.cycleTime.medianDays}d</strong> · p85 <strong>${evidence.cycleTime.p85Days}d</strong><br><span class="muted">${evidence.cycleTime.sampleCount} done items with recorded acceptance, accept to completion.</span></p>`;
222
222
  const forecast = evidence.forecast === null
223
- ? '<p class="muted">No completions in the window, so no forecast.</p>'
224
- : `<p><strong>${evidence.forecast.remaining}</strong> open items remaining.<br>50% by <strong>${escapeHtml(evidence.forecast.date50)}</strong> (${evidence.forecast.weeks50} weeks)<br>85% by <strong>${escapeHtml(evidence.forecast.date85)}</strong> (${evidence.forecast.weeks85} weeks)<br>95% by <strong>${escapeHtml(evidence.forecast.date95)}</strong> (${evidence.forecast.weeks95} weeks)<br><span class="muted">${evidence.forecast.trials} Monte Carlo trials resampling observed weekly throughput.</span></p>`;
223
+ ? '<p class="muted">No closures in the window, so no forecast.</p>'
224
+ : `<p><strong>${evidence.forecast.remaining}</strong> open items remaining.<br>50% by <strong>${escapeHtml(evidence.forecast.date50)}</strong> (${evidence.forecast.weeks50} weeks)<br>85% by <strong>${escapeHtml(evidence.forecast.date85)}</strong> (${evidence.forecast.weeks85} weeks)<br>95% by <strong>${escapeHtml(evidence.forecast.date95)}</strong> (${evidence.forecast.weeks95} weeks)<br><span class="muted">${evidence.forecast.trials} Monte Carlo trials resampling observed weekly closures. A closure-based estimate, not a delivery commitment.</span></p>`;
225
225
 
226
226
  const rows = [
227
227
  chartRow(
@@ -230,15 +230,15 @@ function renderEvidence(evidence, asOf) {
230
230
  '<span class="key key-50">50%</span><span class="key key-85">85%</span><span class="key key-95">95%</span> of Monte Carlo trials.',
231
231
  ),
232
232
  chartRow(
233
- 'Arrivals against completions',
233
+ 'Arrivals against closures',
234
234
  weeklyFlowChart(evidence.weeks),
235
- '<span class="key key-arrival">Arrivals</span><span class="key key-completion">Completions</span>',
235
+ '<span class="key key-arrival">Arrivals</span><span class="key key-closure">Closures</span>',
236
236
  flow,
237
237
  ),
238
238
  chartRow(
239
239
  'Throughput and its four-week mean',
240
240
  throughputChart(evidence.weeks),
241
- '<span class="key key-completion">Completions</span><span class="key key-rolling">4-week mean</span>',
241
+ '<span class="key key-closure">Closures</span><span class="key key-rolling">4-week mean</span>',
242
242
  ),
243
243
  chartRow(
244
244
  'Cumulative flow',
@@ -253,50 +253,132 @@ function renderEvidence(evidence, asOf) {
253
253
  chartRow('Aging by age and status', agingHeatmapChart(evidence.agingMatrix), '', buckets),
254
254
  ].join('');
255
255
 
256
- return `<section id="evidence" class="panel"><div class="section-heading"><div><p class="eyebrow">Evidence</p><h2>Flow and forecast</h2></div><p class="muted">Reconstructed from item dates in this ledger. No stored snapshots.</p></div><div class="evidence-grid"><section><h3>Throughput</h3><p><strong>${evidence.throughput.perWeek}</strong> items/week<br><span class="muted">${evidence.throughput.total} completions over ${evidence.throughput.windowWeeks} weeks.</span></p></section><section><h3>Cycle time</h3>${cycle}</section><section><h3>Forecast</h3>${forecast}</section></div>${rows}</section>`;
256
+ return `<section id="evidence" class="panel"><div class="section-heading"><div><p class="eyebrow">Evidence</p><h2>Flow and forecast</h2></div><p class="muted">Reconstructed from item dates in this ledger. No stored snapshots. Closed includes done, killed, deferred, and archived.${evidence.range === null || evidence.range === undefined ? '' : ` Window ${escapeHtml(evidence.range.from)} to ${escapeHtml(evidence.range.to)}; weeks the range cuts short are marked partial.`} Missing acceptance history is reconstruction uncertainty: deleted items and unrecorded transitions cannot be recovered.${evidence.coverageGaps !== undefined && evidence.coverageGaps.missingAcceptance > 0 ? ` ${evidence.coverageGaps.missingAcceptance} retained items have no recorded acceptance and read as untriaged until departure.` : ''}</p></div><div class="evidence-grid"><section><h3>Throughput</h3><p><strong>${evidence.throughput.perWeek}</strong> items/week<br><span class="muted">${evidence.throughput.total} closures${evidence.throughput.done === undefined ? '' : ` (${evidence.throughput.done} done)`} over ${evidence.throughput.windowWeeks} weeks.</span></p></section><section><h3>Cycle time</h3>${cycle}</section><section><h3>Forecast</h3>${forecast}</section></div>${rows}</section>`;
257
257
  }
258
258
 
259
- function renderTerminalTable(items) {
260
- if (items.length === 0) {
261
- return '<p class="muted">No completed, killed, deferred, or archived items.</p>';
259
+ function renderSwarm(batches, swarm = null) {
260
+ const retained = (Array.isArray(batches) ? batches : []).filter((batch) => Array.isArray(batch) && batch.length > 0);
261
+ if (retained.length === 0) {
262
+ if (swarm === null || swarm === undefined) return '';
263
+ return `<details id="batches" class="panel"><summary>Scoped members of existing batches</summary><p class="muted">No eligible batches in this scope. Batches require mapped area and complexity values in the eligible complexities; missing mappings are omitted, never invented.</p></details>`;
262
264
  }
263
- return `<div class="table-scroll"><table><thead><tr><th>Item</th><th>Title</th><th>Status</th><th>Date</th><th>Decision</th></tr></thead><tbody>${items.map((item) => {
264
- const handle = item.number === null ? item.id : `#${item.number}`;
265
- const decision = item.decisions.at(-1)?.summary ?? '';
266
- return `<tr><td>${escapeHtml(handle)}</td><td>${escapeHtml(item.title)}</td><td>${escapeHtml(item.status)}</td><td>${escapeHtml(item.terminalDate)}</td><td>${escapeHtml(decision)}</td></tr>`;
267
- }).join('')}</tbody></table></div>`;
268
- }
269
-
270
- function renderSwarm(batches) {
271
- if (batches.length === 0) {
272
- return '';
273
- }
274
- return `<section class="panel swarm"><div class="section-heading"><div><p class="eyebrow">Parallel work</p><h2>Swarm candidates</h2></div><p class="muted">Ready items only. Each batch contains no repeated mapped area.</p></div><div class="batch-grid">${batches.map((batch, index) => `<section class="batch"><h3>Batch ${index + 1}</h3><ol>${batch.map((item) => `<li><span>${escapeHtml(item.number === null ? item.id : `#${item.number}`)}</span> ${escapeHtml(item.title)}</li>`).join('')}</ol></section>`).join('')}</div></section>`;
265
+ return `<details id="batches" class="panel"><summary>Scoped members of existing batches</summary><div class="section-heading"><div><p class="eyebrow">Parallel work</p><h2>Swarm candidates</h2></div><p class="muted">Ready items only. Each batch contains no repeated mapped area. Scoped members of the existing allocation; original packing is preserved.</p></div><div class="batch-grid">${retained.map((batch, index) => {
266
+ const ids = batch.map((item) => item.id).filter(Boolean);
267
+ return `<section class="batch" data-batch-ids="${escapeHtml(ids.join(','))}"><h3>Batch ${index + 1}</h3><ol>${batch.map((item) => `<li><span>${escapeHtml(item.number === null ? item.id : `#${item.number}`)}</span> ${escapeHtml(item.title)}</li>`).join('')}</ol><button type="button" data-show-items="${escapeHtml(ids.join(','))}" data-label="${escapeHtml(`Batch ${index + 1}`)}">Show batch</button></section>`;
268
+ }).join('')}</div></details>`;
275
269
  }
276
270
 
277
271
  // A facet group is one dimension of the open set: readiness, lifecycle status,
278
272
  // kind, and one group for every configured mapped field. A mapped field the
279
273
  // item does not carry has no value in that dimension, so it answers no chip
280
- // there rather than answering all of them.
281
- function facetValue(item, key) {
274
+ function facetValues(item, key) {
282
275
  if (key === 'readiness') {
283
- return item.readiness.state;
276
+ return [item.readiness.state];
284
277
  }
285
278
  if (key === 'status') {
286
- return item.status;
279
+ return [item.status];
287
280
  }
288
281
  if (key === 'kind') {
289
- return item.kind;
282
+ return [item.kind];
283
+ }
284
+ if (key === 'priority') {
285
+ return reportFieldValues(item.priority);
286
+ }
287
+ return reportFieldValues(item.fields[key.slice(6)]);
288
+ }
289
+
290
+ function observedTypedValues(items, key) {
291
+ const seen = new Map();
292
+ for (const item of items) {
293
+ for (const value of facetValues(item, key)) {
294
+ const id = `${typeof value}:${JSON.stringify(value)}`;
295
+ if (!seen.has(id)) {
296
+ seen.set(id, value);
297
+ }
298
+ }
290
299
  }
291
- const value = item.fields[key.slice(6)];
292
- return value === undefined ? null : String(value);
300
+ return [...seen.values()].sort((left, right) => {
301
+ const leftKey = `${typeof left}:${JSON.stringify(left)}`;
302
+ const rightKey = `${typeof right}:${JSON.stringify(right)}`;
303
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
304
+ });
305
+ }
306
+
307
+ function hasMissing(items, key) {
308
+ return items.some((item) => facetValues(item, key).length === 0);
309
+ }
310
+
311
+ function renderCoverage(fieldCoverage) {
312
+ const rows = fieldCoverage.map((entry) => {
313
+ const scope = entry.mapped ? `${entry.present} present, ${entry.missing} missing, ${entry.invalid} invalid` : `not configured — ${entry.missing} items have no value here`;
314
+ return `<li><strong>${escapeHtml(fieldLabel(entry.name))}</strong> — ${escapeHtml(scope)}</li>`;
315
+ }).join('');
316
+ return `<details id="coverage" class="panel"><summary>Metadata coverage</summary><ul>${rows}</ul><p class="muted">Missing means the item carries no value there. Invalid values are omitted from filters and never printed raw. Tags accept a nonempty string or an array of nonempty strings.</p></details>`;
293
317
  }
294
318
 
295
- function observedValues(items, key) {
296
- return [...new Set(items.map((item) => facetValue(item, key)).filter((value) => value !== null))].sort();
319
+ function renderQuickViews() {
320
+ const views = [
321
+ ['work-next', 'Work next'],
322
+ ['in-progress', 'In progress'],
323
+ ['blocked', 'Blocked'],
324
+ ['triage', 'Needs triage'],
325
+ ['all-open', 'All open'],
326
+ ];
327
+ return `<div id="items-controls" aria-label="Items controls"><div role="group" aria-label="Quick views">${views.map(([value, label], index) => `<button type="button" data-quick="${value}" aria-pressed="${index === 0 ? 'true' : 'false'}">${label}</button>`).join('')}</div></div>`;
297
328
  }
298
329
 
299
- const FACET_GROUP_LABELS = { readiness: 'Readiness', status: 'Status', kind: 'Kind' };
330
+ function areaCellKey(item) {
331
+ const value = item.fields?.area;
332
+ return (typeof value === 'string' && value !== '') ? value : null;
333
+ }
334
+
335
+ function renderAreaMatrix(openItems, itemNumbers) {
336
+ const areas = [...new Set(openItems.map(areaCellKey))].sort((a, b) => {
337
+ if (a === null) return 1;
338
+ if (b === null) return -1;
339
+ return a < b ? -1 : a > b ? 1 : 0;
340
+ });
341
+ const statuses = [...new Set(openItems.map((item) => item.status))].sort();
342
+ if (openItems.length === 0 || areas.length === 0 || statuses.length === 0) {
343
+ return `<section id="area-matrix" class="panel" aria-label="Area by status matrix"><h3>Area by status</h3><p class="muted">No open items in this scope.</p></section>`;
344
+ }
345
+ const head = `<tr><th scope="col">Area</th>${statuses.map((s) => `<th scope="col">${escapeHtml(String(s))}</th>`).join('')}</tr>`;
346
+ const rows = areas.map((area) => {
347
+ const label = area === null ? 'Missing' : area;
348
+ const cells = statuses.map((status) => {
349
+ const ids = openItems.filter((item) => areaCellKey(item) === area && item.status === status).map((item) => item.id);
350
+ if (ids.length === 0) return '<td><span class="muted">—</span></td>';
351
+ const blocked = ids.filter((id) => openItems.find((item) => item.id === id)?.readiness.state === 'blocked').length;
352
+ const names = ids.map((id) => escapeHtml(handleFor(id, itemNumbers))).join(', ');
353
+ const areaAttr = area === null ? ' data-matrix-missing="1"' : ` data-matrix-area="${escapeHtml(area)}"`;
354
+ return `<td>${ids.length} (blocked ${blocked})<br><span class="muted">${names}</span> <button type="button" data-matrix-status="${escapeHtml(status)}"${areaAttr} data-label="${escapeHtml(`${label} · ${status}`)}">Show</button></td>`;
355
+ }).join('');
356
+ return `<tr><th scope="row">${escapeHtml(label)}</th>${cells}</tr>`;
357
+ }).join('');
358
+ return `<section id="area-matrix" class="panel" aria-label="Area by status matrix"><h3>Area by status</h3><p class="muted">Scoped open work. Each cell opens its exact items.</p><div class="table-scroll"><table><thead>${head}</thead><tbody>${rows}</tbody></table></div></section>`;
359
+ }
360
+
361
+ function renderAttentionSummary(attention, openItems = []) {
362
+ const blockedFacts = attention.blocked.length === 0 ? 'Nothing blocked.' : `#${attention.blocked[0].number} ${attention.blocked[0].title} blocked by ${attention.blocked[0].blockers.map((b) => `#${b.number}`).join(', ')} · age ${attention.blocked[0].ageDays}d`;
363
+ const agingFacts = attention.aging.length === 0 ? 'No open items.' : `#${attention.aging[0].number} ${attention.aging[0].title} · age ${attention.aging[0].ageDays}d · ${attention.aging[0].status}`;
364
+ const stuckFacts = attention.stuck.length === 0 ? 'Nothing past p85.' : `#${attention.stuck[0].number} ${attention.stuck[0].title} · ${attention.stuck[0].elapsedDays}d since accept · p85 ${attention.stuck[0].thresholdDays}d`;
365
+ const inProgress = openItems.filter((item) => item.status === 'in-progress');
366
+ const triage = openItems.filter((item) => item.status === 'triage');
367
+ const numbers = (items) => items.map((item) => item.number).filter((n) => n !== null && n !== undefined).join(',');
368
+ const action = (nums, label) => nums === '' ? '' : ` <button type="button" data-show-numbers="${escapeHtml(nums)}" data-label="${escapeHtml(label)}">Show</button>`;
369
+ const blockedNums = attention.blocked.map((e) => e.number).filter((n) => n !== null && n !== undefined).join(',');
370
+ const agingNums = attention.aging.map((e) => e.number).filter((n) => n !== null && n !== undefined).join(',');
371
+ const stuckNums = attention.stuck.map((e) => e.number).filter((n) => n !== null && n !== undefined).join(',');
372
+ return `<section id="attention-summary" class="panel" aria-label="Attention summary"><div class="attention-grid">`
373
+ + `<section><h3>In progress</h3><p>${inProgress.length === 0 ? 'Nothing in progress.' : escapeHtml(`${inProgress.length} item${inProgress.length === 1 ? '' : 's'} in progress.`)}${action(numbers(inProgress), 'In progress')}</p></section>`
374
+ + `<section><h3>Blocked</h3><p>${escapeHtml(blockedFacts)}</p><p class="muted">Showing ${attention.blocked.length} of ${attention.blockedTotal}.${action(blockedNums, 'Blocked attention')}</p></section>`
375
+ + `<section><h3>Needs triage</h3><p>${triage.length === 0 ? 'Nothing needs triage.' : escapeHtml(`${triage.length} item${triage.length === 1 ? '' : 's'} need triage.`)}${action(numbers(triage), 'Needs triage')}</p></section>`
376
+ + `<section><h3>Oldest</h3><p>${escapeHtml(agingFacts)}${action(agingNums, 'Oldest open work')}</p></section>`
377
+ + `<section><h3>Past p85</h3><p>${escapeHtml(stuckFacts)}${action(stuckNums, 'Past p85 attention')}</p></section>`
378
+ + `</div></section>`;
379
+ }
380
+
381
+ const FACET_GROUP_LABELS = { readiness: 'Readiness', status: 'Status', kind: 'Kind', priority: 'Priority' };
300
382
 
301
383
  // One filter vocabulary for the whole artifact. A fixed criterion a named view
302
384
  // was generated from names its dimension and its values exactly as the
@@ -310,29 +392,45 @@ function optionLabel(key, value) {
310
392
  return key === 'readiness' ? READINESS_LABELS[value] : String(value);
311
393
  }
312
394
 
395
+ function typedIncludes(values, candidate) {
396
+ return values.some((entry) => typeof entry === typeof candidate && entry === candidate);
397
+ }
398
+
313
399
  // Readiness and kind are the schema's own closed vocabularies, so every value
314
400
  // is offered whether or not the open set currently holds one; a chip counting
315
- // zero is a fact about the ledger. Status and the mapped fields are open, so
316
- // only what the open cards actually carry is offered.
401
+ // zero is a fact about the ledger. Status, priority, and the mapped fields are
402
+ // open, so only what the open cards actually carry is offered, plus a typed
403
+ // Missing chip when any card carries no value there.
317
404
  function facetGroups(items, fieldNames) {
318
405
  const declared = [
319
406
  { key: 'readiness', values: ['ready', 'blocked', 'ineligible'] },
320
- { key: 'status', values: observedValues(items, 'status') },
407
+ { key: 'status', values: observedTypedValues(items, 'status') },
321
408
  { key: 'kind', values: ['task', 'epic'] },
409
+ { key: 'priority', values: observedTypedValues(items, 'priority') },
322
410
  ...fieldNames.map((name) => ({
323
411
  key: `field:${name}`,
324
- values: observedValues(items, `field:${name}`),
412
+ values: observedTypedValues(items, `field:${name}`),
325
413
  })),
326
414
  ];
327
- return declared.filter((group) => group.values.length > 0).map((group) => ({
328
- key: group.key,
329
- label: groupLabel(group.key),
330
- options: group.values.map((value) => ({
331
- value,
332
- label: optionLabel(group.key, value),
333
- count: items.filter((item) => facetValue(item, group.key) === value).length,
334
- })),
335
- }));
415
+ return declared
416
+ .map((group) => {
417
+ const options = group.values.map((value) => ({
418
+ selection: { kind: 'value', value },
419
+ value,
420
+ label: optionLabel(group.key, value),
421
+ count: items.filter((item) => typedIncludes(facetValues(item, group.key), value)).length,
422
+ }));
423
+ if (hasMissing(items, group.key)) {
424
+ options.push({
425
+ selection: { kind: 'missing' },
426
+ value: '__missing__',
427
+ label: 'Missing',
428
+ count: items.filter((item) => facetValues(item, group.key).length === 0).length,
429
+ });
430
+ }
431
+ return { key: group.key, label: groupLabel(group.key), options };
432
+ })
433
+ .filter((group) => group.options.length > 0);
336
434
  }
337
435
 
338
436
  // The chip is a checkbox inside its own label, so holding two values at once is
@@ -341,18 +439,12 @@ function facetGroups(items, fieldNames) {
341
439
  // knows what selecting it would leave.
342
440
  function renderFacets(groups, total) {
343
441
  const head = `<div class="facets-head"><p id="result-count" class="result-count" role="status" aria-live="polite">Showing ${total} of ${total} ${total === 1 ? 'item' : 'items'}</p><button type="button" id="clear-facets">Clear filters</button></div>`;
344
- const chips = (group) => group.options.map((option) => `<label class="chip"><input type="checkbox" class="facet" data-group="${escapeHtml(group.key)}" value="${escapeHtml(option.value)}"><span class="chip-text">${escapeHtml(option.label)}</span> <span class="chip-count">${option.count}</span></label>`).join('');
345
- return `<section id="facets" class="facets panel" aria-label="Filter items">${head}${groups.map((group) => `<fieldset class="facet-group" data-group="${escapeHtml(group.key)}"><legend>${escapeHtml(group.label)}</legend><div class="chips">${chips(group)}</div></fieldset>`).join('')}</section>`;
346
- }
347
-
348
- // An artifact a reader can narrow explains an empty list with the controls that
349
- // emptied it. A named view that matched nothing was narrowed by nobody, and no
350
- // control in the file brings an item back, so it states what is true of these
351
- // bytes — visibly, without waiting for scripting.
352
- function renderEmptyItems(model) {
353
- return model.view === null || model.items.length > 0 || model.terminalItems.length > 0
354
- ? `<p id="empty" class="empty" hidden>No items match these filters.</p>`
355
- : `<p id="empty" class="empty">No ledger item matches this view's criteria.</p>`;
442
+ const chips = (group) => group.options.map((option) => {
443
+ const kind = option.selection.kind;
444
+ const dataValue = kind === 'missing' ? '' : ` data-value="${escapeHtml(JSON.stringify(option.selection.value))}"`;
445
+ return `<label class="chip"><input type="checkbox" class="facet" data-group="${escapeHtml(group.key)}" data-kind="${kind}"${dataValue} value="${escapeHtml(option.value)}"><span class="chip-text">${escapeHtml(option.label)}</span> <span class="chip-count">${option.count}</span></label>`;
446
+ }).join('');
447
+ return `<section id="facets" class="facets panel" aria-label="Filter items">${head}<details class="facet-expand" id="facets-expand"><summary>Filters</summary>${groups.map((group) => `<fieldset class="facet-group" data-group="${escapeHtml(group.key)}"><legend>${escapeHtml(group.label)}</legend><div class="chips">${chips(group)}</div></fieldset>`).join('')}</details></section>`;
356
448
  }
357
449
 
358
450
  // A named report is one file about one subset, so it says which subset in its
@@ -386,14 +478,14 @@ function viewContextStyleSource(view) {
386
478
  }
387
479
 
388
480
  function styleSource() {
389
- return `:root{color-scheme:light;--canvas:#f3f0e9;--surface:#fffdf8;--ink:#20231f;--muted:#687069;--line:#d9d7ce;--navy:#15324a;--ready:#1e6b4f;--ready-bg:#e6f3ec;--blocked:#9b4b21;--blocked-bg:#f9e9df;--ineligible:#60666a;--ineligible-bg:#eceeef;--focus:#0b67b2;--shadow:0 10px 28px rgba(32,35,31,.07)}*{box-sizing:border-box}html{background:var(--canvas);color:var(--ink);font:14px/1.55 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{margin:0}button,input,select{font:inherit}button,select,input{border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}button,select{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,.row-link:focus-visible{outline:3px solid color-mix(in srgb,var(--focus) 35%,transparent);outline-offset:2px}.shell{width:min(1500px,100%);margin:auto;padding:28px 36px 56px}.masthead{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0 26px}.identity{display:flex;align-items:center;gap:16px}.logo{width:48px;height:48px;object-fit:contain}.eyebrow{margin:0 0 4px;color:var(--muted);font-size:.75rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.masthead h1,.section-heading h2{margin:0;font-family:ui-serif,Georgia,serif;color:var(--navy);line-height:1.1}.masthead h1{font-size:clamp(1.8rem,4vw,3rem)}.as-of{text-align:right;color:var(--muted)}.stats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin-bottom:16px}.stat,.panel{border:1px solid var(--line);border-radius:12px;background:var(--surface);box-shadow:var(--shadow)}.stat{padding:14px}.stat strong{display:block;color:var(--navy);font-size:1.55rem}.stat span{color:var(--muted)}.controls{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:12px;margin-bottom:18px;background:color-mix(in srgb,var(--surface) 95%,transparent);backdrop-filter:blur(10px)}.controls input{min-width:min(320px,100%);flex:1;padding:9px 11px}.controls select,.controls button{padding:8px 10px}.section-heading{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:14px}.section-heading p{margin:0}.items{display:grid;gap:18px}.group h2{margin:0 0 8px;color:var(--navy);font:700 1.05rem/1.2 ui-sans-serif,-apple-system,sans-serif}.card-grid{display:grid;grid-template-columns:1fr;gap:12px}.card{min-width:0;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 3px 12px rgba(32,35,31,.04);overflow:hidden}.card[open]{box-shadow:var(--shadow)}summary{display:block;padding:14px;cursor:pointer;list-style:none}summary::-webkit-details-marker{display:none}.summary-main{display:flex;gap:8px;align-items:baseline}.handle{flex:none;color:var(--navy);font-variant-numeric:tabular-nums;font-weight:800;overflow-wrap:anywhere}.title{font-weight:700;overflow-wrap:anywhere}.badges{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}.badge{display:inline-flex;border-radius:999px;background:#efeee9;padding:2px 8px;color:#4d534e;font-size:.73rem;font-weight:700}.badge.state-ready{background:var(--ready-bg);color:var(--ready)}.badge.state-blocked{background:var(--blocked-bg);color:var(--blocked)}.badge.state-ineligible{background:var(--ineligible-bg);color:var(--ineligible)}.badge.mapped{background:#e8edf2;color:var(--navy)}.card-detail{border-top:1px solid var(--line);padding:16px}.card-detail h3,.batch h3{margin:0 0 7px;color:var(--navy);font-size:.83rem;letter-spacing:.04em;text-transform:uppercase}.card-detail p{margin:0}.basic-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.standard-only,.detailed-only,.body-section{margin-top:18px}.reasons,.decisions{margin:0;padding-left:20px}.relations{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;margin:0}.relations dt{font-weight:700}.relations dd{margin:0;overflow-wrap:anywhere}.rendered-markdown{overflow-wrap:anywhere}.rendered-markdown h1,.rendered-markdown h2,.rendered-markdown h3{font-family:ui-serif,Georgia,serif;text-transform:none;letter-spacing:0}.rendered-markdown pre,noscript pre{overflow:auto;border-radius:8px;background:#202622;color:#f5f7f3;padding:12px}.rendered-markdown code{border-radius:4px;background:#eeece6;padding:1px 4px}.rendered-markdown pre code{background:transparent;padding:0}.rendered-markdown a{color:#075fa7}.rendered-markdown table,table{width:100%;border-collapse:collapse}.rendered-markdown th,.rendered-markdown td,th,td{border-bottom:1px solid var(--line);padding:8px;text-align:left;vertical-align:top}.panel{margin-top:26px;padding:18px}.facets{margin-top:0;margin-bottom:18px}.facets-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px}.facets-head button{padding:8px 10px}.result-count{margin:0;color:var(--muted);font-weight:700}.facet-group{margin:0;border:0;padding:12px 0 0}.facet-group>legend{padding:0;color:var(--muted);font-size:.72rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.chip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:999px;background:#fff;padding:5px 11px;font-size:.8rem;font-weight:700;cursor:pointer}.chip input{flex:none;width:13px;height:13px;margin:0;border-radius:3px;accent-color:var(--navy)}.chip-count{color:var(--muted);font-variant-numeric:tabular-nums}.chip.selected{border-color:var(--navy);background:var(--navy);color:#fff}.chip.selected .chip-count{color:#c8d3dd}.chip:focus-within{outline:3px solid color-mix(in srgb,var(--focus) 35%,transparent);outline-offset:2px}.table-scroll{overflow-x:auto}.batch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}.batch{border:1px solid var(--line);border-radius:9px;padding:12px}.batch ol{margin:0;padding-left:22px}.muted{color:var(--muted)}.empty{padding:32px;text-align:center;color:var(--muted)}body[data-richness="basic"] .standard-only,body[data-richness="basic"] .detailed-only,body[data-richness="standard"] .detailed-only{display:none}.summary-context{display:block;margin-top:9px;color:var(--muted);font-size:.8rem}.summary-preview{display:block;margin-top:7px;padding-left:9px;border-left:2px solid var(--line);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font-size:.8rem}[hidden]{display:none!important}@media(min-width:851px){.card{scroll-margin-top:140px}}@media(max-width:850px){.shell{padding:20px 16px 40px}.stats{grid-template-columns:repeat(3,1fr)}.masthead,.section-heading{align-items:flex-start;flex-direction:column}.as-of{text-align:left}.controls{position:static}.basic-grid{grid-template-columns:1fr}}@media(max-width:480px){.stats{grid-template-columns:repeat(2,1fr)}.controls>*{width:100%}.summary-main{align-items:flex-start;flex-direction:column}.card-detail{padding:13px}}@media(prefers-reduced-motion:no-preference){.card{transition:box-shadow .15s ease,border-color .15s ease}.chip{transition:background-color .15s ease,color .15s ease}}.row-link{display:block;color:inherit;text-decoration:none}.ranked{counter-reset:rank;margin:0;padding:0;list-style:none;display:grid;gap:9px}.ranked li{counter-increment:rank;border:1px solid var(--line);border-radius:9px;background:#fff}.ranked .row-link{position:relative;padding:12px 14px 12px 52px}.ranked .row-link::before{content:counter(rank);position:absolute;left:14px;top:12px;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--navy);color:#fff;font-variant-numeric:tabular-nums;font-weight:800;font-size:.8rem}.why{display:flex;flex-wrap:wrap;gap:5px;margin:8px 0 0}.reason{display:inline-flex;border-radius:999px;background:#eef1f4;padding:2px 9px;color:var(--navy);font-size:.74rem;font-weight:700}.reason-class,.reason-class-unknown,.reason-due{background:var(--blocked-bg);color:var(--blocked)}.reason-leverage,.reason-epic{background:var(--ready-bg);color:var(--ready)}.notice{margin:0 0 12px;border-left:3px solid var(--blocked);background:var(--blocked-bg);padding:8px 12px;color:var(--blocked)}.attention-grid,.evidence-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:16px}.attention-grid h3,.evidence-grid h3,.chart-row h3{margin:0 0 8px;color:var(--navy);font-size:.83rem;letter-spacing:.04em;text-transform:uppercase}.attention-grid p,.evidence-grid p{margin:0}.plain{margin:0;padding:0;list-style:none;display:grid;gap:9px}.plain li{border-bottom:1px solid var(--line)}.plain li:last-child{border-bottom:0}.plain .row-link{padding-bottom:8px}.plain li:last-child .row-link{padding-bottom:0}.chart-row{margin-top:20px}.chart-figure{margin:0 0 12px}.chart{display:block;width:100%;height:auto}.chart-compact{max-width:min(100%,460px)}.chart-axis{stroke:var(--line);stroke-width:1}.chart-grid{stroke:var(--line);stroke-width:1;stroke-dasharray:3 3}.chart-tick{fill:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}.chart-value{fill:var(--navy);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums}.chart-total{fill:var(--ink);font-size:11px;font-weight:800;font-variant-numeric:tabular-nums}.chart-head{fill:var(--muted);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase}.chart-label{fill:var(--ink);font-size:11px}.bar-arrival{fill:#a8bccb}.bar-completion{fill:var(--ready)}.heat{fill:var(--navy)}.rolling{fill:none;stroke:var(--blocked);stroke-width:2;stroke-linejoin:round}.fan{fill:var(--ready);fill-opacity:.28;stroke:var(--ready);stroke-width:1.5}.mark-50,.mark-85,.mark-95{stroke:var(--navy);stroke-width:1.5;stroke-dasharray:2 2}.band-terminal{fill:var(--ready);fill-opacity:.75}.band-accepted{fill:#7fae97}.band-triage{fill:#cddbd3}.sample{fill:var(--navy);fill-opacity:.55}.percentile-50{stroke:var(--navy);stroke-width:1.5;stroke-dasharray:5 3}.percentile-85{stroke:var(--blocked);stroke-width:1.5;stroke-dasharray:5 3}figcaption{display:flex;flex-wrap:wrap;gap:14px;margin-top:6px;color:var(--muted);font-size:.78rem}.key{display:inline-flex;align-items:center;gap:6px}.key::before{content:"";width:11px;height:11px;border-radius:3px;background:currentColor}.key-arrival{color:#a8bccb}.key-completion,.key-terminal{color:var(--ready)}.key-rolling,.key-p85{color:var(--blocked)}.key-p50{color:var(--navy)}.key-accepted{color:#7fae97}.key-triage{color:#cddbd3}.key-50,.key-85,.key-95{color:var(--ready)}#work-next,#attention,#evidence{margin-top:22px}#drilldown{margin-top:34px;border-top:2px solid var(--line);padding-top:22px}#drilldown .stats{margin-top:14px}`;
481
+ return `:root{color-scheme:light;--canvas:#f3f0e9;--surface:#fffdf8;--ink:#20231f;--muted:#687069;--line:#d9d7ce;--navy:#15324a;--ready:#1e6b4f;--ready-bg:#e6f3ec;--blocked:#9b4b21;--blocked-bg:#f9e9df;--ineligible:#60666a;--ineligible-bg:#eceeef;--focus:#0b67b2;--shadow:0 10px 28px rgba(32,35,31,.07)}*{box-sizing:border-box}html{background:var(--canvas);color:var(--ink);font:14px/1.55 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{margin:0}button,input,select{font:inherit}button,select,input{border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}button,select{cursor:pointer}button:focus-visible,input:focus-visible,select:focus-visible,summary:focus-visible,.row-link:focus-visible{outline:3px solid color-mix(in srgb,var(--focus) 35%,transparent);outline-offset:2px}.shell{width:min(1500px,100%);margin:auto;padding:28px 36px 56px}.masthead{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:4px 0 26px}.identity{display:flex;align-items:center;gap:16px}.logo{width:48px;height:48px;object-fit:contain}.eyebrow{margin:0 0 4px;color:var(--muted);font-size:.75rem;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.masthead h1,.section-heading h2{margin:0;font-family:ui-serif,Georgia,serif;color:var(--navy);line-height:1.1}.masthead h1{font-size:clamp(1.8rem,4vw,3rem)}.as-of{text-align:right;color:var(--muted)}.stats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin-bottom:16px}.stat,.panel{border:1px solid var(--line);border-radius:12px;background:var(--surface);box-shadow:var(--shadow)}.stat{padding:14px}.stat strong{display:block;color:var(--navy);font-size:1.55rem}.stat span{color:var(--muted)}.controls{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:12px;margin-bottom:18px;background:color-mix(in srgb,var(--surface) 95%,transparent);backdrop-filter:blur(10px)}.controls input{min-width:min(320px,100%);flex:1;padding:9px 11px}.controls select,.controls button{padding:8px 10px}.section-heading{display:flex;align-items:end;justify-content:space-between;gap:20px;margin-bottom:14px}.section-heading p{margin:0}.items{display:grid;gap:18px}.group h2{margin:0 0 8px;color:var(--navy);font:700 1.05rem/1.2 ui-sans-serif,-apple-system,sans-serif}.card-grid{display:grid;grid-template-columns:1fr;gap:12px}.card{min-width:0;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 3px 12px rgba(32,35,31,.04);overflow:hidden}.card[open]{box-shadow:var(--shadow)}summary{display:block;padding:14px;cursor:pointer;list-style:none}summary::-webkit-details-marker{display:none}.summary-main{display:flex;gap:8px;align-items:baseline}.handle{flex:none;color:var(--navy);font-variant-numeric:tabular-nums;font-weight:800;overflow-wrap:anywhere}.title{font-weight:700;overflow-wrap:anywhere}.badges{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}.badge{display:inline-flex;border-radius:999px;background:#efeee9;padding:2px 8px;color:#4d534e;font-size:.73rem;font-weight:700}.badge.state-ready{background:var(--ready-bg);color:var(--ready)}.badge.state-blocked{background:var(--blocked-bg);color:var(--blocked)}.badge.state-ineligible{background:var(--ineligible-bg);color:var(--ineligible)}.badge.mapped{background:#e8edf2;color:var(--navy)}.card-detail{border-top:1px solid var(--line);padding:16px}.card-detail h3,.batch h3{margin:0 0 7px;color:var(--navy);font-size:.83rem;letter-spacing:.04em;text-transform:uppercase}.card-detail p{margin:0}.basic-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}.standard-only,.detailed-only,.body-section{margin-top:18px}.reasons,.decisions{margin:0;padding-left:20px}.relations{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px;margin:0}.relations dt{font-weight:700}.relations dd{margin:0;overflow-wrap:anywhere}.rendered-markdown{overflow-wrap:anywhere}.rendered-markdown h1,.rendered-markdown h2,.rendered-markdown h3{font-family:ui-serif,Georgia,serif;text-transform:none;letter-spacing:0}.rendered-markdown pre,noscript pre{overflow:auto;border-radius:8px;background:#202622;color:#f5f7f3;padding:12px}.rendered-markdown code{border-radius:4px;background:#eeece6;padding:1px 4px}.rendered-markdown pre code{background:transparent;padding:0}.rendered-markdown a{color:#075fa7}.rendered-markdown table,table{width:100%;border-collapse:collapse}.rendered-markdown th,.rendered-markdown td,th,td{border-bottom:1px solid var(--line);padding:8px;text-align:left;vertical-align:top}.panel{margin-top:26px;padding:18px}.facets{margin-top:0;margin-bottom:18px}.facets-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px}.facets-head button{padding:8px 10px}.result-count{margin:0;color:var(--muted);font-weight:700}.facet-group{margin:0;border:0;padding:12px 0 0}.facet-group>legend{padding:0;color:var(--muted);font-size:.72rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.chip{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:999px;background:#fff;padding:5px 11px;font-size:.8rem;font-weight:700;cursor:pointer}.chip input{flex:none;width:13px;height:13px;margin:0;border-radius:3px;accent-color:var(--navy)}.chip-count{color:var(--muted);font-variant-numeric:tabular-nums}.chip.selected{border-color:var(--navy);background:var(--navy);color:#fff}.chip.selected .chip-count{color:#c8d3dd}.chip:focus-within{outline:3px solid color-mix(in srgb,var(--focus) 35%,transparent);outline-offset:2px}.table-scroll{overflow-x:auto}.batch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}.batch{border:1px solid var(--line);border-radius:9px;padding:12px}.batch ol{margin:0;padding-left:22px}.muted{color:var(--muted)}.empty{padding:32px;text-align:center;color:var(--muted)}body[data-richness="basic"] .standard-only,body[data-richness="basic"] .detailed-only,body[data-richness="standard"] .detailed-only{display:none}.summary-context{display:block;margin-top:9px;color:var(--muted);font-size:.8rem}.summary-preview{display:block;margin-top:7px;padding-left:9px;border-left:2px solid var(--line);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font-size:.8rem}[hidden]{display:none!important}@media(min-width:851px){.card{scroll-margin-top:140px}}@media(min-width:1100px){#workspace-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:18px;align-items:start}#list-column{min-width:0}#detail-column{min-width:0}#detail-pane:empty{display:none}}#workspace-split{display:block}#list-column{min-width:0}#detail-column{min-width:0;margin-top:18px}@media(min-width:1100px){#detail-column{margin-top:0}}.display-expand{width:100%}.display-expand summary{cursor:pointer;font-weight:800;padding:4px 0}@media(min-width:1100px){.display-expand summary{display:none}}#items-presentation{display:flex;flex-wrap:wrap;gap:10px;align-items:center}.facet-expand{margin-top:12px}.facet-expand summary{cursor:pointer;font-weight:800}.facet-expand .chips{max-height:32vh;overflow:auto}@media print{.view-nav,#sticky-controls,#facets,#items-presentation,#drilldown-pill{display:none!important}.shell{width:auto;padding:0}.card,.panel{box-shadow:none;break-inside:avoid}.scope-caption{font-weight:700}}@media(max-width:850px){.shell{padding:20px 16px 40px}.stats{grid-template-columns:repeat(3,1fr)}.masthead,.section-heading{align-items:flex-start;flex-direction:column}.as-of{text-align:left}.controls{position:sticky;top:0}.basic-grid{grid-template-columns:1fr}}@media(max-width:480px){.stats{grid-template-columns:repeat(2,1fr)}.controls>*{width:100%}.summary-main{align-items:flex-start;flex-direction:column}.card-detail{padding:13px}}@media(prefers-reduced-motion:no-preference){.card{transition:box-shadow .15s ease,border-color .15s ease}.chip{transition:background-color .15s ease,color .15s ease}}.row-link{display:block;color:inherit;text-decoration:none}.ranked{counter-reset:rank;margin:0;padding:0;list-style:none;display:grid;gap:9px}.ranked li{counter-increment:rank;border:1px solid var(--line);border-radius:9px;background:#fff}.ranked .row-link{position:relative;padding:12px 14px 12px 52px}.ranked .row-link::before{content:counter(rank);position:absolute;left:14px;top:12px;display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:var(--navy);color:#fff;font-variant-numeric:tabular-nums;font-weight:800;font-size:.8rem}.why{display:flex;flex-wrap:wrap;gap:5px;margin:8px 0 0}.reason{display:inline-flex;border-radius:999px;background:#eef1f4;padding:2px 9px;color:var(--navy);font-size:.74rem;font-weight:700}.reason-class,.reason-class-unknown,.reason-due{background:var(--blocked-bg);color:var(--blocked)}.reason-leverage,.reason-epic{background:var(--ready-bg);color:var(--ready)}.notice{margin:0 0 12px;border-left:3px solid var(--blocked);background:var(--blocked-bg);padding:8px 12px;color:var(--blocked)}.attention-grid,.evidence-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:16px}.attention-grid h3,.evidence-grid h3,.chart-row h3{margin:0 0 8px;color:var(--navy);font-size:.83rem;letter-spacing:.04em;text-transform:uppercase}.attention-grid p,.evidence-grid p{margin:0}.plain{margin:0;padding:0;list-style:none;display:grid;gap:9px}.plain li{border-bottom:1px solid var(--line)}.plain li:last-child{border-bottom:0}.plain .row-link{padding-bottom:8px}.plain li:last-child .row-link{padding-bottom:0}.chart-row{margin-top:20px}.chart-figure{margin:0 0 12px}.chart{display:block;width:100%;height:auto}.chart-compact{max-width:min(100%,460px)}.chart-axis{stroke:var(--line);stroke-width:1}.chart-grid{stroke:var(--line);stroke-width:1;stroke-dasharray:3 3}.chart-tick{fill:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}.chart-value{fill:var(--navy);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums}.chart-total{fill:var(--ink);font-size:11px;font-weight:800;font-variant-numeric:tabular-nums}.chart-head{fill:var(--muted);font-size:10px;font-weight:800;letter-spacing:.04em;text-transform:uppercase}.chart-label{fill:var(--ink);font-size:11px}.bar-arrival{fill:#a8bccb}.bar-closure{fill:var(--ready)}.heat{fill:var(--navy)}.rolling{fill:none;stroke:var(--blocked);stroke-width:2;stroke-linejoin:round}.fan{fill:var(--ready);fill-opacity:.28;stroke:var(--ready);stroke-width:1.5}.mark-50,.mark-85,.mark-95{stroke:var(--navy);stroke-width:1.5;stroke-dasharray:2 2}.band-terminal{fill:var(--ready);fill-opacity:.75}.band-accepted{fill:#7fae97}.band-triage{fill:#cddbd3}.sample{fill:var(--navy);fill-opacity:.55}.percentile-50{stroke:var(--navy);stroke-width:1.5;stroke-dasharray:5 3}.percentile-85{stroke:var(--blocked);stroke-width:1.5;stroke-dasharray:5 3}figcaption{display:flex;flex-wrap:wrap;gap:14px;margin-top:6px;color:var(--muted);font-size:.78rem}.key{display:inline-flex;align-items:center;gap:6px}.key::before{content:"";width:11px;height:11px;border-radius:3px;background:currentColor}.key-arrival{color:#a8bccb}.key-closure,.key-terminal{color:var(--ready)}.key-rolling,.key-p85{color:var(--blocked)}.key-p50{color:var(--navy)}.key-accepted{color:#7fae97}.key-triage{color:#cddbd3}.key-50,.key-85,.key-95{color:var(--ready)}#work-next,#attention,#evidence{margin-top:22px}#drilldown{margin-top:34px;border-top:2px solid var(--line);padding-top:22px}#drilldown .stats{margin-top:14px}`;
390
482
  }
391
483
 
392
484
  // The one runtime the report ships. Exported so a test can execute it rather
393
485
  // than read it: a case that asserts on this string proves nothing about what
394
486
  // the browser does with it.
395
487
  export function reportClientSource() {
396
- return `'use strict';
488
+ return reportEvidenceBrowserSource() + '\n' + reportSvgBrowserSource() + '\n' + reportSelectionBrowserSource() + `\n'use strict';
397
489
  const itemRoot=document.getElementById('items');
398
490
  const cards=Array.from(itemRoot.querySelectorAll('[data-item]'));
399
491
  const search=document.getElementById('search');
@@ -407,39 +499,151 @@ const chipsByGroup=new Map();
407
499
  for(const chip of facetChips){const group=chip.input.dataset.group;if(!chipsByGroup.has(group))chipsByGroup.set(group,[]);chipsByGroup.get(group).push(chip);}
408
500
  const showHistory=document.getElementById('show-history');
409
501
  const history=document.getElementById('history');
502
+ const drilldownPill=document.getElementById('drilldown-pill');
503
+ const drilldownLabel=document.getElementById('drilldown-label');
504
+ const clearDrilldown=document.getElementById('clear-drilldown');
505
+ var drilldown=null;
506
+ var scopeListeners=new Set();
507
+ var reportData=JSON.parse(document.getElementById('report-data').textContent);var selectionItems=Array.isArray(reportData)?reportData:reportData.items;var workNextIds=Array.isArray(reportData)?selectionItems.map(item=>item.id):reportData.workNextIds;var workNextById=Array.isArray(reportData)?{}:reportData.workNextById;var rawImpact=(Array.isArray(reportData)||!reportData.impactById)?{}:reportData.impactById;var impactById={};for(const key of Object.keys(rawImpact)){var entry=rawImpact[key]||{};var downstream=Array.isArray(entry.downstreamIds)?entry.downstreamIds.slice():[];var ready=Array.isArray(entry.readyIfDoneIds)?entry.readyIfDoneIds.slice():[];impactById[key]=Object.freeze({downstreamIds:Object.freeze(downstream),readyIfDoneIds:Object.freeze(ready)});}Object.freeze(impactById);
508
+ var cardByItemId=new Map();
509
+ for(const card of cards){var key=card.dataset.itemId||card.id;if(!cardByItemId.has(key))cardByItemId.set(key,card);}
510
+ function cardForSelectionId(id){return cardByItemId.get(id)||cards.find(card=>card.id===id);}
511
+ var selectionItemById=new Map();for(const item of selectionItems)selectionItemById.set(item.id,item);function selectionItemForCard(card){return selectionItemById.get(card.dataset.itemId||card.id);}
512
+ function selectedScope(){var facets={};for(const chip of facetChips){if(!chip.input.checked)continue;var group=chip.input.dataset.group;var kind=chip.input.dataset.kind||'value';var selection;if(kind==='missing'){selection={kind:'missing'};}else{var raw=chip.input.dataset.value;var value=raw===undefined?chip.input.value:JSON.parse(raw);selection={kind:'value',value:value};}if(!facets[group])facets[group]=[];facets[group].push(selection);}return {search:search.value,facets:facets};}
513
+ function scopeSignature(){return JSON.stringify(selectedScope());}
514
+ function getScopeItems(){return selectReportItems(selectionItems,selectedScope());}
515
+ var lastScopeSignature=scopeSignature();
516
+ function notifyScopeListeners(){var items=getScopeItems();for(const listener of scopeListeners)listener(items);renderFlow();}
410
517
  const parsedFields=new Map();
411
518
  function fields(card){if(!parsedFields.has(card))parsedFields.set(card,JSON.parse(card.dataset.fields));return parsedFields.get(card);}
412
519
  function fieldValue(card,key){const value=fields(card)[key];return value===undefined?'':String(value);}
413
520
  function groupLabel(card){const key=groupBy.value;if(key==='none')return 'All items';if(key==='readiness')return card.dataset.state;if(key==='status')return card.dataset.status;if(key.startsWith('field:'))return fieldValue(card,key.slice(6))||'Unclassified';return 'Unclassified';}
414
521
  function compareText(left,right){return left<right?-1:left>right?1:0;}
415
- function facetDimension(card,group){if(group==='readiness')return card.dataset.state;if(group==='status')return card.dataset.status;if(group==='kind')return card.dataset.kind;if(group.startsWith('field:')){const value=fields(card)[group.slice(6)];return value===undefined?undefined:String(value);}return undefined;}
416
- function selectedFacets(){const groups=new Map();for(const chip of facetChips){if(!chip.input.checked)continue;const key=chip.input.dataset.group;if(!groups.has(key))groups.set(key,new Set());groups.get(key).add(chip.input.value);}return groups;}
417
522
  function clearFacets(){for(const chip of facetChips)chip.input.checked=false;}
418
- function matchesFacets(card,groups,except){for(const [key,values] of groups){if(key!==except&&!values.has(facetDimension(card,key)))return false;}return true;}
419
- function matchesSearch(card,term){return !term||card.dataset.search.includes(term);}
523
+ function refreshChipsFromSelection(scope){var dimensions=Array.from(chipsByGroup.keys());var counted=countReportFacets(selectionItems,scope,dimensions);var byDim=new Map();for(const entry of counted)byDim.set(entry.dimension,entry.options);for(const [group,chips] of chipsByGroup){var options=byDim.get(group)||[];var countByKey=new Map();for(const option of options)countByKey.set(JSON.stringify(option.selection),option.count);for(const chip of chips){var kind=chip.input.dataset.kind||'value';var sel=kind==='missing'?{kind:'missing'}:{kind:'value',value:chip.input.dataset.value===undefined?chip.input.value:JSON.parse(chip.input.dataset.value)};chip.chip.classList.toggle('selected',chip.input.checked);chip.count.textContent=String(countByKey.get(JSON.stringify(sel))??0);}}}
420
524
  function compareCards(left,right){const key=sortBy.value;if(key==='default')return Number(left.dataset.order)-Number(right.dataset.order);if(key==='priority'){const a=left.dataset.priority===''?null:Number(left.dataset.priority);const b=right.dataset.priority===''?null:Number(right.dataset.priority);if(a===null||b===null)return a===b?Number(left.dataset.order)-Number(right.dataset.order):a===null?1:-1;return a-b||Number(left.dataset.order)-Number(right.dataset.order);}if(key==='created')return compareText(left.dataset.created,right.dataset.created)||Number(left.dataset.order)-Number(right.dataset.order);if(key==='title')return compareText(left.dataset.title,right.dataset.title)||Number(left.dataset.order)-Number(right.dataset.order);if(key.startsWith('field:'))return compareText(fieldValue(left,key.slice(6)),fieldValue(right,key.slice(6)))||Number(left.dataset.order)-Number(right.dataset.order);return 0;}
421
- function applyHistory(){history.hidden=!showHistory.checked;}
525
+ function applyHistory(){if(history)history.hidden=!showHistory.checked;apply();}
422
526
  // Values inside a group are alternatives and groups narrow each other, so each
423
527
  // chip is counted in the cohort the search and the other groups already left,
424
528
  // never its own: two selections in one group cannot make their siblings zero.
425
- function refreshChips(searched,chosen){for(const [group,chips] of chipsByGroup){const counts=new Map();for(const card of searched){if(!matchesFacets(card,chosen,group))continue;const value=facetDimension(card,group);counts.set(value,(counts.get(value)??0)+1);}for(const chip of chips){chip.chip.classList.toggle('selected',chip.input.checked);chip.count.textContent=String(counts.get(chip.input.value)??0);}}}
426
- function apply(){const term=search.value.trim().toLocaleLowerCase('en-US');const chosen=selectedFacets();const searched=cards.filter(card=>matchesSearch(card,term));const visible=searched.filter(card=>matchesFacets(card,chosen)).sort(compareCards);const groups=new Map();for(const card of visible){const label=groupLabel(card);if(!groups.has(label))groups.set(label,[]);groups.get(label).push(card);}itemRoot.replaceChildren();for(const [label,members] of groups){const section=document.createElement('section');section.className='group';if(groupBy.value!=='none'){const heading=document.createElement('h2');heading.textContent=label+' ('+members.length+')';section.append(heading);}const grid=document.createElement('div');grid.className='card-grid';grid.append(...members);section.append(grid);itemRoot.append(section);}empty.hidden=visible.length!==0;refreshChips(searched,chosen);resultCount.textContent='Showing '+visible.length+' of '+cards.length+(cards.length===1?' item':' items');}
529
+
530
+ function apply(){var scope=selectedScope();var scopedItems=selectReportItems(selectionItems,scope);var listState={scope:scope,section:'items',quickView:quickView,showHistory:showHistory.checked,groupBy:groupBy.value,sortBy:sortBy.value,richness:richness.value,selectedId:selectedId,drilldown:drilldown,range:null};var listedItems=selectListItems(selectionItems,listState,workNextIds);var listedCards=listedItems.map(item=>cardForSelectionId(item.id)).filter(card=>card!==undefined);var preserveRank=(quickView==='work-next'&&sortBy.value==='default'&&drilldown===null);if(!preserveRank)listedCards=listedCards.slice().sort(compareCards);var openCards=listedCards;var historyCards=[];if(drilldown===null&&showHistory.checked){var terminal=new Set(['done','killed','archived','deferred']);openCards=[];historyCards=[];for(const card of listedCards){var sel=selectionItemForCard(card);var st=sel?sel.status:card.dataset.status;if(terminal.has(st))historyCards.push(card);else openCards.push(card);}}var groups=new Map();for(const card of openCards){var label=groupLabel(card);if(!groups.has(label))groups.set(label,[]);groups.get(label).push(card);}itemRoot.replaceChildren();for(const [label,members] of groups){var section=document.createElement('section');section.className='group';if(groupBy.value!=='none'){var heading=document.createElement('h2');heading.textContent=label+' ('+members.length+')';section.append(heading);}var grid=document.createElement('div');grid.className='card-grid';grid.append(...members);section.append(grid);itemRoot.append(section);}if(historyCards.length>0){var hsection=document.createElement('section');hsection.className='group history-group';var hheading=document.createElement('h2');hheading.textContent='History ('+historyCards.length+')';hsection.append(hheading);var hgrid=document.createElement('div');hgrid.className='card-grid';hgrid.append(...historyCards);hsection.append(hgrid);itemRoot.append(hsection);}if(drilldown===null){if(drilldownPill)drilldownPill.hidden=true;}else{if(drilldownPill)drilldownPill.hidden=false;if(drilldownLabel)drilldownLabel.textContent=drilldown.label+' ('+listedCards.length+')';}var totalVisible=openCards.length+historyCards.length+(drilldown!==null?0:0);var displayCount=drilldown!==null?listedCards.length:openCards.length+historyCards.length;empty.hidden=displayCount!==0;refreshChipsFromSelection(scope);resultCount.textContent='Showing '+displayCount+' of '+cards.length+(cards.length===1?' item':' items');updateActiveScope(scope);if(flowScope)flowScope.textContent='Scope: '+(activeScope?activeScope.textContent:'');if(graphScope)graphScope.textContent='Scope: '+(activeScope?activeScope.textContent:'');if(historyExplain){var wantsTerminal=false;try{var f=scope.facets&&scope.facets.status||[];for(const sel of f){if(sel.kind==='value'&&(sel.value==='done'||sel.value==='killed'||sel.value==='archived'||sel.value==='deferred'))wantsTerminal=true;}}catch(err){}var scopedTerminal=scopedItems.filter(item=>item.status==='done'||item.status==='killed'||item.status==='archived'||item.status==='deferred');historyExplain.hidden=!(wantsTerminal&&!showHistory.checked&&scopedTerminal.length>0&&displayCount===0);}if(itemList){itemList.replaceChildren();var rows=[];var rowItems=drilldown!==null?listedItems:(openCards.map(card=>selectionItemForCard(card)).filter(Boolean).concat(historyCards.map(card=>selectionItemForCard(card)).filter(Boolean)));for(const item of rowItems){var card=cardForSelectionId(item.id);var anchor=card?card.id:'item-'+item.id;var handle=item.number===null||item.number===undefined?item.id:'#'+item.number;var li=document.createElement('li');var a=document.createElement('a');a.className='row-link';a.setAttribute?a.setAttribute('href','#'+anchor):a.href='#'+anchor;if(a.dataset!==undefined){a.dataset.reveal=anchor;a.dataset.inspect=item.id;}var name=document.createElement('span');name.textContent=handle+' '+item.title;var detail=document.createElement('span');detail.className='muted';var why=workNextById&&workNextById[item.id]&&workNextById[item.id].reasons;if(quickView==='work-next'&&why&&why.length>0)detail.textContent=why.map(r=>r.label).join(' \u00b7 ');else detail.textContent=item.status+' \u00b7 '+(item.readiness&&item.readiness.state||'');a.append(name,detail);a.addEventListener('click',function(ev){var id=this.dataset?this.dataset.inspect||this.dataset.reveal:null;if(id&&reveal(id))ev.preventDefault();});li.append(a);rows.push(li);}itemList.append(...rows);itemList.className=(quickView==='work-next'&&sortBy.value==='default')?'ranked':'plain';}var sig=JSON.stringify(scope)+'|'+quickView+'|'+(showHistory.checked?'h1':'h0');if(sig!==lastScopeSignature){lastScopeSignature=sig;notifyScopeListeners();}}
427
531
  function renderBody(card){const target=card.querySelector('.rendered-markdown');if(target.dataset.rendered==='1')return;const source=card.querySelector('script[type="text/markdown"]');target.innerHTML=window.renderMarkdown(JSON.parse(source.textContent));target.dataset.rendered='1';}
428
532
  for(const card of cards){card.addEventListener('toggle',()=>{if(card.open)renderBody(card);});}
429
533
  function revealOffset(){const controls=document.querySelector('.controls');return window.getComputedStyle(controls).position==='sticky'?controls.getBoundingClientRect().height+12:0;}
430
- function reveal(id){const target=cards.find(card=>card.id===id);if(target===undefined)return false;search.value='';clearFacets();apply();target.open=true;renderBody(target);target.style.scrollMarginTop=revealOffset()+'px';target.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'start'});target.querySelector('summary').focus({preventScroll:true});return true;}
431
- for(const link of document.querySelectorAll('[data-reveal]'))link.addEventListener('click',event=>{if(reveal(link.dataset.reveal))event.preventDefault();});
432
- search.addEventListener('input',apply);groupBy.addEventListener('change',apply);sortBy.addEventListener('change',apply);for(const chip of facetChips)chip.input.addEventListener('change',apply);richness.addEventListener('change',()=>{document.body.dataset.richness=richness.value;});
534
+ function reveal(id){const target=cardForSelectionId(id);if(target===undefined)return false;selectedId=target.dataset.itemId||target.id;var itemsSection=document.getElementById('section-items');if(itemsSection&&itemsSection.hidden)setSection('items',false);const pane=document.getElementById('detail-pane');if(pane&&!target.isConnected)pane.append(target);target.open=true;renderBody(target);var notice=target.querySelector('.outside-notice');if(notice){var scopedIds=new Set(getScopeItems().map(item=>item.id));var tid=target.dataset.itemId||target.id;notice.hidden=scopedIds.has(tid);}target.style.scrollMarginTop=revealOffset()+'px';target.scrollIntoView({behavior:window.matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth',block:'start'});target.querySelector('summary').focus({preventScroll:true});return true;}
535
+ for(const link of document.querySelectorAll('[data-reveal]'))link.addEventListener('click',event=>{if(reveal(link.dataset.reveal))event.preventDefault();});for(const btn of document.querySelectorAll('[data-show-items]'))btn.addEventListener('click',()=>{var raw=btn.dataset.showItems||'';var ids=raw.split(',').map(function(s){return s.trim();}).filter(Boolean);var label=btn.dataset.label||'Selected items';showItems({label:label,itemIds:ids});});for(const btn of document.querySelectorAll('[data-show-numbers]'))btn.addEventListener('click',()=>{var wanted=(btn.dataset.showNumbers||'').split(',').map(function(s){return s.trim();}).filter(Boolean);var ids=selectionItems.filter(function(item){return item.number!==null&&item.number!==undefined&&wanted.indexOf(String(item.number))!==-1;}).map(function(item){return item.id;});if(ids.length===0){var nums=wanted;try{var data=JSON.parse(document.getElementById('report-data').textContent);var all=Array.isArray(data)?data:data.items;ids=(all||[]).filter(function(item){return nums.indexOf(String(item.number))!==-1;}).map(function(item){return item.id;});}catch(e){} }var label=btn.dataset.label||'Selected items';showItems({label:label,itemIds:ids});});for(const btn of document.querySelectorAll('[data-matrix-status]'))btn.addEventListener('click',()=>{var status=btn.dataset.matrixStatus;var area=btn.dataset.matrixArea;var missing=btn.dataset.matrixMissing==='1';var ids=selectionItems.filter(function(item){if(item.status!==status)return false;var value=item.fields&&item.fields.area;var isMissing=!(typeof value==='string'&&value!=='');if(missing)return isMissing;return value===area;}).map(function(item){return item.id;});var label=btn.dataset.label||'Selected items';showItems({label:label,itemIds:ids});});
536
+ function clearDrilldownState(){drilldown=null;drilldownPill.hidden=true;}
537
+ var quickView='work-next';
538
+ var selectedId=null;
539
+ var quickButtons=Array.from(document.querySelectorAll('[data-quick]'));
540
+ function setQuick(view){quickView=view;for(const button of quickButtons){var pressed=button.dataset.quick===view?'true':'false';if(button.setAttribute)button.setAttribute('aria-pressed',pressed);else button.attributes['aria-pressed']=pressed;}}
541
+ for(const button of quickButtons)button.addEventListener('click',()=>{setQuick(button.dataset.quick);apply();});
542
+ var activeScope=document.getElementById('active-scope');
543
+ var historyExplain=document.getElementById('history-explain');
544
+ var showHistoryAction=document.getElementById('show-history-action');
545
+ var itemList=document.getElementById('item-list');
546
+ var flowScope=document.getElementById('flow-scope');
547
+ var graphScope=document.getElementById('graph-scope');
548
+ function terminalStatus(value){return value==='done'||value==='killed'||value==='archived'||value==='deferred';}
549
+ function updateActiveScope(scope){if(!activeScope)return;var parts=[];var q=scope.search.trim();if(q)parts.push('Search: \u201c'+q+'\u201d');for(const [dim,sels] of Object.entries(scope.facets)){var label=dim;try{label=document.querySelector('fieldset[data-group="'+dim+'"] legend')?.textContent||dim;}catch(e){}var names=sels.map(sel=>sel.kind==='missing'?'Missing':String(sel.value)).join(', ');parts.push(label+': '+names);}activeScope.textContent=parts.length===0?'No filters':parts.join(' \u00b7 ');}
550
+ search.addEventListener('input',()=>{clearDrilldownState();apply();});groupBy.addEventListener('change',apply);sortBy.addEventListener('change',apply);for(const chip of facetChips)chip.input.addEventListener('change',()=>{clearDrilldownState();apply();});richness.addEventListener('change',()=>{document.body.dataset.richness=richness.value;});
433
551
  showHistory.addEventListener('change',applyHistory);
434
- document.getElementById('clear-facets').addEventListener('click',()=>{clearFacets();apply();});
435
- document.getElementById('expand-all').addEventListener('click',()=>{for(const card of cards){if(card.isConnected){card.open=true;renderBody(card);}}});
436
- document.getElementById('collapse-all').addEventListener('click',()=>{for(const card of cards)card.open=false;});
437
- applyHistory();apply();`;
552
+ document.getElementById('clear-facets').addEventListener('click',()=>{clearFacets();clearDrilldownState();apply();});
553
+ clearDrilldown.addEventListener('click',()=>{clearDrilldownState();apply();});
554
+ document.getElementById('expand-all').addEventListener('click',()=>{for(const card of itemRoot.querySelectorAll('[data-item]')){card.open=true;renderBody(card);}});
555
+ document.getElementById('collapse-all').addEventListener('click',()=>{for(const card of itemRoot.querySelectorAll('[data-item]')){card.open=false;}});
556
+ if(showHistoryAction)showHistoryAction.addEventListener('click',()=>{showHistory.checked=true;apply();});
557
+ var navButtons=['nav-items','nav-flow','nav-dependencies'].map(function(id){return document.getElementById(id);}).filter(function(el){return el;});
558
+ var sectionByName={};for(const s of Array.from(document.querySelectorAll('[data-section]'))){if(s.id)sectionByName[s.dataset.section]=s;}
559
+ var lastFocusBySection={};
560
+ function setSection(name,focus){var prev=document.body.dataset.section||'items';try{var active=document.activeElement;if(active&&active.id)lastFocusBySection[prev]=active.id;}catch(e){}document.body.dataset.section=name;for(const b of navButtons){var on=b.dataset.section===name;if(b.setAttribute)b.setAttribute('aria-pressed',on?'true':'false');else b.attributes['aria-pressed']=on?'true':'false';}for(const [key,sec] of Object.entries(sectionByName)){if(sec.id&&sec.id.indexOf('section-')===0)sec.hidden=(key!==name);}if(name==='flow'&&!flowOpened){flowOpened=true;try{renderFlow();}catch(e){}}if(focus!==false){var target=null;try{target=document.getElementById(lastFocusBySection[name]||'');}catch(e){}if(target)target.focus();}}
561
+ for(const b of navButtons)b.addEventListener('click',()=>{setSection(b.dataset.section);});
562
+ document.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape'){for(const d of Array.from(document.querySelectorAll('details.facet-expand[open]')))d.open=false;}});
563
+ var displayExpand=document.getElementById('display-expand');var wideScreen=window.matchMedia('(min-width: 1100px)');function syncDisplayExpand(){if(displayExpand)displayExpand.open=wideScreen.matches;}wideScreen.addEventListener('change',syncDisplayExpand);syncDisplayExpand();
564
+ var flowSection=document.getElementById('section-flow');
565
+ var flowFrom=document.getElementById('flow-from');
566
+ var flowTo=document.getElementById('flow-to');
567
+ var flowError=document.getElementById('flow-error');
568
+ var flowLive=document.getElementById('flow-live');
569
+ var flowEvidence=document.getElementById('evidence');
570
+ var flowAsOf=(flowSection&&flowSection.getAttribute)?flowSection.getAttribute('data-asof'):null;
571
+ var lastValidRange=(flowFrom&&flowTo&&flowFrom.value&&flowTo.value)?{from:flowFrom.value,to:flowTo.value}:null;
572
+ var lastFlowKey=null;
573
+ var flowOpened=false;
574
+ var forecastCache={key:null,forecast:null};
575
+ function flowIsTerminal(value){return value==='done'||value==='killed'||value==='archived'||value==='deferred';}
576
+ function flowWindow(){try{return resolveWindow(flowAsOf,lastValidRange);}catch(err){return null;}}
577
+ var flowWeekCache=new Map();function flowWeekOf(date){var hit=flowWeekCache.get(date);if(hit===undefined){hit=weekStart(date);flowWeekCache.set(date,hit);}return hit;}
578
+ function flowArrivalIds(scoped,week){var win=flowWindow();if(!win)return [];return scoped.filter(function(item){return flowWeekOf(item.created)===week&&item.created>=win.start&&item.created<=win.end;}).map(function(item){return item.id;});}
579
+ function flowClosureIds(scoped,week){var win=flowWindow();if(!win)return [];return scoped.filter(function(item){return item.terminalDate!==null&&item.terminalDate!==undefined&&flowWeekOf(item.terminalDate)===week&&item.terminalDate>=win.start&&item.terminalDate<=win.end;}).map(function(item){return item.id;});}
580
+ function flowDoneIds(scoped,week){var win=flowWindow();if(!win)return [];return scoped.filter(function(item){return item.status==='done'&&item.terminalDate!==null&&item.terminalDate!==undefined&&flowWeekOf(item.terminalDate)===week&&item.terminalDate>=win.start&&item.terminalDate<=win.end;}).map(function(item){return item.id;});}
581
+ function flowDrilldown(label,ids){showItems({label:label,itemIds:ids});}
582
+ function flowSelectButton(attrs,label,ids){var button=document.createElement('button');button.type='button';for(var key in attrs){button.dataset[key]=attrs[key];}button.textContent=label+' ('+ids.length+')';button.addEventListener('click',function(){flowDrilldown(label,ids);});return button;}
583
+ function flowWeekRow(scoped,week){var row=document.createElement('div');var arrivals=flowArrivalIds(scoped,week.weekStart);var closures=flowClosureIds(scoped,week.weekStart);var done=flowDoneIds(scoped,week.weekStart);var text=document.createElement('p');text.textContent='Week of '+week.weekStart+': '+arrivals.length+' arrivals, '+closures.length+' closures ('+done.length+' done)';row.append(text);row.append(flowSelectButton({flowChart:'weekly',flowWeek:week.weekStart,flowKind:'arrivals'},'Show arrivals for week of '+week.weekStart,arrivals));row.append(flowSelectButton({flowChart:'weekly',flowWeek:week.weekStart,flowKind:'closures'},'Show closures for week of '+week.weekStart,closures));row.append(flowSelectButton({flowChart:'weekly',flowWeek:week.weekStart,flowKind:'done'},'Show done for week of '+week.weekStart,done));return row;}
584
+ function flowOpenItems(scoped){return scoped.filter(function(item){return !flowIsTerminal(item.status);});}
585
+ function flowAgingBucketForAge(age){if(age<7)return 'under 7d';if(age<30)return '7-30d';if(age<90)return '30-90d';return 'over 90d';}
586
+ function flowAgingIds(scoped,bucket,status){return scoped.filter(function(item){if(flowIsTerminal(item.status))return false;if(item.status!==status)return false;var age=null;try{age=daysBetween(item.created,flowAsOf);}catch(e){age=null;}if(age===null||age===undefined)age=0;return flowAgingBucketForAge(age)===bucket;}).map(function(item){return item.id;});}
587
+ function flowAgingSection(scoped){var open=flowOpenItems(scoped);var matrix=null;try{matrix=buildAgingMatrix(open,flowAsOf);}catch(e){matrix={statuses:[],rows:[]};}var wrap=document.createElement('div');var heading=document.createElement('h3');heading.textContent='Aging by age and status';wrap.append(heading);var figure=document.createElement('div');try{figure.innerHTML=agingHeatmapChart(matrix);}catch(e){}wrap.append(figure);if(open.length===0){var empty=document.createElement('p');empty.textContent='No open items in this scope.';wrap.append(empty);return wrap;}for(var r=0;r<matrix.rows.length;r++){var row=matrix.rows[r];for(var c=0;c<matrix.statuses.length;c++){var status=matrix.statuses[c];var ids=flowAgingIds(scoped,row.label,status);var line=document.createElement('p');line.textContent=row.label+' \u00b7 '+status+': '+ids.length;wrap.append(line);if(ids.length>0){wrap.append(flowSelectButton({flowChart:'aging',flowBucket:row.label,flowStatus:status},'Show '+row.label+' '+status,ids));}}}return wrap;}
588
+ function flowCycleSampleIds(scoped,sample){var byNumber=scoped.filter(function(item){return item.status==='done'&&item.terminalDate===sample.completedOn&&item.number===sample.number;});if(byNumber.length>0)return byNumber.map(function(item){return item.id;});return scoped.filter(function(item){if(item.status!=='done')return false;if(item.terminalDate!==sample.completedOn)return false;var accept=null;for(var k=0;k<(item.decisions||[]).length;k++){if(item.decisions[k].action==='accept'){accept=item.decisions[k].date;break;}}if(accept===null)return false;var elapsed=null;try{elapsed=daysBetween(accept,item.terminalDate);}catch(e){elapsed=null;}return elapsed===sample.days;}).map(function(item){return item.id;});}
589
+ function flowCycleSection(scoped){var terminal=scoped.filter(function(item){return flowIsTerminal(item.status);});var cycle=null;try{cycle=buildCycleTime(terminal);}catch(e){cycle={sampleCount:0,medianDays:null,p85Days:null,samples:[]};}var wrap=document.createElement('div');var heading=document.createElement('h3');heading.textContent='Accept to completion';wrap.append(heading);var figure=document.createElement('div');try{figure.innerHTML=cycleTimeChart(cycle);}catch(e){}wrap.append(figure);if(!cycle||cycle.sampleCount===0){var empty=document.createElement('p');empty.textContent='No accept-to-complete history yet.';wrap.append(empty);return wrap;}var summary=document.createElement('p');summary.textContent='Median '+cycle.medianDays+'d \u00b7 p85 '+cycle.p85Days+'d \u00b7 '+cycle.sampleCount+' done items with recorded acceptance, accept to completion.';wrap.append(summary);for(var i=0;i<cycle.samples.length;i++){var sample=cycle.samples[i];var ids=flowCycleSampleIds(scoped,sample);var line=document.createElement('p');line.textContent='#'+sample.number+' completed '+sample.completedOn+' after '+sample.days+' days: '+ids.length;wrap.append(line);if(ids.length>0){wrap.append(flowSelectButton({flowChart:'cycle',flowCompleted:sample.completedOn,flowDays:String(sample.days)},'Show '+sample.days+'d completion on '+sample.completedOn,ids));}}return wrap;}
590
+ function flowAcceptDate(item){for(var k=0;k<(item.decisions||[]).length;k++){if(item.decisions[k].action==='accept')return item.decisions[k].date;}return null;}
591
+ function flowCumulativeIds(scoped,date,band){return scoped.filter(function(item){if(item.created>date)return false;var terminal=(item.terminalDate!==null&&item.terminalDate!==undefined)?item.terminalDate:null;var bandFor=null;if(terminal!==null&&terminal<=date)bandFor='terminal';else{var accept=flowAcceptDate(item);if(accept!==null&&accept<=date)bandFor='accepted';else bandFor='triage';}return bandFor===band;}).map(function(item){return item.id;});}
592
+ function flowCumulativeSection(scoped){var points=null;try{points=buildCumulativeFlow(scoped,flowAsOf,lastValidRange);}catch(e){points=[];}var wrap=document.createElement('div');var heading=document.createElement('h3');heading.textContent='Cumulative flow';wrap.append(heading);var figure=document.createElement('div');try{figure.innerHTML=cumulativeFlowChart(points);}catch(e){}wrap.append(figure);if(!points||points.length===0){var none=document.createElement('p');none.textContent='No items in this scope for the selected range.';wrap.append(none);return wrap;}var picker=document.createElement('div');var dateLabel=document.createElement('label');dateLabel.textContent='Cumulative date ';var dateInput=document.createElement('input');dateInput.id='flow-cumulative-date';dateInput.value=flowAsOf;dateLabel.append(dateInput);picker.append(dateLabel);var bandLabel=document.createElement('label');bandLabel.textContent=' Band ';var bandSelect=document.createElement('select');bandSelect.id='flow-cumulative-band';var bands=['triage','accepted','terminal'];for(var bi=0;bi<bands.length;bi++){var opt=document.createElement('option');opt.value=bands[bi];opt.textContent=bands[bi];bandSelect.append(opt);}bandSelect.value='accepted';bandLabel.append(bandSelect);picker.append(bandLabel);var showBtn=document.createElement('button');showBtn.id='flow-cumulative-show';showBtn.type='button';showBtn.textContent='Show cumulative items';showBtn.addEventListener('click',function(){var d=dateInput.value;var b=bandSelect.value;var ids=flowCumulativeIds(scoped,d,b);var count=ids.length;for(var i=0;i<points.length;i++){if(points[i].date===d&&points[i][b]!==undefined){count=points[i][b];break;}}flowDrilldown('Cumulative '+b+' on '+d+' ('+count+')',ids);});picker.append(showBtn);wrap.append(picker);var table=document.createElement('table');var cap=document.createElement('caption');cap.textContent='Cumulative inventory by date and band. Each band count opens its exact items.';table.append(cap);for(var pi=0;pi<points.length;pi++){var point=points[pi];var row=document.createElement('tr');var cell=document.createElement('td');cell.textContent=point.date+': triage '+point.triage+', accepted '+point.accepted+', terminal '+point.terminal;row.append(cell);for(var bj=0;bj<bands.length;bj++){var band=bands[bj];var ids=flowCumulativeIds(scoped,point.date,band);var action=document.createElement('td');if(point[band]>0){var btn=flowSelectButton({flowChart:'cumulative',flowDate:point.date,flowBand:band},'Show '+band+' on '+point.date,ids);btn.textContent='Show '+band+' on '+point.date+' ('+point[band]+')';action.append(btn);}else{action.textContent=band+' 0';}row.append(action);}table.append(row);}wrap.append(table);return wrap;}
593
+ function flowForecastCohortKey(scoped){return JSON.stringify(scoped.map(function(item){return item.id;}).sort())+'|'+JSON.stringify(lastValidRange);}
594
+ function flowForecastSection(scoped,weeks){var wrap=document.createElement('div');wrap.id='flow-forecast';var heading=document.createElement('h3');heading.textContent='Forecast';wrap.append(heading);if(!flowOpened){var pending=document.createElement('p');pending.id='flow-forecast-text';pending.textContent='Forecast loads when Flow opens. Open Flow to compute the closure-based estimate.';wrap.append(pending);return wrap;}var cohortKey=flowForecastCohortKey(scoped);var forecast=null;if(forecastCache.key===cohortKey){forecast=forecastCache.forecast;}else{var openCount=flowOpenItems(scoped).length;try{forecast=buildForecast(weeks,openCount,flowAsOf);}catch(e){forecast=null;}forecastCache={key:cohortKey,forecast:forecast};}var figure=document.createElement('div');try{figure.innerHTML=forecastChart(forecast,flowAsOf);}catch(e){figure.innerHTML='';}wrap.append(figure);if(forecast===null){var none=document.createElement('p');none.id='flow-forecast-text';none.textContent='No closures in the window, so no forecast.';wrap.append(none);return wrap;}if(forecast.remaining===0){var done=document.createElement('p');done.id='flow-forecast-text';done.textContent='0 open items remaining.';wrap.append(done);return wrap;}var text=document.createElement('p');text.id='flow-forecast-text';text.textContent=forecast.remaining+' open items remaining. 50% by '+forecast.date50+' ('+forecast.weeks50+' weeks). 85% by '+forecast.date85+' ('+forecast.weeks85+' weeks). 95% by '+forecast.date95+' ('+forecast.weeks95+' weeks). '+forecast.trials+' Monte Carlo trials resampling observed weekly closures. A closure-based estimate, not a delivery commitment.';wrap.append(text);return wrap;}
595
+ function flowThroughputSection(scoped,weeks){var wrap=document.createElement('div');wrap.id='flow-throughput';var heading=document.createElement('h3');heading.textContent='Throughput';wrap.append(heading);var figure=document.createElement('div');try{figure.innerHTML=throughputChart(weeks);}catch(e){figure.innerHTML='';}wrap.append(figure);var win=null;try{win=flowWindow();}catch(e){win=null;}var closureTotal=weeks.reduce(function(sum,week){return sum+week.closures;},0);var doneTotal=weeks.reduce(function(sum,week){return sum+(week.done||0);},0);var windowWeeks=win?win.windowWeeks:weeks.length;var perWeek=(windowWeeks===0)?0:Math.round((closureTotal/windowWeeks)*100)/100;var text=document.createElement('p');text.id='flow-throughput-text';if(closureTotal===0){text.textContent='No closures in the window, so no throughput mean. 0 closures (0 done) over '+windowWeeks+' weeks. 4-week mean needs four complete weeks.';}else{text.textContent=closureTotal+' closures ('+doneTotal+' done) over '+windowWeeks+' weeks. '+perWeek+' items/week. 4-week mean needs four complete weeks.';}wrap.append(text);return wrap;}
596
+ function flowMissingAcceptance(scoped){return scoped.filter(function(item){if(item.status==='triage')return false;var hasAccept=false;var hasKill=false;for(var k=0;k<(item.decisions||[]).length;k++){if(item.decisions[k].action==='accept')hasAccept=true;if(item.decisions[k].action==='kill')hasKill=true;}if(hasAccept)return false;if(item.status==='killed'&&hasKill)return false;return true;}).length;}
597
+ function flowGapsSection(scoped){var wrap=document.createElement('div');wrap.id='flow-gaps';var heading=document.createElement('h3');heading.textContent='History gaps';wrap.append(heading);var text=document.createElement('p');text.id='flow-gaps-text';if(scoped.length===0){text.textContent='No items in this scope for the selected range.';wrap.append(text);return wrap;}var count=flowMissingAcceptance(scoped);if(count>0){text.textContent=count+' retained items have no recorded acceptance and read as untriaged until departure. Missing acceptance history is reconstruction uncertainty: deleted items and unrecorded transitions cannot be recovered.';}else{text.textContent='All retained items carry the acceptance history this reconstruction needs. Deleted items and unrecorded transitions cannot be recovered from the snapshot.';}wrap.append(text);var cohort=document.createElement('p');cohort.textContent='Current-metadata cohort: '+scoped.length+' retained items. Closed includes done, killed, deferred, and archived.';wrap.append(cohort);return wrap;}
598
+ function renderFlow(){if(!flowLive||!flowAsOf)return;var scope=selectedScope();var key=JSON.stringify(scope)+'|'+JSON.stringify(lastValidRange);var scoped=getScopeItems();var cohortKey=flowForecastCohortKey(scoped);var needsCheap=(key!==lastFlowKey);var needsForecast=(flowOpened&&forecastCache.key!==cohortKey);if(!needsCheap&&!needsForecast)return;if(needsCheap)lastFlowKey=key;var weeks=buildWeeklyFlow(scoped,flowAsOf,lastValidRange);flowLive.replaceChildren();var heading=document.createElement('h3');heading.textContent='Arrivals against closures';flowLive.append(heading);var figure=document.createElement('div');figure.innerHTML=weeklyFlowChart(weeks);flowLive.append(figure);for(var i=0;i<weeks.length;i++){flowLive.append(flowWeekRow(scoped,weeks[i]));}flowLive.append(flowThroughputSection(scoped,weeks));flowLive.append(flowAgingSection(scoped));flowLive.append(flowCycleSection(scoped));flowLive.append(flowCumulativeSection(scoped));flowLive.append(flowGapsSection(scoped));flowLive.append(flowForecastSection(scoped,weeks));if(flowEvidence)flowEvidence.hidden=true;}
599
+ function flowOnRangeChange(){if(!flowFrom||!flowTo)return;var from=flowFrom.value;var to=flowTo.value;var problem=null;if(!isValidDateString(from)||!isValidDateString(to)){problem='Enter valid From and To dates as YYYY-MM-DD.';}else if(from>to){problem='From '+from+' is after To '+to+'. Keeping the last valid range.';}else if(flowAsOf&&to>flowAsOf){problem='To '+to+' is after the report date '+flowAsOf+'. Keeping the last valid range.';}if(flowError){if(problem){flowError.textContent=problem;flowError.hidden=false;}else{flowError.textContent='';flowError.hidden=true;}}if(!problem){lastValidRange={from:from,to:to};renderFlow();}}
600
+ if(flowFrom)flowFrom.addEventListener('change',flowOnRangeChange);
601
+ if(flowTo)flowTo.addEventListener('change',flowOnRangeChange);
602
+ if(flowLive)flowLive.addEventListener('click',function(ev){var target=null;try{target=ev.target&&ev.target.closest?ev.target.closest('[data-chart]'):null;}catch(e){target=null;}if(!target)return;var chart=null;try{chart=target.getAttribute('data-chart');}catch(e){chart=null;}if(!chart)return;var scoped=getScopeItems();if(chart==='weekly'){var week=null;var kind=null;try{week=target.getAttribute('data-week');kind=target.getAttribute('data-kind');}catch(e){}if(!week||!kind)return;var ids=kind==='arrivals'?flowArrivalIds(scoped,week):flowClosureIds(scoped,week);var label=kind==='arrivals'?'Show arrivals for week of '+week:'Show closures for week of '+week;flowDrilldown(label,ids);}else if(chart==='aging'){var bucket=null;var status=null;try{bucket=target.getAttribute('data-bucket');status=target.getAttribute('data-status');}catch(e){}if(!bucket||!status)return;var aids=flowAgingIds(scoped,bucket,status);flowDrilldown('Show '+bucket+' '+status,aids);}else if(chart==='cycle'){var numberRaw=null;var completed=null;var daysRaw=null;try{numberRaw=target.getAttribute('data-number');completed=target.getAttribute('data-completed');daysRaw=target.getAttribute('data-days');}catch(e){}if(!completed)return;var sample={number:numberRaw===''||numberRaw===null?null:Number(numberRaw),completedOn:completed,days:Number(daysRaw)};var cids=flowCycleSampleIds(scoped,sample);flowDrilldown('Show '+sample.days+'d completion on '+completed,cids);}});
603
+ function subscribeScope(listener){listener(getScopeItems());scopeListeners.add(listener);return function(){scopeListeners.delete(listener);};}
604
+ function inspectItem(id){return reveal(id);}
605
+ function showItems(selection){drilldown={label:selection.label,itemIds:Array.from(selection.itemIds)};try{if(typeof setSection==='function'&&document.getElementById('section-items'))setSection('items',false);}catch(e){}apply();}
606
+ window.wowbaggerReport={getScopeItems:getScopeItems,subscribeScope:subscribeScope,inspectItem:inspectItem,showItems:showItems,impactById:impactById};
607
+ setSection('items',false);applyHistory();apply();`;
608
+ }
609
+
610
+ function renderItemList(model) {
611
+ const rows = model.workNext.map((entry) => {
612
+ const anchor = `item-${entry.number === null ? entry.id : entry.number}`;
613
+ const handle = entry.number === null ? entry.id : `#${entry.number}`;
614
+ const reasons = entry.reasons.map((reason) => `<span class="reason reason-${escapeHtml(reason.code)}">${escapeHtml(reason.label)}</span>`).join('');
615
+ return `<li><a class="row-link" href="#${escapeHtml(anchor)}" data-reveal="${escapeHtml(anchor)}" aria-labelledby="row-work-next-${escapeHtml(anchor)}-name" aria-describedby="row-work-next-${escapeHtml(anchor)}-detail"><span class="summary-main" id="row-work-next-${escapeHtml(anchor)}-name"><span class="handle">${escapeHtml(handle)}</span><span class="title">${escapeHtml(entry.title)}</span></span><p class="why" id="row-work-next-${escapeHtml(anchor)}-detail">${reasons}</p></a></li>`;
616
+ }).join('');
617
+ const historyRows = model.terminalItems.map((item) => {
618
+ const anchor = `item-${item.number === null ? item.id : item.number}`;
619
+ const handle = item.number === null ? item.id : `#${item.number}`;
620
+ return `<li><a class="row-link" href="#${escapeHtml(anchor)}" data-reveal="${escapeHtml(anchor)}" aria-labelledby="row-history-${escapeHtml(anchor)}-name" aria-describedby="row-history-${escapeHtml(anchor)}-detail"><span class="summary-main" id="row-history-${escapeHtml(anchor)}-name"><span class="handle">${escapeHtml(handle)}</span><span class="title">${escapeHtml(item.title)}</span></span><p class="why" id="row-history-${escapeHtml(anchor)}-detail"><span class="muted">${escapeHtml(item.status)} \u00b7 terminal</span></p></a></li>`;
621
+ }).join('');
622
+ const unknown = model.unknownClasses.length === 0 ? '' : `<p class="notice">Unrecognised class values, ranked as standard: ${model.unknownClasses.map((entry) => `${escapeHtml(entry.value)} (${entry.numbers.map((number) => `#${number}`).join(', ')})`).join('; ')}</p>`;
623
+ return `${unknown}<ol id="item-list" class="ranked">${rows}</ol>${historyRows === '' ? '' : `<h3>History</h3><ol id="item-history" class="plain">${historyRows}</ol>`}`;
438
624
  }
439
625
 
440
626
  export function renderReportHtml(model, { logoDataUrl = null, graphBundle } = {}) {
441
627
  const graph = buildGraphModel(model);
442
- const fieldNames = [...new Set(model.items.flatMap((item) => Object.keys(item.fields)))].sort();
628
+ const retainedItems = [...model.items, ...model.terminalItems];
629
+ const selectionItems = retainedItems.map((item, index) => ({
630
+ id: item.id,
631
+ number: item.number,
632
+ title: item.title,
633
+ status: item.status,
634
+ kind: item.kind,
635
+ priority: item.priority,
636
+ created: item.created,
637
+ terminalDate: item.terminalDate,
638
+ decisions: (item.decisions ?? []).map((decision) => ({ action: decision.action, date: decision.date })),
639
+ readiness: { state: item.readiness.state },
640
+ fields: item.fields,
641
+ order: index,
642
+ }));
643
+ const workNextIds = model.workNext.map((entry) => entry.id);
644
+ const workNextById = Object.fromEntries(model.workNext.map((entry) => [entry.id, { reasons: entry.reasons, number: entry.number, title: entry.title }]));
645
+ const reportDataJson = scriptJson({ items: selectionItems, workNextIds, workNextById, impactById: model.impactById ?? {} });
646
+ const fieldNames = [...new Set(retainedItems.flatMap((item) => Object.keys(item.fields)))].sort();
443
647
  const groupOptions = [
444
648
  ['none', 'No grouping'],
445
649
  ['readiness', 'Readiness'],
@@ -453,7 +657,7 @@ export function renderReportHtml(model, { logoDataUrl = null, graphBundle } = {}
453
657
  ['title', 'Title'],
454
658
  ...fieldNames.map((name) => [`field:${name}`, fieldLabel(name)]),
455
659
  ];
456
- const facets = facetGroups(model.items, fieldNames);
660
+ const facets = facetGroups(retainedItems, fieldNames);
457
661
  const logo = logoDataUrl === null ? '' : `<img class="logo" src="${escapeHtml(logoDataUrl)}" alt="">`;
458
662
  const stats = [
459
663
  ['Total', model.stats.total],
@@ -467,19 +671,18 @@ export function renderReportHtml(model, { logoDataUrl = null, graphBundle } = {}
467
671
  const interactionRuntime = reportClientSource().replaceAll('</script', '<\\/script');
468
672
  const graphRuntime = graphClientSource(graph).replaceAll('</script', '<\\/script');
469
673
 
470
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'"><title>${escapeHtml(model.title)}</title><style>${styleSource()}${graphStyleSource()}.history-toggle{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer}.controls .history-toggle input{min-width:0;flex:none;margin:0;padding:0}${viewContextStyleSource(model.view)}</style></head><body data-richness="standard"><main class="shell">
674
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src 'none'; img-src data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'"><title>${escapeHtml(model.title)}</title><style>${styleSource()}${graphStyleSource()}.history-toggle{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer}.controls .history-toggle input{min-width:0;flex:none;margin:0;padding:0}${viewContextStyleSource(model.view)}</style></head><body data-richness="standard" data-section="items"><main class="shell">
471
675
  <header class="masthead"><div class="identity">${logo}<div><p class="eyebrow">${escapeHtml(model.repository.name)}</p><h1>${escapeHtml(model.title)}</h1></div></div><div class="as-of">Ledger state<br><strong>${escapeHtml(model.asOf)}</strong></div></header>${renderViewContext(model.view, model.repository.name)}
472
- ${renderWorkNext(model.workNext, model.unknownClasses)}
473
- ${renderAttention(model.attention, model.itemNumbers)}
474
- ${renderEvidence(model.evidence, model.asOf)}
475
- ${graphSection(graph, graphBundle.manifest, model.view)}
476
- <section id="drilldown"><div class="section-heading"><div><p class="eyebrow">Drill-down</p><h2>Every item</h2></div><p class="muted">State counts and item detail below the decision surface.</p></div>
676
+ <nav class="view-nav" aria-label="Report sections"><button type="button" id="nav-items" data-section="items" aria-pressed="true">Items</button><button type="button" id="nav-flow" data-section="flow" aria-pressed="false">Flow</button><button type="button" id="nav-dependencies" data-section="dependencies" aria-pressed="false">Dependencies</button></nav><noscript><p>Sections: <a href="#section-items">Items</a> · <a href="#section-flow">Flow</a> · <a href="#section-dependencies">Dependencies</a>. Fixed scope: all retained items; filters require scripting.</p></noscript>
677
+ <section id="section-items" data-section="items"><div class="section-heading"><div><p class="eyebrow">Workspace</p><h2>Items</h2></div><p class="muted">One canonical list and one detail per retained item. Scope narrows summaries, flow, and graph; quick views change only the list.</p></div>
477
678
  <section class="stats" aria-label="Ledger summary">${stats.map(([label, value]) => `<div class="stat"><strong>${escapeHtml(value)}</strong><span>${label}</span></div>`).join('')}</section>
478
- <section class="controls panel" aria-label="Report controls"><label class="sr-only" for="search">Search items</label><input id="search" type="search" placeholder="Search ID, number, title, or mapped fields"><select id="group-by" aria-label="Group items">${groupOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}</select><select id="sort-by" aria-label="Sort items">${sortOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}</select><select id="richness" aria-label="Detail level"><option value="basic">Basic</option><option value="standard" selected>Standard</option><option value="detailed">Detailed</option></select><label class="history-toggle"><input id="show-history" type="checkbox" checked>Show history</label><button type="button" id="expand-all">Expand all</button><button type="button" id="collapse-all">Collapse all</button></section>
479
- ${renderFacets(facets, model.items.length)}
480
- <section><div class="section-heading"><div><p class="eyebrow">Current work</p><h2>Ledger items</h2></div><p class="muted">Priority, then created date, then immutable ID.</p></div><div id="items" class="items">${model.items.map((item, index) => renderCard(item, index, model.itemNumbers)).join('')}</div>${renderEmptyItems(model)}</section>
481
- ${renderSwarm(model.swarmBatches)}
679
+ <div id="sticky-controls" class="sticky-controls controls panel" aria-label="Scope and display controls"><label class="sr-only" for="search">Search items</label><input id="search" type="search" placeholder="Search ID, number, title, or mapped fields">${renderQuickViews()}<details id="display-expand" class="display-expand"><summary>Display</summary><div id="items-presentation"><select id="group-by" aria-label="Group items">${groupOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}</select><select id="sort-by" aria-label="Sort items">${sortOptions.map(([value, label]) => `<option value="${escapeHtml(value)}">${escapeHtml(label)}</option>`).join('')}</select><select id="richness" aria-label="Detail level"><option value="basic">Basic</option><option value="standard" selected>Standard</option><option value="detailed">Detailed</option></select><label class="history-toggle"><input id="show-history" type="checkbox" checked>Show history</label><button type="button" id="expand-all">Expand all</button><button type="button" id="collapse-all">Collapse all</button></div></details><div id="active-scope" aria-live="polite"></div></div>
680
+ ${renderFacets(facets, retainedItems.length)}
681
+ <section aria-label="Item browser"><div class="section-heading"><div><p class="eyebrow">Current work</p><h2>Ledger items</h2></div><p class="muted">Summaries, matrix, and batches follow the current scope. Work next keeps its rank and reasons.</p></div>${renderAttentionSummary(model.attention, model.items)}${renderAreaMatrix(model.items, model.itemNumbers)}${renderCoverage(model.fieldCoverage ?? [])}<div id="drilldown-pill" hidden><span id="drilldown-label"></span> <button type="button" id="clear-drilldown">Clear drilldown</button></div><div id="workspace-split"><div id="list-column">${renderItemList(model)}${model.view === null || model.items.length > 0 || model.terminalItems.length > 0 ? `<p id="empty" class="empty" hidden>No items match these filters.</p><p id="history-explain" class="empty" hidden>A terminal status is selected while history is hidden. <button type="button" id="show-history-action">Show history</button></p>` : `<p id="empty" class="empty">No ledger item matches this view's criteria.</p><p id="history-explain" class="empty" hidden>A terminal status is selected while history is hidden. <button type="button" id="show-history-action">Show history</button></p>`}</div><div id="detail-column"><div id="items" class="items">${(() => { const retainedIds = new Set([...model.items, ...model.terminalItems].map((item) => item.id)); return retainedItems.map((item, index) => renderCard(item, index, model.itemNumbers, retainedIds, model.impactById ?? {})).join(''); })()}</div><div id="detail-pane" aria-live="polite"></div></div></div>${renderSwarm(model.swarmBatches, model.swarm)}
682
+ </section>
482
683
  </section>
483
- <section id="history" class="panel"><div class="section-heading"><div><p class="eyebrow">History</p><h2>Terminal items</h2></div><p class="muted">Completed, killed, deferred, and archived.</p></div>${renderTerminalTable(model.terminalItems)}</section>
684
+ <section id="section-flow" data-section="flow" data-asof="${escapeHtml(model.asOf)}" hidden><div class="section-heading"><div><p class="eyebrow">Flow</p><h2>Flow</h2></div><p class="muted">Scoped cumulative flow, arrivals, closures, age, durations, and forecast.</p></div><p class="scope-caption" id="flow-scope"></p><div class="panel" id="flow-controls"><label for="flow-from">From</label><input id="flow-from" type="date" value="${escapeHtml(flowDefaultFrom(model))}" max="${escapeHtml(model.asOf)}"><label for="flow-to">To</label><input id="flow-to" type="date" value="${escapeHtml(model.asOf)}" max="${escapeHtml(model.asOf)}"><p id="flow-error" role="alert" hidden></p></div><div id="flow-live" aria-live="polite"><p class="muted">Scoped flow charts load here when scripting is available. The fixed snapshot below remains for readers without scripting.</p></div>${renderEvidence(model.evidence, model.asOf)}</section>
685
+ <section id="section-dependencies" data-section="dependencies" hidden><div class="section-heading"><div><p class="eyebrow">Dependencies</p><h2>Dependencies</h2></div><p class="muted">Ledger graph and roster share the current scope.</p></div><p class="scope-caption" id="graph-scope"></p>${graphSection(graph, graphBundle.manifest, model.view)}</section>
686
+ <script id="report-data" type="application/json">${reportDataJson}</script>
484
687
  </main><script>${markdownRuntime}</script><script>${interactionRuntime}</script><script>${graphBundle.source}</script><script>${graphRuntime}</script></body></html>`;
485
688
  }