@bongos/core 1.21.46 → 1.21.47

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.
@@ -108,8 +108,10 @@ body[data-page="street"] {
108
108
 
109
109
  .mcard-btn:focus-visible, .mcard-textbtn:focus-visible, .mcard-form__text:focus-visible, .mcard-form__select:focus-visible, .mcard-chip:focus-visible, .mcard-chips__field:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
110
110
 
111
- /* the toast: one short line over the street */
112
- .street-toast { position: absolute; left: 50%; top: 192px; transform: translateX(-50%); max-width: calc(100% - 32px); box-sizing: border-box; font-size: 15px; line-height: 1.35; color: var(--ink); padding: 11px 20px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--street-rule-strong); z-index: 10; pointer-events: none; text-align: center; }
111
+ /* the toast: one short line over the street. Centred with translate, not
112
+ transform: the enter animation ends on transform none, which put the toast
113
+ half its width right of centre (task 1004562) */
114
+ .street-toast { position: absolute; left: 50%; top: 192px; translate: -50% 0; max-width: calc(100% - 32px); box-sizing: border-box; font-size: 15px; line-height: 1.35; color: var(--ink); padding: 11px 20px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--street-rule-strong); z-index: 10; pointer-events: none; text-align: center; }
113
115
  .street-toast[hidden] { display: none; }
114
116
 
115
117
  /* the city answers: a building rises (the prototype's rise) or lights, a pin settles */
@@ -128,6 +130,10 @@ body[data-page="street"] {
128
130
  @media (max-width: 560px) {
129
131
  .street-toast { top: 96px; border-radius: 16px; }
130
132
  }
133
+ /* the desk's toast after an act (Desks.dc.html, task 1004562): an ink pill with
134
+ the orange dot, low over the desk, just above the six readings */
135
+ .street-toast--desk { top: auto; bottom: 150px; display: flex; align-items: center; gap: 10px; width: max-content; font-size: 14px; background: var(--ink); color: var(--bg); border: 0; border-radius: 999px; text-align: left; }
136
+ .street-toast--desk::before { content: ''; flex: none; width: 8px; height: 8px; border-radius: 999px; background: var(--accent); }
131
137
 
132
138
  /* The Kill Switch: under reduced motion nothing moves; the street still
133
139
  answers, at rest (the toast, the new pins, the lit windows). */
@@ -96,6 +96,50 @@
96
96
  if (d < 14) return d + ' days';
97
97
  return Math.round(d / 7) + ' weeks';
98
98
  }
