@bongos/core 1.21.90 → 1.21.92

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.
Files changed (46) hide show
  1. package/.bongos-core.json +67 -52
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +4 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/api/openapi.json +36 -1
  8. package/docs/api-reference.md +7 -1
  9. package/docs/copy-inventory.md +364 -374
  10. package/docs/copy-registry.json +566 -648
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +15 -18
  13. package/docs/page-readings.json +1500 -1191
  14. package/modules/hall-ui/public/craft-mode.js +60 -0
  15. package/modules/hall-ui/public/style.css +32 -0
  16. package/modules/hall-ui/records/craft-mode.md +1 -1
  17. package/modules/lifecycle/module.json +2 -1
  18. package/modules/lifecycle/moving.js +55 -0
  19. package/modules/lifecycle/routes/moving.js +30 -0
  20. package/modules/public-landing/public/projects-hall-live.states.json +6 -35
  21. package/modules/public-landing/public/projects.html +1019 -983
  22. package/modules/public-landing/public/projects.probes.json +15 -18
  23. package/modules/public-landing/public/projects.states.json +22 -25
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/release-notes.json +12 -0
  27. package/src/module-api.js +1 -1
  28. package/tests/craft_mode_tallies.mjs +149 -0
  29. package/tests/onboard.mjs +8 -8
  30. package/tests/project_invite_ui.mjs +6 -5
  31. package/tests/projects_hub.mjs +77 -98
  32. package/tests/projects_hub_app_status.mjs +3 -3
  33. package/tests/projects_hub_app_step.mjs +8 -8
  34. package/tests/projects_hub_cloud_hop.mjs +29 -53
  35. package/tests/projects_hub_look.mjs +29 -39
  36. package/tests/projects_hub_module_picker.mjs +32 -39
  37. package/tests/projects_hub_pre_uat.mjs +11 -14
  38. package/tests/wizard_demo.mjs +30 -25
  39. package/tests/wizard_draft_resume.mjs +24 -32
  40. package/tests/wizard_front_door.mjs +91 -243
  41. package/tests/wizard_intent_resume.mjs +28 -48
  42. package/tests/wizard_one_page.mjs +201 -0
  43. package/tests/wizard_physics.mjs +46 -41
  44. package/tests/wizard_physics_more.mjs +71 -106
  45. package/tests/wizard_ship_mode.mjs +1 -1
  46. package/tests/wizard_six_screens.mjs +0 -129
@@ -196,6 +196,64 @@
196
196
  if (mark) mark.textContent = look;
197
197
  }
198
198
 
199
+ // ---------- Governing's header tallies (task 1004563) ----------
200
+ // "needs you N · moving M" beside the eyebrow, as on the four-mode prototype.
201
+ // Needs you is the open-blocker total GET /blockers already carries; moving is
202
+ // tasks being built or waiting to land (active + confirmed, owner 2026-10-08),
203
+ // counted by the server at GET /moving. Read once per page. A figure that cannot
204
+ // be read is LEFT OUT, never shown as 0: zero would say nothing waits on you.
205
+ var TALLY_API = '/api/bongos';
206
+ var tallyAsk = null;
207
+ function readCount(path, pick) {
208
+ if (typeof fetch !== 'function') return Promise.resolve(null);
209
+ return fetch(TALLY_API + path, { credentials: 'same-origin', headers: { Accept: 'application/json' } })
210
+ .then(function (r) { return r.ok ? r.json() : null; })
211
+ .then(function (j) {
212
+ var n = j ? pick(j) : null;
213
+ return (typeof n === 'number' && isFinite(n) && n >= 0) ? n : null;
214
+ }, function () { return null; });
215
+ }
216
+ function askTallies() {
217
+ if (!tallyAsk) {
218
+ tallyAsk = Promise.all([
219
+ readCount('/blockers?limit=1', function (j) { return j.page && j.page.total; }),
220
+ readCount('/moving', function (j) { return j.moving; }),
221
+ ]).then(function (v) { return { needs: v[0], moving: v[1] }; });
222
+ }
223
+ return tallyAsk;
224
+ }
225
+ function tallyHtml(t) {
226
+ var parts = [];
227
+ function one(label, n, due) {
228
+ return '<span class="craft-tally' + (due ? ' craft-tally--due' : '') + '">'
229
+ + '<span class="craft-tally__label">' + label + '</span> '
230
+ + '<span class="craft-tally__n">' + n + '</span></span>';
231
+ }
232
+ if (t && t.needs != null) parts.push(one('needs you', t.needs, t.needs > 0));
233
+ if (t && t.moving != null) parts.push(one('moving', t.moving, false));
234
+ return parts.join(' <span class="craft-tallies__sep" aria-hidden="true">·</span> ');
235
+ }
236
+ function applyTallies(mode) {
237
+ var box = document.getElementById('craft-tallies');
238
+ if (mode !== 'governing') { remove(box); return; }
239
+ var mark = document.getElementById('craft-eyebrow');
240
+ if (!box && mark && mark.parentNode) {
241
+ box = document.createElement('span');
242
+ box.id = 'craft-tallies';
243
+ box.className = 'craft-tallies';
244
+ box.setAttribute('hidden', '');
245
+ mark.parentNode.insertBefore(box, mark.nextSibling);
246
+ }
247
+ if (!box) return;
248
+ askTallies().then(function (t) {
249
+ var live = document.getElementById('craft-tallies');
250
+ if (!live || document.documentElement.dataset.craft !== 'governing') return;
251
+ var html = tallyHtml(t);
252
+ live.innerHTML = html;
253
+ if (html) live.removeAttribute('hidden'); else live.setAttribute('hidden', '');
254
+ });
255
+ }
256
+
199
257
  // ---------- each mode's tools (task 1004496 onward) ----------
