instar 1.3.810 → 1.3.811

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.
@@ -0,0 +1,533 @@
1
+ // Shared glance component — the three-layer template for the Dashboard UX Standard
2
+ // glance floors F10 (glance) + F11 (universal drill-down). Spec:
3
+ // docs/specs/dashboard-ux-standard.md ("The glance floors", topic 29836).
4
+ //
5
+ // Browser-native ESM (no build step; served at /dashboard/glance.js and imported by
6
+ // index.html on tab activation). The pure functions are exported so the three-tier
7
+ // jsdom tests exercise the SHIPPED code, not a copy.
8
+ //
9
+ // THE THREE LAYERS
10
+ // Layer 1 (glance) — one plain-English headline + ≤5 labeled tiles. 100%
11
+ // COMPONENT-AUTHORED: no agent/user free text ever reaches it.
12
+ // Layer 2 (list) — click a tile → the rows behind that number, in plain words.
13
+ // Layer 3 (record) — click a row → the full record (IDs, timestamps, raw detail).
14
+ //
15
+ // LOAD-BEARING SAFETY CONTRACT (mirrors dashboard/subscriptions.js): every dynamic
16
+ // value flows through sanitizeForDisplay before the DOM; ALL DOM writes are
17
+ // textContent only (never innerHTML); the only dynamic attributes are a fixed
18
+ // state→literal token and a numeric count. Agent/user free text lives at Layer 2/3
19
+ // where it is *displayed* through the sanitizer — it is never vocab-gated (F10's
20
+ // jargon check runs only over the component-authored Layer-1 strings).
21
+ //
22
+ // F9 COMPOSITION: while a drill interaction is open (the drill container carries
23
+ // data-interaction-open, or a field is focused/dirty) a background refresh MERGES
24
+ // live counts via patchGlanceCounts instead of rebuilding over the interaction —
25
+ // reusing the shipped hasOpenInteraction primitive.
26
+
27
+ import { sanitizeForDisplay, hasOpenInteraction } from './subscriptions.js';
28
+
29
+ export const GLANCE_MAX_TILES = 5;
30
+ export const GLANCE_WORD_BUDGET = 150; // words on the front page before interaction
31
+ export const GLANCE_MAX_TOKEN_LEN = 40; // a longer token is a glued-word budget dodge
32
+
33
+ // ── The glance-adopted / grandfathered registries (the ratchet) ──────────────
34
+ // A tab is ON the glance floor (F10/F11 apply) once it builds its glance through
35
+ // this component. GLANCE_ADOPTED_TABS grows as tabs migrate; GLANCE_GRANDFATHERED
36
+ // is every registered tab NOT yet on the floor, grandfathered against the survey
37
+ // scorecard (topic 29836). THE RATCHET: the grandfather list only shrinks — a tab
38
+ // leaves it only by adopting the floor (and passing F10/F11). Adding a tab here (or
39
+ // shipping a NEW tab grandfathered) requires raising GLANCE_GRANDFATHERED_CEILING,
40
+ // a visible committed change that needs a written justification + operator sign-off
41
+ // (same discipline as the F3 purpose-line exempt list). The completeness test
42
+ // asserts adopted ∪ grandfathered == every TAB_REGISTRY id, so a NEW tab in NEITHER
43
+ // set fails the build; the monotonicity test asserts the grandfather size ≤ ceiling.
44
+ export const GLANCE_ADOPTED_TABS = ['commitments'];
45
+
46
+ export const GLANCE_GRANDFATHERED = [
47
+ 'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features', 'systems',
48
+ 'integrated-being', 'pr-pipeline', 'projects', 'initiatives', 'tokens',
49
+ 'resources', 'llm-activity', 'routing-map', 'spend', 'threadline', 'evidence',
50
+ 'process-health', 'subscriptions', 'preferences-learning', 'machines', 'mandates',
51
+ 'blockers', 'secrets',
52
+ ];
53
+
54
+ // The committed ceiling on grandfathered-tab count. Only ever LOWER this (each
55
+ // lowering marks a tab retrofitted onto the floor). Never raise it without an
56
+ // operator-signed justification — raising it is how a NEW tab would silently ship
57
+ // below the floor, the exact regression the ratchet exists to prevent.
58
+ export const GLANCE_GRANDFATHERED_CEILING = 25;
59
+
60
+ // ── F10 — insider-vocab detection ────────────────────────────────────────────
61
+ // A readability floor, NOT a secret-redaction boundary (secret handling stays at
62
+ // the API/data layer). It scans ONLY component-authored Layer-1 strings (headline +
63
+ // tile labels + tile values), so it can never blank the glance on user free text.
64
+
65
+ // Concept-jargon the form heuristics can't catch (curated; extend as jargon is
66
+ // found). Matched case-insensitively as whole words/phrases over normalized text.
67
+ const INSIDER_TERM_DENYLIST = [
68
+ 'atrisk', 'at risk', 'at-risk', 'suppressed', 'beacon', 'beacons',
69
+ 'beaconenabled', 'beaconsuppressed', 'cadence', 'heartbeat', 'heartbeats',
70
+ 'lane', 'reflow', 'ttl', 'slo', 'sla', 'mrr', 'paid door', 'money-gated',
71
+ 'quiet-hours', 'quiet hours',
72
+ ];
73
+
74
+ // Internal IDs: a letter-run glued or hyphen/underscore-joined to 3+ digits
75
+ // (CMT-953, CMT_953, cmt953) — separator-agnostic, case-insensitive, NOT
76
+ // space-separated (so a quantity like "664 open promises" is never flagged).
77
+ const ID_RE = /[a-z]{2,}[-_]?\d{3,}/i;
78
+ // An all-caps prefix + optional space/sep + 3+ digits (CMT 953 / CMT-953) — safe
79
+ // because component-authored plain copy never writes an ALLCAPS token beside a number.
80
+ const ALLCAPS_ID_RE = /\b[A-Z]{2,6}[-_ ]?\d{3,}\b/;
81
+ // Machine/agent hex ids: m_<hex>, agent_<hex>, machine-<hex-with-digit>.
82
+ const HEX_ID_RE = /\b(?:[a-z]{1,}_[0-9a-f]{4,}|m_[0-9a-f]{4,}|[a-z]{2,}-[0-9a-f]*\d[0-9a-f]*)\b/i;
83
+ // Config keys: a camelCase transition (softDeadlineAt) or a snake_case token.
84
+ const CAMEL_RE = /\b[a-z][a-z0-9]*[A-Z][a-zA-Z0-9]*\b/;
85
+ const SNAKE_RE = /\b[a-z0-9]+_[a-z0-9]+\b/i;
86
+ // Machine-duration cadences: a bare number glued/spaced to a time unit — 1800s,
87
+ // 1800 s, 1800sec, 1800000ms, PT30M — EXCLUDING 4-digit year/decade prose ("1800s"
88
+ // meaning the 1800s decade is excluded via the decade guard below).
89
+ const CADENCE_RE = /\b\d{1,9}\s?(?:ms|milliseconds?|secs?|seconds?|s)\b|\bPT\d+[HMSD]\b/i;
90
+ const DECADE_RE = /^(?:1[5-9]\d0|20[0-4]\d)s$/i; // 1500s..1990s, 2000s..2049s
91
+
92
+ /**
93
+ * Return the insider-vocabulary hits in a component-authored string. Empty array =
94
+ * clean. Each hit is { type, match }. NFKC-normalized + case-insensitive so
95
+ * look-alike glyphs and case tricks can't dodge the check.
96
+ */
97
+ export function findInsiderVocab(text) {
98
+ const norm = String(text == null ? '' : text).normalize('NFKC');
99
+ const lower = norm.toLowerCase();
100
+ const hits = [];
101
+
102
+ const id = norm.match(ID_RE) || norm.match(ALLCAPS_ID_RE);
103
+ if (id) hits.push({ type: 'internal-id', match: id[0] });
104
+ const hex = norm.match(HEX_ID_RE);
105
+ if (hex) hits.push({ type: 'machine-id', match: hex[0] });
106
+ const camel = norm.match(CAMEL_RE);
107
+ if (camel) hits.push({ type: 'config-key', match: camel[0] });
108
+ const snake = norm.match(SNAKE_RE);
109
+ if (snake) hits.push({ type: 'config-key', match: snake[0] });
110
+
111
+ for (const m of lower.matchAll(new RegExp(CADENCE_RE, 'gi'))) {
112
+ const tok = m[0].replace(/\s+/g, '');
113
+ // "1800s" is ambiguous (1800 seconds vs the 1800s decade). Only treat it as a
114
+ // decade — and skip — when it reads as decade PROSE (preceded by "the"/"in").
115
+ if (DECADE_RE.test(tok)) {
116
+ const before = lower.slice(Math.max(0, m.index - 8), m.index);
117
+ if (/\b(?:the|in|early|late|mid)\s+$/.test(before)) continue;
118
+ }
119
+ hits.push({ type: 'cadence', match: m[0].trim() });
120
+ }
121
+
122
+ for (const term of INSIDER_TERM_DENYLIST) {
123
+ // Word/phrase boundary so "beacon" matches but "beaconing-signal-lantern" as a
124
+ // whole is still caught by the substring intent; keep it simple + robust.
125
+ const re = new RegExp(`(?:^|[^a-z0-9])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:$|[^a-z0-9])`, 'i');
126
+ if (re.test(lower)) hits.push({ type: 'insider-term', match: term });
127
+ }
128
+ return hits;
129
+ }
130
+
131
+ /**
132
+ * Tokenize Layer-1 copy for the word budget: split on whitespace and structural
133
+ * punctuation (hyphen / underscore / slash / common separators) so a glued
134
+ * "carrying-664-open-cmt953" cannot pose as one word. A token longer than
135
+ * GLANCE_MAX_TOKEN_LEN is itself a budget dodge and is reported separately.
136
+ */
137
+ export function tokenizeGlance(text) {
138
+ return String(text == null ? '' : text)
139
+ .normalize('NFKC')
140
+ .split(/[\s\-_/.,;:·|()[\]{}]+/)
141
+ .map((t) => t.trim())
142
+ .filter(Boolean);
143
+ }
144
+
145
+ export function countGlanceWords(text) {
146
+ return tokenizeGlance(text).length;
147
+ }
148
+
149
+ /** The full component-authored Layer-1 text: headline + every tile label + value. */
150
+ export function glanceText(spec) {
151
+ const parts = [String(spec?.headline ?? '')];
152
+ for (const t of spec?.tiles ?? []) {
153
+ parts.push(String(t?.label ?? ''));
154
+ parts.push(String(t?.value ?? ''));
155
+ }
156
+ return parts.join(' ');
157
+ }
158
+
159
+ /**
160
+ * F10 validator — the shared component refuses to build a glance that breaks the
161
+ * budget or carries jargon. Returns { ok, violations: [{code, detail}] }. Scans the
162
+ * concatenation of headline + every tile label + every tile value (all
163
+ * component-authored) — there is no free-text hole to hide jargon in.
164
+ */
165
+ export function validateGlanceSpec(spec) {
166
+ const violations = [];
167
+ const tiles = Array.isArray(spec?.tiles) ? spec.tiles : [];
168
+
169
+ if (!spec || typeof spec.headline !== 'string' || spec.headline.trim() === '') {
170
+ violations.push({ code: 'no-headline', detail: 'a glance needs one plain-English headline sentence' });
171
+ }
172
+ if (tiles.length > GLANCE_MAX_TILES) {
173
+ violations.push({ code: 'too-many-tiles', detail: `${tiles.length} tiles > max ${GLANCE_MAX_TILES}` });
174
+ }
175
+
176
+ const text = glanceText(spec);
177
+ const words = countGlanceWords(text);
178
+ if (words > GLANCE_WORD_BUDGET) {
179
+ violations.push({ code: 'over-budget', detail: `${words} words > budget ${GLANCE_WORD_BUDGET}` });
180
+ }
181
+ for (const tok of tokenizeGlance(text)) {
182
+ if (tok.length > GLANCE_MAX_TOKEN_LEN) {
183
+ violations.push({ code: 'glued-token', detail: `"${tok.slice(0, 24)}…" (${tok.length} chars) evades the word count` });
184
+ break;
185
+ }
186
+ }
187
+
188
+ const jargon = findInsiderVocab(text);
189
+ for (const hit of jargon) {
190
+ violations.push({ code: 'insider-vocab', detail: `${hit.type}: "${hit.match}"` });
191
+ }
192
+
193
+ return { ok: violations.length === 0, violations };
194
+ }
195
+
196
+ // ── Rendering ────────────────────────────────────────────────────────────────
197
+
198
+ function el(doc, tag, cls, text) {
199
+ const node = doc.createElement(tag);
200
+ if (cls) node.className = cls;
201
+ if (text != null) node.textContent = sanitizeForDisplay(text, 'label');
202
+ return node;
203
+ }
204
+
205
+ /**
206
+ * Render the three-layer glance into `root` from `spec`:
207
+ * spec = { headline, tiles: [{ key, label, value, tone?, onActivate?(ctx) }] }
208
+ * Honors F9: if an interaction is open under `root`, MERGE live counts instead of
209
+ * rebuilding (patchGlanceCounts). On a spec that fails validation renders an HONEST
210
+ * DEGRADED glance (truncated headline + a "See details" drill) — NEVER a raw-record
211
+ * fallback. Returns a handle { root, headline, tiles, drilldown, spec }.
212
+ */
213
+ export function renderGlance(doc, root, spec, opts = {}) {
214
+ if (!doc || !root) return null;
215
+
216
+ // F9 merge arm: an open drill / focused / dirty interaction holds the DOM.
217
+ if (root.querySelector('[data-glance-layer]') && hasOpenInteraction(doc, root)) {
218
+ patchGlanceCounts(doc, root, spec);
219
+ return { root, held: true, spec };
220
+ }
221
+
222
+ const { ok, violations } = validateGlanceSpec(spec);
223
+ if (!ok && typeof console !== 'undefined' && console.warn) {
224
+ console.warn('[glance] spec failed F10 validation — rendering honest degraded glance:', violations);
225
+ }
226
+
227
+ // Replace, don't append — repeated renders never leak detached DOM/listeners.
228
+ root.replaceChildren();
229
+
230
+ const layer = el(doc, 'div', 'glance-layer');
231
+ layer.setAttribute('data-glance-layer', '');
232
+
233
+ const headline = el(doc, 'div', 'glance-headline');
234
+ headline.setAttribute('data-glance-headline', '');
235
+ // Degraded mode: truncate to budget, never dump raw records.
236
+ const headlineText = ok
237
+ ? String(spec?.headline ?? '')
238
+ : truncateToWords(String(spec?.headline ?? 'Details available'), GLANCE_WORD_BUDGET);
239
+ headline.textContent = sanitizeForDisplay(headlineText, 'summary');
240
+ layer.appendChild(headline);
241
+
242
+ const drilldown = doc.createElement('section');
243
+ drilldown.className = 'glance-drilldown';
244
+ drilldown.setAttribute('data-glance-drilldown', '');
245
+ drilldown.hidden = true;
246
+
247
+ const tilesWrap = el(doc, 'div', 'glance-tiles');
248
+ tilesWrap.setAttribute('data-glance-tiles', '');
249
+ const tiles = Array.isArray(spec?.tiles) ? spec.tiles : [];
250
+ const tileNodes = [];
251
+ const usableTiles = ok ? tiles.slice(0, GLANCE_MAX_TILES) : [];
252
+ for (const tile of usableTiles) {
253
+ const btn = doc.createElement('button');
254
+ btn.type = 'button';
255
+ btn.className = 'glance-tile';
256
+ btn.setAttribute('data-glance-tile', String(tile.key ?? tile.label ?? ''));
257
+ if (tile.tone) btn.setAttribute('data-tone', String(tile.tone));
258
+ btn.setAttribute('aria-expanded', 'false');
259
+
260
+ const valEl = el(doc, 'span', 'glance-tile-value');
261
+ valEl.setAttribute('data-glance-count', '');
262
+ valEl.textContent = sanitizeForDisplay(String(tile.value ?? ''), 'label');
263
+ const labelEl = el(doc, 'span', 'glance-tile-label', String(tile.label ?? ''));
264
+
265
+ // Accessible label (F5) — never an icon-only/bare control.
266
+ btn.setAttribute('aria-label', `${sanitizeForDisplay(String(tile.label ?? ''), 'label')}: ${sanitizeForDisplay(String(tile.value ?? ''), 'label')}`);
267
+ btn.appendChild(valEl);
268
+ btn.appendChild(labelEl);
269
+
270
+ btn.addEventListener('click', () => openDrill(doc, root, drilldown, btn, tile, tileNodes));
271
+ tilesWrap.appendChild(btn);
272
+ tileNodes.push(btn);
273
+ }
274
+
275
+ // Degraded fallback: one honest "See details" affordance, never the raw dump.
276
+ if (!ok) {
277
+ const btn = doc.createElement('button');
278
+ btn.type = 'button';
279
+ btn.className = 'glance-tile glance-tile-degraded';
280
+ btn.setAttribute('data-glance-tile', '__details__');
281
+ btn.setAttribute('aria-expanded', 'false');
282
+ btn.appendChild(el(doc, 'span', 'glance-tile-label', 'See details'));
283
+ btn.addEventListener('click', () => openDrill(doc, root, drilldown, btn, {
284
+ key: '__details__',
285
+ onActivate: (ctx) => { ctx.drilldown.appendChild(el(doc, 'div', 'glance-empty', 'Details are being prepared.')); },
286
+ }, tileNodes));
287
+ tilesWrap.appendChild(btn);
288
+ tileNodes.push(btn);
289
+ }
290
+
291
+ layer.appendChild(tilesWrap);
292
+ root.appendChild(layer);
293
+ root.appendChild(drilldown);
294
+
295
+ return { root, headline, tiles: tileNodes, drilldown, spec };
296
+ }
297
+
298
+ function truncateToWords(text, max) {
299
+ const toks = tokenizeGlance(text);
300
+ if (toks.length <= max) return text;
301
+ return toks.slice(0, max).join(' ') + '…';
302
+ }
303
+
304
+ /**
305
+ * Open a tile's drill (Layer 2). Replaces the drill container (never appends),
306
+ * calls the tile's onActivate to populate it, reveals it, and marks it
307
+ * data-interaction-open so a background refresh HOLDS it (F9). Clicking the same
308
+ * tile again (or the Back control) releases the hold. onActivate receives:
309
+ * { doc, drilldown, tile, openRecord } — openRecord(node) swaps in a Layer-3 record.
310
+ */
311
+ function openDrill(doc, root, drilldown, btn, tile, allTiles) {
312
+ const alreadyOpen = drilldown.getAttribute('data-open-tile') === btn.getAttribute('data-glance-tile') && !drilldown.hidden;
313
+ // Collapse any open tile first.
314
+ for (const t of allTiles) t.setAttribute('aria-expanded', 'false');
315
+ drilldown.replaceChildren();
316
+ drilldown.removeAttribute('data-interaction-open');
317
+
318
+ if (alreadyOpen) {
319
+ drilldown.hidden = true;
320
+ drilldown.removeAttribute('data-open-tile');
321
+ return;
322
+ }
323
+
324
+ const header = el(doc, 'div', 'glance-drill-header');
325
+ const back = doc.createElement('button');
326
+ back.type = 'button';
327
+ back.className = 'glance-drill-back';
328
+ back.setAttribute('aria-label', 'Back to the glance');
329
+ back.textContent = '← Back';
330
+ back.addEventListener('click', () => {
331
+ drilldown.replaceChildren();
332
+ drilldown.hidden = true;
333
+ drilldown.removeAttribute('data-interaction-open');
334
+ drilldown.removeAttribute('data-open-tile');
335
+ btn.setAttribute('aria-expanded', 'false');
336
+ });
337
+ header.appendChild(back);
338
+ const title = el(doc, 'span', 'glance-drill-title', tile.label != null ? String(tile.label) : 'Details');
339
+ header.appendChild(title);
340
+ drilldown.appendChild(header);
341
+
342
+ const body = el(doc, 'div', 'glance-drill-body');
343
+ body.setAttribute('data-glance-drill-body', '');
344
+ drilldown.appendChild(body);
345
+
346
+ const openRecord = (node) => {
347
+ // Layer 3: swap the list for the full record, with a Back-to-list control.
348
+ const recWrap = el(doc, 'div', 'glance-record');
349
+ recWrap.setAttribute('data-glance-record', '');
350
+ const toList = doc.createElement('button');
351
+ toList.type = 'button';
352
+ toList.className = 'glance-drill-back';
353
+ toList.setAttribute('aria-label', 'Back to the list');
354
+ toList.textContent = '← Back to list';
355
+ const priorList = Array.from(body.childNodes);
356
+ toList.addEventListener('click', () => {
357
+ body.replaceChildren(...priorList);
358
+ });
359
+ recWrap.appendChild(toList);
360
+ if (node) recWrap.appendChild(node);
361
+ body.replaceChildren(recWrap);
362
+ };
363
+
364
+ try {
365
+ if (typeof tile.onActivate === 'function') {
366
+ tile.onActivate({ doc, drilldown: body, tile, openRecord });
367
+ }
368
+ } catch (err) {
369
+ // A drill builder that throws must not white-screen the tab: show an honest
370
+ // error state, never a raw dump. @silent-fallback-ok — degraded drill, logged.
371
+ body.replaceChildren(el(doc, 'div', 'glance-empty', 'Could not load these details right now.'));
372
+ if (typeof console !== 'undefined' && console.warn) console.warn('[glance] drill onActivate failed:', err);
373
+ }
374
+
375
+ // An honest F6 empty-state if the drill produced nothing (e.g. a zero-count tile).
376
+ if (body.childNodes.length === 0) {
377
+ body.appendChild(el(doc, 'div', 'glance-empty', 'Nothing here right now.'));
378
+ }
379
+
380
+ drilldown.setAttribute('data-open-tile', btn.getAttribute('data-glance-tile') || '');
381
+ drilldown.setAttribute('data-interaction-open', 'glance-drill');
382
+ drilldown.hidden = false;
383
+ btn.setAttribute('aria-expanded', 'true');
384
+ }
385
+
386
+ /**
387
+ * F9 merge arm — patch the live tile counts (and headline) from a fresh spec
388
+ * WITHOUT rebuilding the DOM, so an open drill interaction is never clobbered.
389
+ * Returns the number of tiles patched.
390
+ */
391
+ export function patchGlanceCounts(doc, root, spec) {
392
+ if (!root || !spec) return 0;
393
+ let patched = 0;
394
+ const headline = root.querySelector('[data-glance-headline]');
395
+ if (headline && typeof spec.headline === 'string') {
396
+ headline.textContent = sanitizeForDisplay(spec.headline, 'summary');
397
+ }
398
+ for (const tile of spec.tiles ?? []) {
399
+ const key = String(tile.key ?? tile.label ?? '');
400
+ const btn = root.querySelector(`[data-glance-tile="${cssEscape(key)}"]`);
401
+ if (!btn) continue;
402
+ const val = btn.querySelector('[data-glance-count]');
403
+ if (val) { val.textContent = sanitizeForDisplay(String(tile.value ?? ''), 'label'); patched++; }
404
+ }
405
+ return patched;
406
+ }
407
+
408
+ function cssEscape(s) {
409
+ if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(s);
410
+ return String(s).replace(/["\\\]]/g, '\\$&');
411
+ }
412
+
413
+ // ── Commitments reference builder (the Phase-1 living example) ────────────────
414
+ // Pure: turns the /commitments open-promises list into a glance spec. Derives every
415
+ // tile + the headline from ONE population — the beacon-watched open promises
416
+ // (beaconEnabled && status==='pending'), the identical set the drill-down shows — so
417
+ // the headline count EQUALS the Layer-2 list length by construction, and each tile
418
+ // maps to an EXISTING server field (no client-side state re-derivation).
419
+
420
+ /** The single population: beacon-watched open promises. */
421
+ export function commitmentsOpenPopulation(commitments) {
422
+ return (Array.isArray(commitments) ? commitments : [])
423
+ .filter((c) => c && c.beaconEnabled && c.status === 'pending');
424
+ }
425
+
426
+ export function buildCommitmentsGlance(commitments, now = Date.now()) {
427
+ const open = commitmentsOpenPopulation(commitments);
428
+ const dueSoon = open.filter((c) => c.atRisk === true);
429
+ const waiting = open.filter((c) => c.blockedOn === 'user-input' || c.blockedOn === 'user-authorization');
430
+ const quiet = open.filter((c) => c.beaconSuppressed === true);
431
+ const overdue = open.filter((c) => c.hardDeadlineAt && Date.parse(c.hardDeadlineAt) < now);
432
+
433
+ // Component-authored, jargon-free headline — honest to the one population.
434
+ let headline;
435
+ if (open.length === 0) {
436
+ headline = "You have no open promises right now.";
437
+ } else {
438
+ const soonClause = dueSoon.length > 0 ? `${dueSoon.length} need attention soon` : 'none need attention soon';
439
+ const overdueClause = overdue.length > 0 ? `${overdue.length} overdue` : 'none overdue';
440
+ const noun = open.length === 1 ? 'open promise' : 'open promises';
441
+ headline = `I'm carrying ${open.length} ${noun}; ${soonClause}, ${overdueClause}.`;
442
+ }
443
+
444
+ const tiles = [
445
+ { key: 'open', label: 'Open', value: String(open.length), tone: 'neutral', rows: open },
446
+ { key: 'due-soon', label: 'Due soon', value: String(dueSoon.length), tone: dueSoon.length ? 'warn' : 'neutral', rows: dueSoon },
447
+ { key: 'waiting', label: 'Waiting on you', value: String(waiting.length), tone: waiting.length ? 'warn' : 'neutral', rows: waiting },
448
+ { key: 'quiet', label: 'Quiet', value: String(quiet.length), tone: 'muted', rows: quiet },
449
+ ];
450
+
451
+ return { headline, tiles, population: open };
452
+ }
453
+
454
+ /** One plain-word Layer-2 row for a commitment (no IDs/cadences — those are Layer 3). */
455
+ export function commitmentRowText(c) {
456
+ const summary = sanitizeForDisplay(c.agentResponse || c.userRequest || 'A promise', 'summary');
457
+ return summary;
458
+ }
459
+
460
+ function defaultFmtTs(iso) {
461
+ if (!iso) return '—';
462
+ try { return new Date(iso).toLocaleString(); } catch { return String(iso); }
463
+ }
464
+
465
+ /**
466
+ * Layer-3 full record for a commitment — the raw detail (IDs, cadence, deadlines)
467
+ * lives HERE, one click below the plain Layer-2 row. All values via textContent
468
+ * (XSS-safe); this is where insider fields legitimately appear. Optional onDeliver
469
+ * wires the existing "Mark delivered" action onto the record.
470
+ */
471
+ export function commitmentRecordNode(doc, c, opts = {}) {
472
+ const fmtTs = opts.fmtTs || defaultFmtTs;
473
+ const wrap = el(doc, 'div', 'glance-record-fields');
474
+ const rows = [
475
+ ['Promise', c.agentResponse || c.userRequest || '—'],
476
+ ['id', c.id || '—'],
477
+ ['topic', c.topicId != null ? String(c.topicId) : '—'],
478
+ ['cadence', c.cadenceMs ? `${Math.round(c.cadenceMs / 1000)}s` : '—'],
479
+ ['heartbeats', String(c.heartbeatCount ?? 0)],
480
+ ['last heartbeat', fmtTs(c.lastHeartbeatAt)],
481
+ ['soft deadline', fmtTs(c.softDeadlineAt)],
482
+ ['hard deadline', fmtTs(c.hardDeadlineAt)],
483
+ ];
484
+ for (const [k, v] of rows) {
485
+ const row = el(doc, 'div', 'glance-record-row');
486
+ row.appendChild(el(doc, 'span', 'glance-record-key', String(k)));
487
+ row.appendChild(el(doc, 'span', 'glance-record-val', String(v)));
488
+ wrap.appendChild(row);
489
+ }
490
+ if (typeof opts.onDeliver === 'function' && c.id) {
491
+ const btn = doc.createElement('button');
492
+ btn.type = 'button';
493
+ btn.className = 'glance-record-action';
494
+ btn.textContent = 'Mark delivered';
495
+ btn.addEventListener('click', () => { btn.disabled = true; opts.onDeliver(c.id); });
496
+ wrap.appendChild(btn);
497
+ }
498
+ return wrap;
499
+ }
500
+
501
+ /**
502
+ * Build the FULL Commitments glance spec with drill wiring — the reference
503
+ * implementation, importable by index.html AND the three test tiers. Each tile's
504
+ * onActivate renders the filtered open-promises as plain Layer-2 rows; each row
505
+ * opens the Layer-3 record. Population + counts come from buildCommitmentsGlance
506
+ * (one denominator, honest counts).
507
+ */
508
+ export function commitmentsGlanceSpec(doc, commitments, opts = {}) {
509
+ const now = opts.now ?? Date.now();
510
+ const base = buildCommitmentsGlance(commitments, now);
511
+ const tiles = base.tiles.map((t) => ({
512
+ key: t.key,
513
+ label: t.label,
514
+ value: t.value,
515
+ tone: t.tone,
516
+ onActivate: ({ doc: d, drilldown, openRecord }) => {
517
+ const rows = t.rows || [];
518
+ if (rows.length === 0) return; // component renders the honest F6 empty-state
519
+ const list = el(d, 'div', 'glance-list');
520
+ for (const c of rows) {
521
+ const row = d.createElement('button');
522
+ row.type = 'button';
523
+ row.className = 'glance-list-row';
524
+ row.setAttribute('aria-label', 'Open the full record');
525
+ row.appendChild(el(d, 'span', 'glance-list-summary', commitmentRowText(c)));
526
+ row.addEventListener('click', () => openRecord(commitmentRecordNode(d, c, { onDeliver: opts.onDeliver })));
527
+ list.appendChild(row);
528
+ }
529
+ drilldown.appendChild(list);
530
+ },
531
+ }));
532
+ return { headline: base.headline, tiles, population: base.population };
533
+ }
@@ -593,6 +593,71 @@
593
593
  max-width: 900px;
594
594
  }
595
595
 
596
+ /* ── Glance component (Dashboard UX Standard F10/F11, topic 29836) ──
597
+ The shared three-layer template rendered by dashboard/glance.js: a
598
+ plain-English headline + ≤5 labeled tiles (Layer 1), each drilling into a
599
+ list (Layer 2) then a full record (Layer 3). Theme-consistent + mobile
600
+ responsive (tiles/records reflow; nothing scrolls the page sideways). */
601
+ .glance-root { display: flex; flex-direction: column; gap: 16px; }
602
+ .glance-layer { display: flex; flex-direction: column; gap: 16px; }
603
+ .glance-headline {
604
+ font-size: 20px; font-weight: 600; line-height: 1.4;
605
+ color: var(--text-bright); max-width: 900px;
606
+ }
607
+ .glance-tiles {
608
+ display: grid; gap: 12px;
609
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
610
+ }
611
+ .glance-tile {
612
+ display: flex; flex-direction: column; gap: 4px; align-items: flex-start;
613
+ padding: 16px; border: 1px solid var(--border); border-radius: 10px;
614
+ background: var(--bg-panel); color: var(--text); cursor: pointer;
615
+ text-align: left; min-width: 0;
616
+ transition: background .15s, border-color .15s;
617
+ }
618
+ .glance-tile:hover { background: var(--bg-hover); border-color: var(--accent-dim); }
619
+ .glance-tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
620
+ .glance-tile[aria-expanded="true"] { border-color: var(--accent); background: var(--bg-active); }
621
+ .glance-tile-value { font-size: 28px; font-weight: 700; color: var(--text-bright); }
622
+ .glance-tile-label { font-size: 13px; color: var(--text-dim); }
623
+ .glance-tile[data-tone="warn"] .glance-tile-value { color: var(--orange); }
624
+ .glance-tile[data-tone="muted"] .glance-tile-value { color: var(--text-dim); }
625
+ .glance-tile-degraded { justify-content: center; }
626
+ .glance-tile-degraded .glance-tile-label { color: var(--text); font-weight: 600; }
627
+ .glance-drilldown {
628
+ border: 1px solid var(--border); border-radius: 10px; background: var(--bg-panel);
629
+ padding: 16px; display: flex; flex-direction: column; gap: 12px;
630
+ }
631
+ .glance-drilldown[hidden] { display: none; }
632
+ .glance-drill-header { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
633
+ .glance-drill-title { font-weight: 600; color: var(--text-bright); }
634
+ .glance-drill-back {
635
+ padding: 4px 10px; border: 1px solid var(--border); border-radius: 6px;
636
+ background: var(--bg-hover); color: var(--text); cursor: pointer; font-size: 12px;
637
+ }
638
+ .glance-drill-back:hover { border-color: var(--accent-dim); }
639
+ .glance-list { display: flex; flex-direction: column; gap: 8px; }
640
+ .glance-list-row {
641
+ text-align: left; padding: 12px 14px; border: 1px solid var(--border);
642
+ border-radius: 8px; background: var(--bg); color: var(--text); cursor: pointer;
643
+ line-height: 1.4; width: 100%;
644
+ }
645
+ .glance-list-row:hover { background: var(--bg-hover); border-color: var(--accent-dim); }
646
+ .glance-empty { padding: 24px; text-align: center; color: var(--text-dim); }
647
+ .glance-record { display: flex; flex-direction: column; gap: 10px; }
648
+ .glance-record-fields { display: flex; flex-direction: column; gap: 6px; }
649
+ .glance-record-row {
650
+ display: grid; grid-template-columns: minmax(80px, 140px) 1fr; gap: 10px; font-size: 13px;
651
+ }
652
+ .glance-record-key { color: var(--text-dim); }
653
+ .glance-record-val { color: var(--text); word-break: break-word; overflow-wrap: anywhere; }
654
+ .glance-record-action {
655
+ align-self: flex-start; margin-top: 8px; padding: 6px 12px;
656
+ border: 1px solid var(--border); border-radius: 6px; background: var(--bg-hover);
657
+ color: var(--text); cursor: pointer;
658
+ }
659
+ .glance-record-action:hover { border-color: var(--accent-dim); }
660
+
596
661
  .terminal-header {
597
662
  display: flex;
598
663
  align-items: center;
@@ -3312,15 +3377,11 @@
3312
3377
  <button onclick="loadCommitments()" style="padding:6px 12px">Refresh</button>
3313
3378
  </div>
3314
3379
  <div class="tab-purpose">
3315
- Open promises — beacon-watched commitments (⏳). States:
3316
- <span style="color:#4a9a4a">pending</span>,
3317
- <span style="color:#e27d3b">atRisk</span>,
3318
- <span style="color:#888">suppressed</span>.
3319
- </div>
3320
- <div id="commitmentsEmpty" style="padding:40px;text-align:center;color:var(--text-dim);display:none">
3321
- No open promises.
3380
+ Your open promises at a glance the headline says where things stand; tap a tile to see which promises, tap one for the full record.
3322
3381
  </div>
3323
- <div id="commitmentsList" style="display:flex;flex-direction:column;gap:12px"></div>
3382
+ <!-- Glance floors F10/F11 (topic 29836): the shared component renders the
3383
+ headline + tiles + drill-down here. Reference implementation. -->
3384
+ <div id="commitmentsGlance" class="glance-root"></div>
3324
3385
  </div>
3325
3386
 
3326
3387
  <!-- Tokens Tab — read-only token-usage observability -->
@@ -8757,119 +8818,72 @@
8757
8818
  }
8758
8819
  }
8759
8820
 
8760
- // ── Commitments Tab (PROMISE-BEACON-SPEC Open Promises) ────
8761
- // Fetches /commitments?status=active and renders beacon-watched
8762
- // pending + atRisk commitments with a "Mark delivered" action.
8763
- // All content goes through textContent; no innerHTML. XSS-safe.
8821
+ // ── Commitments Tab — glance floors F10/F11 (topic 29836) ────
8822
+ // The reference implementation of the shared glance component. Fetches
8823
+ // /commitments?status=active and renders a plain-English headline + ≤5 tiles
8824
+ // (Layer 1); each tile drills into the filtered open-promises list (Layer 2);
8825
+ // each row opens the full record with IDs/cadence/deadlines (Layer 3). All
8826
+ // content goes through the component's sanitizer + textContent (XSS-safe).
8827
+ let __glanceModule = null;
8828
+ async function loadGlanceModule() {
8829
+ if (!__glanceModule) {
8830
+ // Same try/catch dynamic-import guard the other tabs use — a missing/failed
8831
+ // glance.js degrades the tab gracefully instead of white-screening.
8832
+ try { __glanceModule = await import('/dashboard/glance.js'); }
8833
+ catch (e) { console.error('[glance] module load failed', e); return null; }
8834
+ }
8835
+ return __glanceModule;
8836
+ }
8837
+
8764
8838
  async function loadCommitments() {
8765
- const list = document.getElementById('commitmentsList');
8766
- const empty = document.getElementById('commitmentsEmpty');
8839
+ const glanceRoot = document.getElementById('commitmentsGlance');
8767
8840
  const countBadge = document.getElementById('tabCommitmentCount');
8768
- while (list.firstChild) list.removeChild(list.firstChild);
8769
- empty.style.display = 'none';
8841
+ if (!glanceRoot) return;
8842
+
8843
+ const glance = await loadGlanceModule();
8844
+ if (!glance) {
8845
+ // Graceful degrade: the component couldn't load. Show an honest note, no crash.
8846
+ glanceRoot.textContent = 'Loading the glance view failed — refresh to retry.';
8847
+ return;
8848
+ }
8770
8849
 
8771
8850
  let res = null;
8772
8851
  try {
8773
8852
  res = await apiFetch('/commitments?status=active');
8774
- } catch { /* fall through */ }
8853
+ } catch { /* fall through to the disabled/empty glance @silent-fallback-ok — network hiccup renders the honest empty state */ }
8775
8854
 
8776
8855
  if (!res || !res.enabled) {
8777
- empty.style.display = 'block';
8778
- empty.textContent = 'CommitmentTracker not available.';
8856
+ glanceRoot.textContent = '';
8857
+ const note = document.createElement('div');
8858
+ note.className = 'glance-empty';
8859
+ note.textContent = res && res.enabled === false
8860
+ ? 'Promise tracking is not set up on this agent yet.'
8861
+ : 'Could not load your promises right now — refresh to retry.';
8862
+ glanceRoot.appendChild(note);
8779
8863
  if (countBadge) countBadge.textContent = '0';
8780
8864
  return;
8781
8865
  }
8866
+
8782
8867
  const items = Array.isArray(res.commitments) ? res.commitments : [];
8783
- // Show only beacon-watched pending + atRisk.
8784
8868
  const open = items.filter(c => c.beaconEnabled && c.status === 'pending');
8785
8869
  if (countBadge) countBadge.textContent = String(open.length);
8786
- if (open.length === 0) {
8787
- empty.style.display = 'block';
8788
- empty.textContent = 'No open promises.';
8789
- return;
8790
- }
8791
-
8792
- const fmtTs = (iso) => {
8793
- if (!iso) return '—';
8794
- try { return new Date(iso).toLocaleString(); } catch { return iso; }
8795
- };
8796
-
8797
- for (const c of open) {
8798
- const card = document.createElement('div');
8799
- card.style.cssText = 'padding:14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-dim);display:flex;flex-direction:column;gap:8px';
8800
8870
 
8801
- const header = document.createElement('div');
8802
- header.style.cssText = 'display:flex;justify-content:space-between;gap:12px;align-items:flex-start';
8803
- const summary = document.createElement('div');
8804
- summary.style.cssText = 'flex:1;font-weight:600;line-height:1.3';
8805
- summary.textContent = (c.agentResponse || c.userRequest || '(no summary)').slice(0, 160);
8806
- header.appendChild(summary);
8807
-
8808
- // State badge.
8809
- const badge = document.createElement('span');
8810
- const atRisk = !!c.atRisk;
8811
- const suppressed = !!c.beaconSuppressed;
8812
- const [badgeText, badgeBg] = suppressed
8813
- ? [`suppressed: ${c.beaconSuppressionReason || '?'}`, '#555']
8814
- : atRisk
8815
- ? ['atRisk', '#e27d3b']
8816
- : ['pending', '#4a9a4a'];
8817
- badge.textContent = badgeText;
8818
- badge.style.cssText = `font-size:11px;padding:3px 8px;border-radius:4px;background:${badgeBg};color:#fff;white-space:nowrap`;
8819
- header.appendChild(badge);
8820
- card.appendChild(header);
8821
-
8822
- const meta = document.createElement('div');
8823
- meta.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:6px;font-size:12px;color:var(--text-dim)';
8824
- const rows = [
8825
- ['id', c.id],
8826
- ['topic', c.topicId != null ? String(c.topicId) : '—'],
8827
- ['cadence', c.cadenceMs ? `${Math.round(c.cadenceMs / 1000)}s` : '—'],
8828
- ['heartbeats', String(c.heartbeatCount ?? 0)],
8829
- ['lastHeartbeat', fmtTs(c.lastHeartbeatAt)],
8830
- ['nextUpdateDue', fmtTs(c.nextUpdateDueAt)],
8831
- ['softDeadline', fmtTs(c.softDeadlineAt)],
8832
- ['hardDeadline', fmtTs(c.hardDeadlineAt)],
8833
- ];
8834
- for (const [k, v] of rows) {
8835
- const cell = document.createElement('div');
8836
- const kEl = document.createElement('span');
8837
- kEl.textContent = `${k}: `;
8838
- kEl.style.opacity = '0.7';
8839
- const vEl = document.createElement('span');
8840
- vEl.textContent = v;
8841
- vEl.style.color = 'var(--text)';
8842
- cell.appendChild(kEl);
8843
- cell.appendChild(vEl);
8844
- meta.appendChild(cell);
8845
- }
8846
- card.appendChild(meta);
8847
-
8848
- // Actions.
8849
- const actions = document.createElement('div');
8850
- actions.style.cssText = 'display:flex;gap:8px;align-items:center';
8851
- const deliverBtn = document.createElement('button');
8852
- deliverBtn.textContent = 'Mark delivered';
8853
- deliverBtn.style.cssText = 'padding:6px 10px;cursor:pointer';
8854
- deliverBtn.addEventListener('click', async () => {
8855
- deliverBtn.disabled = true;
8871
+ const spec = glance.commitmentsGlanceSpec(document, items, {
8872
+ now: Date.now(),
8873
+ onDeliver: async (id) => {
8856
8874
  try {
8857
- await apiFetch(`/commitments/${encodeURIComponent(c.id)}/deliver`, {
8875
+ await apiFetch(`/commitments/${encodeURIComponent(id)}/deliver`, {
8858
8876
  method: 'POST',
8859
8877
  headers: { 'Content-Type': 'application/json' },
8860
8878
  body: JSON.stringify({}),
8861
8879
  });
8862
8880
  await loadCommitments();
8863
8881
  } catch (err) {
8864
- deliverBtn.disabled = false;
8865
8882
  alert('Deliver failed: ' + (err && err.message ? err.message : String(err)));
8866
8883
  }
8867
- });
8868
- actions.appendChild(deliverBtn);
8869
- card.appendChild(actions);
8870
-
8871
- list.appendChild(card);
8872
- }
8884
+ },
8885
+ });
8886
+ glance.renderGlance(document, glanceRoot, spec);
8873
8887
  }
8874
8888
 
8875
8889
  // ── Secrets Tab ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.810",
3
+ "version": "1.3.811",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-11T01:56:42.834Z",
5
- "instarVersion": "1.3.810",
4
+ "generatedAt": "2026-07-11T02:35:23.065Z",
5
+ "instarVersion": "1.3.811",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,53 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Added two new floors to the Dashboard UX Standard and shipped their enforcement plus
9
+ the first reference view:
10
+
11
+ - **F10 (glance floor):** every main view's front page is one plain-English headline +
12
+ at most 5 big labeled tiles, under 150 words, with no insider vocabulary (internal IDs,
13
+ state-machine names, config keys, seconds-cadences).
14
+ - **F11 (universal drill-down):** every tile/count/row is clickable and opens the next
15
+ detail layer — a filtered list, then the full record — with no dead-end summaries.
16
+ - A shared component `dashboard/glance.js` renders the three-layer template (headline +
17
+ tiles + drill-down) and *refuses* to build an over-budget or jargon-carrying glance, so
18
+ the rule is baked into the code, not left to memory.
19
+ - Enforcement across all three test tiers (unit F10 + F11, integration against the real
20
+ `/commitments` route, e2e feature-alive), with the 25 other tabs grandfathered against a
21
+ survey scorecard whose list can only shrink (a NEW tab can't ship below the floor).
22
+ - The **Commitments tab** is the reference implementation: its old wall of raw records
23
+ (IDs, `cadence: 1800s`, `atRisk`) is now a headline ("I'm carrying N open promises; K
24
+ need attention soon, none overdue.") over tiles (Open · Due soon · Waiting on you ·
25
+ Quiet); tap a tile to see which promises, tap one for the full record.
26
+
27
+ This is Phase 1 of the operator-approved four-phase rollout (topic 29836). Phases 2–4
28
+ retrofit the remaining tabs and are tracked in the spec.
29
+
30
+ ## What to Tell Your User
31
+
32
+ Your dashboard's Commitments tab is now readable at a glance: instead of a wall of
33
+ technical records, you'll see a one-sentence summary of where your open promises stand
34
+ plus a few big tiles. Tap any tile to see exactly which promises are behind that number,
35
+ and tap one of those to see its full detail. Nothing is lost — the IDs and timestamps
36
+ just moved one or two taps down instead of crowding the front page. This is the first tab
37
+ brought to the new "glance" standard; the rest follow in later updates.
38
+
39
+ ## Summary of New Capabilities
40
+
41
+ - The Commitments dashboard tab now leads with a plain-English headline + tiles, with
42
+ every number tappable down to the full record.
43
+ - A new dashboard-wide standard (F10/F11) that keeps future views readable at a glance and
44
+ fully drillable, enforced automatically in CI.
45
+
46
+ ## Evidence
47
+
48
+ - Unit: `tests/unit/dashboard-glance-word-budget.test.ts` (31), `tests/unit/dashboard-glance-drilldown.test.ts` (8).
49
+ - Integration: `tests/integration/glance-commitments-tab.test.ts`.
50
+ - E2E: `tests/e2e/glance-commitments-tab-lifecycle.test.ts`.
51
+ - Live browser render (Playwright): headline + 4 tiles render; tile → filtered list → full
52
+ record (`id: CMT-953`, `cadence: 1800s` shown only at the record layer).
53
+ - Spec convergence: `docs/specs/reports/dashboard-ux-standard-convergence.md`.
@@ -0,0 +1,225 @@
1
+ # Side-Effects Review — Dashboard glance floors F10/F11 (Phase 1)
2
+
3
+ **Version / slug:** `glance-floors-f10-f11`
4
+ **Date:** `2026-07-10`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required` (no block/allow, session-lifecycle, or gate/sentinel/guard surface — this is a display-only front-end component; see §5)
7
+
8
+ ## Summary of the change
9
+
10
+ Adds the two "glance floors" (F10 glance + F11 universal drill-down) to the Dashboard
11
+ UX Standard and ships their enforcement: one shared front-end component
12
+ `dashboard/glance.js` (a plain-English headline + ≤5 labeled tiles + a drill-down
13
+ container that opens a filtered list then a full record), a pure `validateGlanceSpec`
14
+ that refuses an over-budget or jargon-carrying glance, and three test tiers
15
+ (unit F10 + F11, integration against the real `/commitments` route, e2e feature-alive).
16
+ One view — the Commitments tab — is wired onto the component as the reference
17
+ implementation (glance layer → existing list as Layer 2 → full record as Layer 3).
18
+ Files: `docs/specs/dashboard-ux-standard.md` (+ `.eli16.md` + convergence report),
19
+ `docs/STANDARDS-REGISTRY.md`, `dashboard/glance.js` (new), `dashboard/index.html`
20
+ (Commitments panel markup + CSS + `loadCommitments` rewrite), and four test files.
21
+ **No `src/*.ts` change, no new server route, no config, no hooks.**
22
+
23
+ ## Decision-point inventory
24
+
25
+ **No decision-point surface.** Nothing here gates information flow, blocks actions,
26
+ filters messages, or constrains agent behavior. `validateGlanceSpec` is a build-time
27
+ quality assertion over component-authored UI copy — it decides how a dashboard tab
28
+ *renders*, never what the agent may do. The one "refusal" (the component refusing to
29
+ render an over-budget/jargon glance) affects presentation only and falls back to an
30
+ honest degraded glance, never a raw dump — it holds no authority over any pipeline.
31
+
32
+ - `validateGlanceSpec` (dashboard/glance.js) — add — a pure UI-copy budget/jargon
33
+ check; presentation-only, no runtime authority.
34
+
35
+ ---
36
+
37
+ ## 1. Over-block
38
+
39
+ **No block/allow surface — over-block not applicable.** The nearest analog is the
40
+ jargon check possibly rejecting legitimate glance copy. It is scoped to
41
+ *component-authored* Layer-1 strings only (headline + tile labels + values); agent/
42
+ user free text lives at Layer 2/3 and is displayed (sanitized), never vocab-gated — so
43
+ a user phrasing a promise with jargon can never blank the operator's glance. If the
44
+ check does reject a builder's copy, the component renders an honest degraded glance
45
+ (truncated headline + a drill), never a raw dump — no operator-visible data is lost.
46
+
47
+ ---
48
+
49
+ ## 2. Under-block
50
+
51
+ **No block/allow surface — under-block not applicable.** As a readability floor the
52
+ jargon detector is deliberately heuristic (it is NOT a secret-redaction boundary —
53
+ secret handling stays at the API/data layer, untouched here). It can miss novel
54
+ concept-jargon expressed in ordinary words; that residual is a tracked later-phase
55
+ tightening, and the curated insider-TERM denylist + form heuristics cover the known
56
+ classes (internal IDs, machine ids, config keys, cadences, state-machine names) with
57
+ bypass-variant tests.
58
+
59
+ ---
60
+
61
+ ## 3. Level-of-abstraction fit
62
+
63
+ Right layer. F10/F11 are enforced at the **component boundary** (a shared renderer +
64
+ a pure validator), not by scraping bespoke per-tab markup — because the glance content
65
+ is JS-rendered from live data, a static grep of `index.html` cannot see it, so the
66
+ component is the correct place to make the floor structural. The component REUSES the
67
+ existing lower-level primitives instead of re-implementing them: `sanitizeForDisplay`,
68
+ `hasOpenInteraction`, and `updateCountdowns` are imported from `dashboard/subscriptions.js`
69
+ (the shipped F9 safety + interaction-hold bar), so both surfaces share one contract.
70
+
71
+ ---
72
+
73
+ ## 4. Signal vs authority compliance
74
+
75
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
76
+
77
+ - [x] No — this change has no block/allow surface.
78
+
79
+ `validateGlanceSpec` is a build-time/presentation quality check with no blocking
80
+ authority over any runtime pipeline. It informs how a tab renders (and, in the tests,
81
+ fails CI on a NEW below-floor tab) — it never gates a message, action, session, or
82
+ information flow. There is no brittle detector holding runtime authority here.
83
+
84
+ ---
85
+
86
+ ## 5. Interactions
87
+
88
+ - **Shadowing:** none. The glance component is additive; it renders into a new
89
+ `#commitmentsGlance` container. It does not run before/after any check.
90
+ - **Double-fire:** none. `loadCommitments` replaces (never appends) the glance root
91
+ and the drill container, so repeated renders/polls cannot accumulate DOM or listeners.
92
+ - **Races:** F9-composed. While a drill interaction is open (the drill container carries
93
+ `data-interaction-open`, or a field is focused/dirty), a re-render MERGES live counts
94
+ via `patchGlanceCounts` instead of rebuilding over the interaction — reusing the shipped
95
+ `hasOpenInteraction`. The Commitments tab has no background poll today, so this is
96
+ latent-but-correct (and unit-tested), and will already be right if a poll is added.
97
+ - **Feedback loops:** none — a pure renderer over data the tab already fetched.
98
+
99
+ ---
100
+
101
+ ## 6. External surfaces
102
+
103
+ - **Other agents / users / external systems:** none. `dashboard/glance.js` is a
104
+ client-side ESM module served statically; it holds no secrets, opens no endpoint, and
105
+ makes no network call of its own (the Commitments reference reuses the EXISTING authed
106
+ `/commitments` GET and the EXISTING `/commitments/:id/deliver` POST).
107
+ - **Persistent state:** none touched.
108
+ - **Operator surface (Mobile-Complete):** the operator actions on this surface (view
109
+ promises, drill in, mark delivered) are all completable from the dashboard, which is
110
+ phone-reachable via the tunnel + PIN — no new laptop-bound step. The "Mark delivered"
111
+ action lives on the Layer-3 record and calls the existing authed route.
112
+
113
+ ---
114
+
115
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
116
+
117
+ This change touches an operator surface (`dashboard/glance.js`, `dashboard/index.html`).
118
+ The glance floors ARE the whole-view structural application of this standard.
119
+
120
+ 1. **Leads with the primary action?** Yes. On arrival the Commitments tab shows the
121
+ headline answer ("I'm carrying N open promises; K need attention soon, none overdue.")
122
+ and the big labeled tiles — the state and the way in, visible immediately, no toggle,
123
+ no below-the-fold, no explanatory prose in front.
124
+ 2. **Zero raw internals as primary content?** Yes — enforced by F10 itself. No internal
125
+ IDs, state-machine names, config keys, or seconds-cadences may appear at the glance
126
+ layer (a machine-checked rule). The raw detail (`CMT-953`, `cadence 1800s`, timestamps)
127
+ lives at Layer 3, one click down, where it belongs — verified in a real browser (tile →
128
+ list → record).
129
+ 3. **Destructive actions de-emphasized?** N/a-leaning: the only action is the constructive
130
+ "Mark delivered", placed on the Layer-3 record (not above the glance). There is no
131
+ destructive control on this surface; nothing louder than the primary path.
132
+ 4. **Plain language + phone width?** Yes. Copy reads the way a person would say it
133
+ ("Waiting on you", "Due soon", "Quiet"). Tiles use an auto-fit grid that reflows at
134
+ phone width; the drill list/records stack; nothing scrolls the page sideways (records
135
+ use `word-break`/`overflow-wrap`). Verified visually at 900px and via the responsive
136
+ grid.
137
+
138
+ ---
139
+
140
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
141
+
142
+ **machine-local BY DESIGN → effectively `unified` by construction.** `dashboard/glance.js`
143
+ is a **stateless client-side renderer**: it persists nothing, reads no config, holds no
144
+ server state, and introduces no machine-divergent state. It renders whatever data the
145
+ adopting tab already fetched and inherits that endpoint's existing posture — the
146
+ Commitments reference drills into `GET /commitments`, whose pool-scope posture
147
+ (`?scope=mesh`) is unchanged by this PR. So there is no new machine-local surface to
148
+ justify and no `machine-local-justification` marker is required.
149
+
150
+ - **User-facing notices:** none emitted (a passive render surface — one-voice gating n/a).
151
+ - **Durable state:** none held (nothing strands on topic transfer).
152
+ - **Generated URLs:** none (no links minted that must survive a machine boundary).
153
+
154
+ **Migration Parity:** met by construction. The dashboard ships wholesale via
155
+ `express.static(dashboardDir)` from the installed package directory (`package.json`
156
+ `files` includes `dashboard/`; `AgentServer.resolveDashboardDir` resolves to the package
157
+ root, not the agent home). A package update replaces `dashboard/glance.js` + updated
158
+ `index.html` on the normal update path — exactly as `dashboard/subscriptions.js` shipped —
159
+ so already-deployed agents receive the glance with **no `PostUpdateMigrator` entry** and
160
+ no `init`-only templating. `loadCommitments` loads `glance.js` through the same `try/catch`
161
+ dynamic-import guard the other tabs use, so a missing/failed module degrades the tab
162
+ gracefully. **Agent Awareness:** n/a — this is an internal dashboard UX/dev standard (how
163
+ a view renders), not a new operator-invocable capability/route/config/hook; the awareness
164
+ surface is `STANDARDS-REGISTRY.md` (nine → eleven floors), already updated.
165
+
166
+ ---
167
+
168
+ ## 8. Rollback cost
169
+
170
+ **Pure front-end change — revert and ship a patch.** No persistent state, no data
171
+ migration, no agent-state repair. `dashboard/glance.js` and the `index.html` edits are
172
+ replaced wholesale on the next package update; reverting the commit restores the prior
173
+ Commitments renderer. No user-visible regression during the rollback window (the tab
174
+ simply reverts to its previous look). The new tests would revert with the code.
175
+
176
+ ---
177
+
178
+ ## Conclusion
179
+
180
+ This review produced no blocking concerns. The change is display-only with no
181
+ decision-point, block/allow, session-lifecycle, or gate/sentinel surface, so no
182
+ second-pass review is required. The design was materially hardened by /spec-converge
183
+ (a six-angle internal panel + the code-backed Standards-Conformance Gate + an external
184
+ Gemini pass across three rounds): the Layer-1-is-100%-component-authored invariant that
185
+ makes the jargon check both safe and complete; the XSS/display-safety contract reusing
186
+ the shipped `sanitizeForDisplay`; bypass-resistant vocab detection; the honest-degraded
187
+ (never raw-dump) failure mode; a strengthened F11 (non-vacuous, distinct, tile→list→record,
188
+ XSS + dead-end negatives); the structural grandfather ratchet (completeness + monotonic
189
+ ceiling); the one-population honest tile→server-field derivation with a count-truthfulness
190
+ test; and all three test tiers. Verified live in a real browser (Playwright): the
191
+ Commitments glance renders headline + tiles, and drilling opens the filtered list then the
192
+ full record with the raw IDs/cadence correctly one click down. Clear to ship.
193
+
194
+ ---
195
+
196
+ ## Second-pass review (if required)
197
+
198
+ **Reviewer:** not required — no block/allow, messaging-dispatch, session-lifecycle,
199
+ compaction, coherence/idempotency/trust, or sentinel/guard/gate/watchdog surface (Phase-5
200
+ triggers). Display-only front-end component.
201
+
202
+ ---
203
+
204
+ ## Evidence pointers
205
+
206
+ - Unit: `tests/unit/dashboard-glance-word-budget.test.ts` (31 tests — F10 budget/jargon +
207
+ bypass variants + adversarial-fixture conformance + count-truthfulness + the ratchet),
208
+ `tests/unit/dashboard-glance-drilldown.test.ts` (8 tests — F11 walk, Layer-2→3, negatives,
209
+ F9 hold, XSS).
210
+ - Integration: `tests/integration/glance-commitments-tab.test.ts` (glance built + walked
211
+ against a real `GET /commitments` HTTP response with a live `CommitmentTracker`).
212
+ - E2E: `tests/e2e/glance-commitments-tab-lifecycle.test.ts` (feature-alive: 200 not 503,
213
+ feature ON/OFF, full render, no `<script>` survives).
214
+ - Live: Playwright render of the reference glance — headline "I'm carrying 5 open
215
+ promises; 2 need attention soon, none overdue.", 4 tiles, tile→list (2 rows)→record
216
+ (`id: CMT-953`, `cadence: 1800s` at Layer 3 only).
217
+ - Spec convergence report: `docs/specs/reports/dashboard-ux-standard-convergence.md`.
218
+
219
+ ---
220
+
221
+ ## Class-Closure Declaration (display-only mirror)
222
+
223
+ No agent-authored-artifact defect and no self-triggered controller (no loop / monitor /
224
+ sentinel / reaper / scheduler / recovery path that fires a restart / swap / respawn /
225
+ spawn / notify / retry / re-drive / kill) — **not applicable**.