99
+ // The record line's age, in the board's short words: 'today', '3d', '2w', '1mo'
100
+ // (task 1004562: the long age was cut off the end of nearly every record item).
101
+ function shortAge(at, now) {
102
+ const t = at ? Date.parse(at) : NaN;
103
+ if (!isFinite(t) || !now) return '';
104
+ const d = Math.floor(Math.max(0, now - t) / 86400000);
105
+ if (d < 1) return 'today';
106
+ if (d < 14) return d + 'd';
107
+ if (d < 60) return Math.round(d / 7) + 'w';
108
+ return Math.round(d / 30) + 'mo';
109
+ }
110
+ // One record item: the title, then what came of it and when, in 30
111
+ // characters. The age always shows; the outcome only when the title fits
112
+ // beside it whole ('Lea → metic · 2w' on the board).
113
+ function recordText(h, now) {
114
+ const title = str(h && h.title).replace(/\s+/g, ' ').trim();
115
+ const age = shortAge(h && h.at, now);
116
+ const out = str(h && h.outcome).trim();
117
+ const tail = (parts) => parts.filter(Boolean).join(' · ');
118
+ const full = tail([title, out, age]);
119
+ if (full.length <= LIMITS.recordLen) return full;
120
+ const room = LIMITS.recordLen - (age ? age.length + 3 : 0);
121
+ return fit(tail([fit(title, room), age]), LIMITS.recordLen);
122
+ }
123
+ // A quiet floor's tile says what its number counts, in two words that read as
124
+ // a label: the floor line's own words when they follow that same number and
125
+ // are short ('5 seats'), else the floor's name ('3 testers', '2 signed off'),
126
+ // else the line's last two words ('1 went live'). Never the first two words of
127
+ // a longer line, which read 'Lu, Bo' or 'piece of' (task 1004562).
128
+ function floorLabel(count, line, name) {
129
+ const first = str(line).split(' · ')[0].replace(/\s+/g, ' ').trim();
130
+ const m = /^([$]?[0-9][0-9,.%]*)\s+(.+)$/.exec(first);
131
+ const same = !!m && m[1] === str(count).trim();
132
+ const w = (s) => s.split(' ').filter(Boolean);
133
+ if (same && w(m[2]).length <= LIMITS.tileWords) return m[2];
134
+ const nm = str(name).replace(/\s+/g, ' ').trim().toLowerCase();
135
+ if (nm && !/^§/.test(nm) && w(nm).length <= LIMITS.tileWords) return nm;
136
+ if (same) return w(m[2]).slice(-LIMITS.tileWords).join(' ');
137
+ return 'here';
138
+ }
139
+ // A count agrees with its noun: '1 rank', '2 open votes' (task 1004562).
140
+ const SINGULAR = Object.freeze({ ranks: 'rank', runners: 'runner', builders: 'builder', invites: 'invite', settings: 'setting', modules: 'module', backups: 'backup', updates: 'update' });
141
+ const PLURAL = Object.freeze({ 'open vote': 'open votes', update: 'updates' });
142
+ function agree(n, noun) { return Number(n) === 1 ? (SINGULAR[noun] || noun) : (PLURAL[noun] || noun); }
99
143
 
100
144
  // The words a floor counts its rows in, and says when it is cleared.