200
258
  // Artistry has two: the palette (a feedback doc, page-notes.js) and the
201
259
  // paintbrush (the page's words as a wording change, paintbrush.js). Ideating
@@ -311,6 +369,7 @@
311
369
  if (sw) sw.classList.toggle('craft-switch--unset', !mode);
312
370
  applyNav();
313
371
  applyLook(mode);
372
+ applyTallies(mode);
314
373
  applyTools(mode);
315
374
  try { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: mode } })); } catch (_) { /* old engine */ }
316
375
  }
@@ -521,6 +580,7 @@
521
580
  applyNav: applyNav,
522
581
  LOOKS: LOOKS,
523
582
  artHtml: artHtml,
583
+ tallyHtml: tallyHtml,
524
584
  TOOLS: TOOLS,
525
585
  switchHtml: switchHtml,
526
586
  mount: mount,
@@ -1472,6 +1472,38 @@ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing
1472
1472
  flex-shrink: 0;
1473
1473
  }
1474
1474
  @media (max-width: 640px) { .craft-eyebrow { display: none; } }
1475
+ /* Governing's header tallies (craft-mode.js applyTallies, task 1004563):
1476
+ "needs you N · moving M" after the eyebrow. The warm dot marks needs-you only
1477
+ while something waits. Hidden at <=960px, earlier than the eyebrow: below that
1478
+ they squeeze the jump button under its own label (measured at 800px). */
1479
+ .craft-tallies {
1480
+ display: inline-flex;
1481
+ align-items: baseline;
1482
+ gap: 10px;
1483
+ white-space: nowrap;
1484
+ flex-shrink: 0;
1485
+ font-size: 12px;
1486
+ }
1487
+ .craft-tallies[hidden] { display: none; }
1488
+ .craft-tally { display: inline-flex; align-items: baseline; gap: 6px; }
1489
+ .craft-tally__label {
1490
+ font-family: var(--label-face);
1491
+ font-size: 11px;
1492
+ letter-spacing: .12em;
1493
+ text-transform: uppercase;
1494
+ color: var(--ink-soft);
1495
+ }
1496
+ .craft-tally__n { color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 500; }
1497
+ .craft-tally--due::before {
1498
+ content: '';
1499
+ align-self: center;
1500
+ width: 7px;
1501
+ height: 7px;
1502
+ border-radius: 999px;
1503
+ background: var(--accent);
1504
+ }
1505
+ .craft-tallies__sep { color: var(--ink-faint); }
1506
+ @media (max-width: 960px) { .craft-tallies { display: none; } }
1475
1507
  /* The signature: the mode's door scene (craft-mode.js artHtml, the same art the
1476
1508
  picker draws) framed under the brand, 40px in the closed rail and 64px open.
1477
1509
  The 110px-tall scene is scaled to the plate's height and widened to fill it,
@@ -34,7 +34,7 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
34
34
  - **The accent stays the action colour.** `--craft` paints only the switch's current pill, the current tab (tint, icon, label, marker) and the top bar's hairline.
35
35
  - **Two pieces are drawn by `craft-mode.js` `applyLook()`, on every mode change:** the eyebrow beside `#topbar-title` (`LOOKS`: "the studio · night", "the sky", "the city"; hidden at <=640px, where it overflowed phones) and the scene plate under `.app-sidebar__brand` (40px closed rail, 64px open). The plate's markup IS `artHtml(mode)`, the picker's door art, exported rather than copied; light mode recolours it to each scene's day (the city is the dark one through `invert(1) hue-rotate(180deg)`). It does not animate.
36
36
  - **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
37
- - **Governing's header tallies** ("needs you · moving") are task 1004563, not this look.
37
+ - **Governing's header tallies** (task 1004563): `applyTallies()` draws "needs you N · moving M" right after the eyebrow, Governing only, hidden at <=960px (below that they squeezed the jump button under its own label, measured at 800px). Needs you is `GET /blockers?limit=1`'s `page.total`; moving is `GET /moving` (lifecycle, tasks `active` + `confirmed`, owner 2026-10-08), a count in SQL so the page never pulls task rows. Read once per page; a figure that cannot be read is LEFT OUT, never drawn as 0. The warm dot marks needs-you only above zero. It toggles the `hidden` ATTRIBUTE, never `.hidden =` (the nav gate owns that property; `tests/craft_mode_nav.mjs`). Pinned by `tests/craft_mode_tallies.mjs`.
38
38
 
39
39
  ## The paintbrush (task 1004497)
40
40
 
@@ -32,7 +32,8 @@
32
32
  "artist-gate",
33
33
  "removal-proposals",
34
34
  "sky",
35
- "goal-task-relevance"
35
+ "goal-task-relevance",
36
+ "moving"
36
37
  ],
37
38
  "pollers": [
38
39
  "publish-reconciler"
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/moving.js — how much of the project is MOVING right now, as one
4
+ // count (task 1004563, goal 1000095 WA6).
5
+ //
6
+ // WHY. Governing mode's top bar reads "needs you N · moving M" (the four-mode
7
+ // prototype the owner named the standard on 2026-10-03). "Needs you" already had
8
+ // a source — the public open-blocker list carries its own total — but "moving" had
9
+ // none, and the only way to get one from the browser was GET /tasks?status=…, which
10
+ // returns every row with its whole description to produce a single number. That is
11
+ // the read the hall's bounded-reads rule forbids (modules/hall-ui/CLAUDE.md: a Home
12
+ // card asks the server for what it draws). So the number is counted here, in SQL.
13
+ //
14
+ // WHAT COUNTS (owner decision, 2026-10-08): a task being built right now plus a task
15
+ // waiting to land — status `active` or `confirmed`. Not `completed` (a ship waiting on
16
+ // its grade is neither being built nor queued to land) and nothing terminal. The two
17
+ // halves come back separately as well as summed, so a caller can say which is which.
18
+ //
19
+ // MODULE BOUNDARY: reaches core only through the published doorway.
20
+
21
+ const api = require('../../src/module-api');
22
+
23
+ const MOVING_STATUSES = Object.freeze(['active', 'confirmed']);
24
+
25
+ // → { moving, active, confirmed }. `deps.pool` is the #538 test seam, honoured only
26
+ // under NODE_ENV==='test' like its siblings in this module.
27
+ async function countMoving(deps = {}) {
28
+ const pool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : api.pool;
29
+ const { rows } = await pool.query(
30
+ `SELECT status, count(*)::int AS n
31
+ FROM tasks
32
+ WHERE status = ANY($1::text[])
33
+ GROUP BY status`,
34
+ [MOVING_STATUSES],
35
+ );
36
+ const by = Object.fromEntries(rows.map((r) => [r.status, Number(r.n) || 0]));
37
+ const active = by.active || 0;
38
+ const confirmed = by.confirmed || 0;
39
+ return { moving: active + confirmed, active, confirmed };
40
+ }
41
+
42
+ // The GET /moving handler, injectable so a test drives it with no express.
43
+ function movingHandler({ log, count = countMoving } = {}) {
44
+ return async function movingRoute(_req, res) {
45
+ try {
46
+ res.set('Cache-Control', 'private, max-age=30');
47
+ res.json(await count());
48
+ } catch (err) {
49
+ if (log) log.error('GET /moving', err);
50
+ res.fail('moving_failed', { status: 500, message: 'internal error' });
51
+ }
52
+ };
53
+ }
54
+
55
+ module.exports = { MOVING_STATUSES, countMoving, movingHandler };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/routes/moving.js — GET /moving, how many tasks are being built or
4
+ // waiting to land (task 1004563, goal 1000095 WA6).
5
+ //
6
+ // WHY ITS OWN ROUTE FILE. routes/tasks.js sits at the 1,500-line ratchet, and the path
7
+ // is top-level `/moving`, never `/tasks/moving`: the manifest mounts `tasks` first and
8
+ // its `/tasks/:id` would swallow a later literal segment (the `/inbox/awaiting-nod`
9
+ // trap). The count and the handler live in ../moving.js so a test drives them with no
10
+ // express and no database.
11
+ //
12
+ // MODULE BOUNDARY: reaches core only through the published doorway
13
+ // `../../../src/module-api`; `../moving` is this module's own read.
14
+
15
+ const express = require('express');
16
+ const api = require('../../../src/module-api');
17
+ const moving = require('../moving');
18
+
19
+ const log = api.logger('lifecycle');
20
+
21
+ module.exports = function buildMovingRouter() {
22
+ const router = express.Router();
23
+
24
+ // rank: any-builder — GET /moving, the count behind Governing mode's "moving" tally.
25
+ // A count of rows every signed-in builder can already list on the Tasks board, so no
26
+ // floor above sign-in; nothing here is writable.
27
+ router.get('/moving', api.requireBuilder, moving.movingHandler({ log }));
28
+
29
+ return router;
30
+ };
@@ -32,22 +32,6 @@
32
32
  "wait",
33
33
  600
34
34
  ],
35
- [
36
- "click",
37
- "#termsAck"
38
- ],
39
- [
40
- "wait",
41
- 100
42
- ],
43
- [
44
- "click",
45
- "#wizStart"
46
- ],
47
- [
48
- "wait",
49
- 400
50
- ],
51
35
  [
52
36
  "fill",
53
37
  "#newName",
@@ -62,14 +46,6 @@
62
46
  "#f-desc",
63
47
  "A co-op farming game where the seasons never repeat."
64
48
  ],
65
- [
66
- "click",
67
- "#panel-what button[type=submit]"
68
- ],
69
- [
70
- "wait",
71
- 400
72
- ],
73
49
  [
74
50
  "select",
75
51
  "#physCountry",
@@ -91,13 +67,9 @@
91
67
  "click",
92
68
  "[data-phys=\"entity\"][data-val=\"intent-solo\"]"
93
69
  ],
94
- [
95
- "click",
96
- "#physNext"
97
- ],
98
70
  [
99
71
  "wait",
100
- 700
72
+ 300
101
73
  ],
102
74
  [
103
75
  "click",
@@ -135,11 +107,11 @@
135
107
  ],
136
108
  [
137
109
  "click",
138
- "#reviewGo"
110
+ "#termsAck"
139
111
  ],
140
112
  [
141
113
  "wait",
142
- 500
114
+ 100
143
115
  ],
144
116
  [
145
117
  "click",
@@ -159,12 +131,11 @@
159
131
  "#panel-done",
160
132
  "#wizInvite",
161
133
  "#wizInviteWho",
162
- "#humanSteps li.cur",
163
- "#stepPager",
164
- "#stepNext"
134
+ "#humanSteps"
165
135
  ],
166
136
  "hidden": [
167
- "#hop"
137
+ "#hop",
138
+ "#wizParts"
168
139
  ],
169
140
  "text": [
170
141
  [