101
145
  const NOUNS = Object.freeze({
@@ -105,6 +149,10 @@
105
149
  const CLEAR = Object.freeze({
106
150
  access_requests: 'Everyone is in', goal_join_requests: 'No one is asking', override_requests: 'No overrides asked',
107
151
  gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
152
+ // every other floor that holds matters says its own line too (task 1004562)
153
+ redteam_reports: 'No reports open', removal_proposals: 'No removals asked', grade_appeals: 'No appeals open',
154
+ module_rechecks: 'Every module checked', criteria_uat: 'Nothing waits for a tester', core_update: 'Core is up to date',
155
+ autobongos_fence: 'The switch is set', standing_rules: 'No rules proposed', treasury_spending: 'No spending asked',
108
156
  });
109
157
  // The card's acts under the desk's words (the board's button labels).
110
158
  const ACT_WORDS = Object.freeze({
@@ -246,7 +294,12 @@
246
294
  if (card && card.subject) card.subject.council = councilOf(ctx.docket, card.building) ? card.building : null;
247
295
  const actions = [];
248
296
  const choices = {};
297
+ // 'open' leaves for an older page: a row that can act here keeps its detail
298
+ // in place (tap the name) and drops the link; only a row with no act of its
299
+ // own may link out (owner standard, task 1004562)
300
+ const inPlace = acts.some((a) => a && a.key !== 'open' && a.key !== 'record');
249
301
  acts.forEach((a) => {
302
+ if (a.key === 'open' && inPlace) return;
250
303
  // the core's 'take' is its own word ('take it'), not the rank's 'take back'
251
304
  const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
252
305
  if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
@@ -311,7 +364,18 @@
311
364
  // a desk row that stands in for a source (the core update's check-then-take)
312
365
  // replaces that source's matter rows on this floor
313
366
  const over = desked.map((r) => r.over).filter(Boolean);
314
- return floorMatters(key, floor, docket).filter((m) => !done[str(m.id)] && over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
367
+ const here = floorMatters(key, floor, docket).filter((m) => !done[str(m.id)]);
368
+ // the stand-in keeps what it stands for: a matter that needs you still
369
+ // needs you on the desk (orange status, its age), as on the floor's sign
370
+ // and the street's count (task 1004562)
371
+ desked.forEach((r) => {
372
+ if (!r.over) return;
373
+ const stood = here.filter((m) => m.source === r.over && stateOf(m) === 'needs');
374
+ if (!stood.length) return;
375
+ r.needs = true;
376
+ if (!r.opened) r.opened = stood.reduce((a, m) => (m.opened_at && (!a || Date.parse(m.opened_at) < Date.parse(a)) ? m.opened_at : a), null);
377
+ });
378
+ return here.filter((m) => over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
315
379
  .concat(desked)
316
380
  .sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
317
381
  }
@@ -400,9 +464,11 @@
400
464
  const quiet = rows.length === 0;
401
465
  let status, hot = false;
402
466
  if (!docket) status = 'the docket could not be read';
403
- else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
404
- else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
405
- else if (rows.length) status = fl.line;
467
+ else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + agree(needs.length, noun) + (age ? ' · oldest ' + age : ''); }
468
+ else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + agree(counted.length, moving) + (age ? ' · oldest ' + age : ''); }
469
+ // only tools left (an invite, a setting): the floor's line, unless that line
470
+ // still counts what you just did ('1 needs you' after you let them in)
471
+ else if (rows.length) status = /needs? you/.test(str(fl.line)) ? 'nothing needs you' : fl.line;
406
472
  else if (cleared) status = 'floor clear';
407
473
  else status = 'nothing needs you · ' + fl.line;
408
474
  const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
@@ -419,13 +485,16 @@
419
485
  // the order's one number: the share of its goals' promises kept
420
486
  const on = ordered.reduce((a, r) => a + (r.pips || []).filter((p) => p.on).length, 0);
421
487
  const of = ordered.reduce((a, r) => a + (r.pips || []).length, 0);
422
- tile(needs.length, noun);
488
+ tile(needs.length, agree(needs.length, noun));
423
489
  if (of) tile(Math.round((100 * on) / of) + '%', 'promises kept');
424
- } else if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
425
- else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
490
+ } else if (rows.length) { tile(needs.length, agree(needs.length, noun)); tile(counted.length - needs.length, agree(counted.length - needs.length, moving)); }
491
+ // a quiet floor's own number; never on a floor you just cleared, and never a
492
+ // 'needs you' count on a floor where nothing does (task 1004562)
493
+ else if (fl.count && !cleared && !/needs? you/.test(str(fl.line))) tile(fl.count, floorLabel(fl.count, fl.line, spec[0]));
426
494
  tile(history.length, 'on record');
427
- const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
428
- .concat(history.map((h, i) => ({ key: 'h' + i, text: fit([h.title, h.outcome, ageWord(h.at, o.now)].filter(Boolean).join(' · '), LIMITS.recordLen), mine: false })))
495
+ // what you did keeps its ' · now'; a closed item keeps its age (task 1004562)
496
+ const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t, LIMITS.recordLen - 6) + ' · now', mine: true }))
497
+ .concat(history.map((h, i) => ({ key: 'h' + i, text: recordText(h, o.now), mine: false })))
429
498
  .slice(0, LIMITS.record);
430
499
  return {
431
500
  key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
@@ -344,7 +344,10 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
344
344
  .desk-quiet--cleared .desk-quiet__tick { stroke: var(--street-accent-text); }
345
345
  .desk-quiet__line { font-size: 20px; font-weight: 300; }
346
346
  .desk-quiet__sub { font-size: 13.5px; color: var(--ink-faint); }
347
- .desk-record { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 22px; border-top: 1px solid var(--street-rule); margin-top: 18px; padding-top: 14px; }
347
+ /* the record is ONE line, as on the board: items that do not fit wrap onto a
348
+ second line that is clipped away, so the newest stay and none is cut in half
349
+ (task 1004562) */
350
+ .desk-record { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 22px; border-top: 1px solid var(--street-rule); margin-top: 18px; padding-top: 14px; box-sizing: border-box; line-height: 20px; max-height: 35px; overflow: hidden; }
348
351
  .desk-record__label { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
349
352
  .desk-record__item { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
350
353
  .desk-record__item--mine { color: var(--ink); }
@@ -707,11 +707,11 @@
707
707
  draw();
708
708
  return send(req).then(function (out) {
709
709
  desk.busy = false;
710
- if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
710
+ if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
711
711
  desk.order[k] = null;
712
712
  desk.mine[k] = [dm.save.council ? 'order sent to council' : 'order set'].concat(desk.mine[k] || []);
713
713
  bumpToday();
714
- say(dm.save.said);
714
+ say(dm.save.said, true);
715
715
  draw();
716
716
  return load().then(function () { return true; });
717
717
  });
@@ -760,7 +760,7 @@
760
760
  var C = window.GovMatterCard, st = view.inside;
761
761
  if (!C || !st || !r.card || desk.busy) return Promise.resolve(false);
762
762
  var refused = C.check(r.card, key, input);
763
- if (refused) { say(refused); return Promise.resolve(false); }
763
+ if (refused) { say(refused, true); return Promise.resolve(false); }
764
764
  var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
765
765
  var req = C.request(r.card, key, input);
766
766
  var settle = function (body) {
@@ -774,14 +774,14 @@
774
774
  if (st.floor == null && view.insideModel) st.floor = view.insideModel.selFloor;
775
775
  bumpToday();
776
776
  if (a.building && a.kind) card.answer = { building: a.building, matter: r.matter, kind: a.kind, at: Date.now() };
777
- say(a.toast || 'Done.');
777
+ say(a.toast || 'Done.', true);
778
778
  };
779
779
  if (!req) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
780
780
  desk.busy = true;
781
781
  draw();
782
782
  return send(req).then(function (out) {
783
783
  desk.busy = false;
784
- if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
784
+ if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
785
785
  return new Promise(function (res) {
786
786
  clearRow(r.key, function () {
787
787
  settle(out.body);
@@ -1230,9 +1230,12 @@
1230
1230
  for (var j = 0; j < pins.length; j++) if (String(pins[j].getAttribute('data-matters') || pins[j].getAttribute('data-matter')).split(' ').indexOf(a.matter) >= 0) pins[j].className += ' street-pin--answer';
1231
1231
  }
1232
1232
  var toastTimer = null;
1233
- function say(text) {
1233
+ // desk: the Desks board's toast after an act on a floor (an ink pill with the
1234
+ // orange dot, low over the desk); the street's own toast otherwise (task 1004562)
1235
+ function say(text, desk) {
1234
1236
  var t = $('streetToast');
1235
1237
  if (!t || !text) return;
1238
+ t.className = 'street-toast' + (desk ? ' street-toast--desk' : '');
1236
1239
  t.textContent = text;
1237
1240
  t.hidden = false;
1238
1241
  if (typeof setTimeout !== 'function') return;
@@ -33,3 +33,17 @@ Files: `street-inside.js` (`window.GovInside`, node-require-safe, pure): the pro
33
33
  - **Narrow.** Under 900 px the board becomes a column: the drawing in its own sideways strip (never scaled under 0.6, so a floor stays a 24 px target), then the panel.
34
34
 
35
35
  Tests: `tests/hall_street_inside.mjs` runs the prototype's own `Component` beside `street-inside.js` on the prototype's sample floors and holds every building's section, panel bindings and every district equal to it in both modes, then the docket rules (no invented numbers) and the page in a vm (the persistent lift, the flip, the drawers, the amendment note, the card from a table row, back to the street, a district's tasks). The states file renders every building, a floor pick, the amendment note and a district.
36
+
37
+ ## The floor desks, walked (task 1004562)
38
+
39
+ Every building and every floor desk was walked in light, dark and at phone width (375 px) against `Desks.dc.html` and the line rule of `Ground.dc.html`. The live site is behind its password gate, so the walk ran on the real pages in the hall preview harness with its fixtures. What it settled:
40
+
41
+ - **A quiet floor's tile says what it counts.** Two words that read as a label: the floor line's own noun when it follows the same number ("5 seats"), else the floor's name ("3 testers", "2 signed off"), else the line's last two words ("1 went live"). Never the first two words of a longer line ("3 Lu, Bo", "1 piece of"). `floorLabel()` in `street-desk.js`.
42
+ - **A cleared floor shows no count of the work just done.** The docket's floor count lags the act, so a cleared floor draws no floor-count tile, and a floor left with only a tool (the invite) does not repeat a stale "1 needs you" as its status. Floors that hold matters each say their own clear line ("No removals asked").
43
+ - **Counts agree with their nouns**: "2 open votes", "1 rank".
44
+ - **The record is one line.** Each item keeps its age in the board's short words ("· 3d", "· 2w"); the outcome shows only when the title fits beside it; what you did keeps its "· now". Items that do not fit wrap onto a clipped second line, so none is cut in half.
45
+ - **A stand-in row keeps the need it stands for.** The Release desk's core row replaces the core-update matter; it now carries that matter's need, so the status is orange where the floor sign and the street count already said "needs you".
46
+ - **No link out beside an act.** A row that can act in place drops the server's `open` act; its detail opens in place by its name. A row with no act of its own (a Full Idea that needs its page to finish, Work categories) still links, as the standard allows.
47
+ - **The desk's toast is the board's**: an ink pill with the orange dot, low over the desk. The street's own toast is centred again (its enter animation had undone the centring).
48
+
49
+ Left as found, on purpose: the six readings stay under a building (task 1004526 marks the building's own reading), the old month drawers stay under the desk, and the recording sign-off still goes to its page (an open owner choice).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.46",
3
+ "version": "1.21.47",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.46",
9
+ "version": "1.21.47",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.46",
3
+ "version": "1.21.47",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9040,5 +9040,11 @@
9040
9040
  "id": "1004572",
9041
9041
  "text": "Setting up GitHub sign-in for a new project now says what is really happening at each step, and always leaves a way forward if it gets stuck."
9042
9042
  }
9043
+ ],
9044
+ "1.21.47": [
9045
+ {
9046
+ "id": "1004562",
9047
+ "text": "Walked every building and floor desk of the Governor City in light, dark and on a phone, and fixed what did not match the design: number tiles now say what they count, a cleared floor no longer shows a stale count, counts read"
9048
+ }
9043
9049
  ]
9044
9050
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.46'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.47'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -327,6 +327,7 @@ test('the page: the desk acts in place — the row clears, the floor lights with
327
327
  assert.deepEqual(p.sent, [{ method: 'PATCH', url: '/api/bongos/access-requests/11', body: { status: 'invited' } }], 'the card\'s own route, nothing else');
328
328
  assert.match(p.byId.get('streetToast').textContent, /Ana Ruiz is invited in/);
329
329
  assert.equal(p.byId.get('streetToast').hidden, false);
330
+ assert.match(p.byId.get('streetToast').className, /street-toast--desk/, "the desk's own toast, as on the board (task 1004562)");
330
331
  assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
331
332
  assert.equal(p.byId.get('streetTodayPips').children.filter((x) => x.className.includes('--on')).length, 1, 'one pip per decision');
332
333
  assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'the day keeps its count');
@@ -624,6 +625,12 @@ test('Release: the core update is checked first; take is offered only after a pa
624
625
  assert.equal(C.request(passed.card, 'take', {}), null, 'no version chip, no move');
625
626
  assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, open_intent: { state: 'running' } }), [], 'a run in progress offers nothing');
626
627
  assert.deepEqual(coreRoute.coreDeskRows({ ...PREVIEW, behind: false }), []);
628
+ // the stand-in keeps what it stands for: the core update needs you, so the
629
+ // desk says so in orange, as the floor's sign and the street's count do (task 1004562)
630
+ const desk = D.deskModel({ key: 'release', floor: 3, docket: moreDesks({ preflight: null }), goals: GOALS, now: NOW, viewerId: '95' });
631
+ assert.equal(desk.hot, true);
632
+ assert.match(desk.status, /^1 update/);
633
+ assert.deepEqual(D.breaks(desk), []);
627
634
  });
628
635
 
629
636
  test('Rules: the rot timer as day chips, work categories as a link, modules on in one tap and off with a yes', () => {
@@ -747,10 +754,11 @@ test('Ideas to ratify: a promise ratifies in one tap; a carried Full Idea become
747
754
  const dk = planningDocket();
748
755
  const rows = D.rowsFor('planning', 1, dk, { now: NOW, goals: GOALS });
749
756
  const promise = rows.find((r) => r.name === 'Calm mode promise');
750
- assert.deepEqual(promise.actions.map((a) => a.label), ['ratify', 'open']);
757
+ assert.deepEqual(promise.actions.map((a) => a.label), ['ratify'], 'a row that acts here keeps its detail in place: no link out (task 1004562)');
758
+ assert.ok(promise.facts.length >= 1, 'its detail opens in place');
751
759
  assert.deepEqual(j(C.request(promise.card, 'ratify')), { method: 'POST', url: '/api/bongos/inbox/5/ratify', body: { notes_md: 'Ratified on the floor desk.' } });
752
760
  const full = rows.find((r) => r.name === 'Objection goes back');
753
- assert.deepEqual(full.actions.map((a) => a.label), ['make goal', 'open']);
761
+ assert.deepEqual(full.actions.map((a) => a.label), ['make goal'], 'no link out beside an act');
754
762
  assert.deepEqual(full.choices.ratify_goal.options.map((o) => o.label), ['V2', 'V3'], 'the target version is a chip');
755
763
  assert.deepEqual(j(C.request(full.card, 'ratify_goal', full.choices.ratify_goal.options[1].input)), { method: 'POST', url: '/api/bongos/inbox/6/ratify-goal', body: { target_version: 'V3', notes_md: 'Ratified on the floor desk.' } });
756
764
  assert.ok(!rows.some((r) => r.name === 'Still sitting'), 'a Full Idea the board has not carried is not ratified from here');
@@ -832,3 +840,57 @@ test('the page: one tap on its option decides a blocker, its options standing op
832
840
  assert.deepEqual(p.sent, [{ method: 'POST', url: '/api/bongos/blockers/12/resolve', body: { resolution_note: 'Decided on the floor desk: Paddle.' } }]);
833
841
  assert.match(p.byId.get('streetToast').textContent, /Decided/);
834
842
  });
843
+
844
+ // ── the live walkthrough's differences (task 1004562) ─────────────────────────
845
+ function floorRow(key, count, line) {
846
+ return { key, building: key.split('.')[0], status: 'live', count, line, lines: [], hot: false, reason: null };
847
+ }
848
+ test('walkthrough: a quiet floor\'s tile says what its number counts, in two words that read as a label', () => {
849
+ const dk = docket([], { history: [], floors: [
850
+ floorRow('people.testers', '3', 'Lu, Bo and Ines'),
851
+ floorRow('people.council', '5', '5 seats · no term yet'),
852
+ floorRow('people.newcomers', '2', '2 newcomer tasks being built'),
853
+ floorRow('release.live_month', '1', '1 piece of work went live'),
854
+ floorRow('review.signed_off', '2', '2 signed off this month'),
855
+ ] });
856
+ const tile = (key, floor) => D.deskModel({ key, floor, docket: dk, now: NOW }).tiles.map((t) => t.v + ' ' + t.l);
857
+ assert.deepEqual(tile('people', 1), ['3 testers'], 'a line that is not a count names nothing: the floor does');
858
+ assert.deepEqual(tile('people', 0), ['5 seats'], 'the line\'s own noun when it follows the same number');
859
+ assert.deepEqual(tile('people', 3), ['2 newcomers'], 'a long line: the floor\'s name');
860
+ assert.deepEqual(tile('release', 4), ['1 went live'], 'a long line under a long name: its last two words');
861
+ assert.deepEqual(tile('review', 2), ['2 signed off']);
862
+ });
863
+ test('walkthrough: a cleared floor shows no count of the work just done, and says its own clear line', () => {
864
+ const m = matter({ id: 'removal_proposals:61', source: 'removal_proposals', building: 'disputes', state: 'needs', title: 'Old search page', ref: { id: 61 } });
865
+ const dk = docket([m], { history: [], floors: [floorRow('disputes.removals', '2', '1 needs you')] });
866
+ const desk = D.deskModel({ key: 'disputes', floor: 1, docket: dk, now: NOW, done: { 'removal_proposals:61': true }, cleared: true, today: 2 });
867
+ assert.equal(desk.cleared, true);
868
+ assert.deepEqual(desk.tiles, [], 'no "2 needs you" tile on a floor you just cleared');
869
+ assert.deepEqual([desk.status, desk.clearLine, desk.clearSub], ['floor clear', 'No removals asked', 'lit up · 2 decided today']);
870
+ assert.deepEqual(D.breaks(desk), []);
871
+ // with a tool left on the floor (the invite), the status does not repeat the stale count
872
+ const join = matter({ id: 'access_requests:7', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Builder 4' }, ref: { id: 7 } });
873
+ const invite = { key: 'people.invite', status: 'live', rows: [{ id: 'people.invite:new', desk: 'people.invite', building: 'people', name: 'Invite someone', fact: 'by their GitHub name', facts: [], needs: false, tool: true, ref: { type: 'invite', id: 'new' }, acts: [{ key: 'invite', field: 'login' }] }] };
874
+ const left = D.deskModel({ key: 'people', floor: 4, docket: docket([join], { history: [], desks: [invite], floors: [floorRow('people.join', '1', '1 needs you')] }), now: NOW, done: { 'access_requests:7': true } });
875
+ assert.deepEqual(left.rows.map((r) => r.name), ['Invite someone']);
876
+ assert.equal(left.status, 'nothing needs you');
877
+ assert.equal(left.hot, false);
878
+ });
879
+ test('walkthrough: a count agrees with its noun, in the status and on the tile', () => {
880
+ const votes = [1, 2].map((n) => matter({ id: 'board:' + n, source: 'board', building: 'charter', state: 'needs', title: 'Vote ' + n }));
881
+ const two = D.deskModel({ key: 'charter', floor: 0, docket: docket(votes, { history: [] }), now: NOW });
882
+ assert.match(two.status, /^2 open votes · oldest/);
883
+ assert.deepEqual(two.tiles.map((t) => t.v + ' ' + t.l), ['2 open votes']);
884
+ const one = D.deskModel({ key: 'charter', floor: 0, docket: docket(votes.slice(0, 1), { history: [] }), now: NOW });
885
+ assert.match(one.status, /^1 open vote · oldest/);
886
+ });
887
+ test('walkthrough: a record item keeps its age, and what you did keeps its "now"', () => {
888
+ const hist = [
889
+ { id: 'a', building: 'release', title: 'The new sign-up', outcome: 'live', at: '2026-10-01T12:00:00.000Z' },
890
+ { id: 'b', building: 'release', title: 'Mira holds the Treasurer seat', outcome: 'granted', at: '2026-09-12T12:00:00.000Z' },
891
+ { id: 'c', building: 'release', title: 'Today it went out', outcome: '', at: '2026-10-03T09:00:00.000Z' },
892
+ ];
893
+ const desk = D.deskModel({ key: 'release', floor: 2, docket: docket([], { history: hist }), now: NOW, mine: ['voted yes A returned idea carries it back'] });
894
+ assert.deepEqual(desk.record.map((x) => x.text), ['voted yes A returned id… · now', 'The new sign-up · live · 2d', 'Mira holds the Treasurer… · 3w', 'Today it went out · today']);
895
+ assert.ok(desk.record.every((x) => x.text.length <= D.LIMITS.recordLen));
896
+ });