@bongos/core 1.25.30 → 1.25.32

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 (34) hide show
  1. package/.bongos-core.json +42 -47
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/api/openapi.json +3 -6
  4. package/docs/copy-inventory.md +13 -13
  5. package/docs/copy-registry.json +14 -14
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-readings.json +229 -203
  8. package/modules/government/docket.js +37 -6
  9. package/modules/hall-ui/public/city.js +1 -1
  10. package/modules/hall-ui/public/gov-feeds.js +2 -2
  11. package/modules/hall-ui/public/index.html +4 -4
  12. package/modules/hall-ui/public/shell-game.css +2 -3
  13. package/modules/hall-ui/public/shell-game.js +6 -5
  14. package/modules/hall-ui/public/welcome-boot.css +18 -31
  15. package/modules/hall-ui/public/welcome-boot.js +81 -97
  16. package/modules/hall-ui/records/start-screen.md +6 -6
  17. package/modules/hall-ui/records/welcome-tower.md +29 -20
  18. package/modules/lifecycle/routes/tasks.js +1 -1
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/release-notes.json +12 -0
  22. package/src/module-api.js +1 -1
  23. package/tests/docket_load_speed.mjs +99 -0
  24. package/tests/hall_city_living.mjs +1 -1
  25. package/tests/hall_gov_counts.mjs +1 -1
  26. package/tests/hall_gov_real_site.mjs +1 -1
  27. package/tests/hall_sky_time.mjs +3 -2
  28. package/tests/hall_start_screen.mjs +3 -2
  29. package/tests/hall_street_deck.mjs +1 -1
  30. package/tests/hall_street_depth.mjs +2 -2
  31. package/tests/welcome_boot.mjs +35 -27
  32. package/tests/welcome_boot_browser.mjs +25 -19
  33. package/modules/hall-ui/public/welcome-day.webp +0 -0
  34. package/modules/hall-ui/public/welcome-night.webp +0 -0
@@ -1,15 +1,21 @@
1
1
  # The welcome — Home's first visit of the day (tasks 1004785, 1004818)
2
2
 
3
3
  The owner's Builders' Hall idea 1001262, second half (the first is [`home-reshuffle.md`](home-reshuffle.md)).
4
- Files: `public/welcome-boot.js` + `public/welcome-boot.css`, and the owner's two paintings
5
- `public/welcome-day.webp` / `public/welcome-night.webp`, on Home only.
4
+ Files: `public/welcome-boot.js` + `public/welcome-boot.css`, on Home only. The scene is drawn by
5
+ Home's own city painters (`sky-time.js`, `city-draw.js`, `home-city-draw.js`).
6
6
 
7
7
  **How the look got here (2026-10-23, one day of owner rounds).** Task 1004785 built v5 "the tower"
8
8
  ([`docs/design/mocks/hall/welcome/v5.src.html`](../../../docs/design/mocks/hall/welcome/v5.src.html)),
9
9
  drawn live from the Repo Atlas. The owner then asked for day and night and a slow way out, then
10
10
  swapped the tower for a glowing module city with a lunch crew on a beam, dropped the crew, and
11
11
  finally had Gemini paint the city by day and by night and asked for those to be animated (task
12
- 1004818). The atlas-drawn scene is gone; the painting is the scene.
12
+ 1004818). The atlas-drawn scene is gone.
13
+
14
+ **The painting is retired too (task 1005308, owner 2026-11-14: "retire it everywhere").** The scene
15
+ is now the city in the clouds from task 1005196, the same city Home and the City page draw, in the
16
+ hour's sky. The two WebP paintings and every light that was pinned to their pixels (the circuit
17
+ towers' pulses, the hover cars, the moon and stars) are deleted. The game's start and load screens
18
+ stand on the same scene (`records/start-screen.md`).
13
19
 
14
20
  - **When.** A signed-in builder's first Home load of the LOCAL day, per builder id
15
21
  (`localStorage['hall-welcome-day:<id>']`). Storage that throws is never shown it, because it could not
@@ -20,15 +26,16 @@ finally had Gemini paint the city by day and by night and asked for those to be
20
26
  `welcome-boot.js` loads before the `shell-trail.js` / `craft-mode.js` / `shell.js` trio, which
21
27
  `tests/craft_mode.mjs` keeps adjacent.
22
28
  - **Day and night.** `timeOfDay` makes 06:00 to 17:59 local `day`, the rest `night`; the root wears
23
- `.wb--day` or `.wb--night` and shows the matching painting. It opens from a veil (`.wb__veil`),
24
- white by day and black by night, that lifts once the painting has loaded (or after 1.5s).
25
- - **The motion is laid on the painting, in its own pixels.** `.wb__art` covers the screen at the
26
- paintings' 1024x572 shape (both share one layout), so the SVG over it (`viewBox 0 0 1024 572`) keeps
27
- every light on its mark however the screen crops: `TOWERS` (the circuit towers, where `pulseRuns`
28
- climbs), `BEACONS` (mast lights, night), `LANES` (hover cars), the moon and stars (night), plus a
29
- slow drift of the whole picture and two clouds. **A new painting must keep that layout, or the
30
- coordinates in welcome-boot.js move with it** — `tests/welcome_boot.mjs` checks both files are
31
- 1024x572 WebP.
29
+ `.wb--day` or `.wb--night`, which sets the ink. It opens from a veil (`.wb__veil`), white by day
30
+ and black by night, that lifts two frames after the scene is painted (or after 1.5s). The scene's
31
+ own colours follow the hour more finely (`sky-time.js`: night, dawn, midday, dusk, blended).
32
+ - **The scene is the city, as a backdrop.** `paintScene(host)` asks `OTBHomeCityDraw.citySvg` for an
33
+ empty week (the Cloud Bongos core and the museum, standing in cloud) and rewrites it with
34
+ `sceneShape`: every link becomes a plain group, tooltips and goal hooks are dropped, the faded
35
+ `hc-quiet` look is lifted so the core leads, and every id is prefixed `cs-` so it never meets the
36
+ ids of Home's own city on the same page. It covers the screen, standing on the bottom edge
37
+ (`xMidYMax slice`), over `.cloud-scene`'s sky gradient, with a slow drift and two clouds. The same
38
+ `paintScene` is handed to the game's start screen as `scene`.
32
39
  - **Banner.** "Working on the core of <project>" from `__BRANDING__.identity` (never a hardcoded word),
33
40
  with `core <version>` under it when the public `GET /version` reports one.
34
41
  - **Sequence.** Veil lifts, the city powers up (pulses brighten), the greeting comes up at ~3.4s.
@@ -40,11 +47,13 @@ finally had Gemini paint the city by day and by night and asked for those to be
40
47
  Artisan → stays on Home (the Ideator door, once `/thinking`, folded into Artist). A pick also calls
41
48
  `OTBCraftMode.choose()` with patron / artist / artisan. **Patron shows only where
42
49
  `OTBCraftMode.offered('patron')`** — the patron craft is a seat — and the list is built when "Start with your role" is pressed, after the seats have settled.
43
- - **No painting** (a 404) hides the image and leaves the sky gradient; everything else works.
44
- - **Colours are the paintings'**, in both hall themes: `welcome-boot.css` and `welcome-boot.js` are on
45
- `tests/hall_audit.mjs`'s literal exemptions with that reason.
46
- - **Tests.** `tests/welcome_boot.mjs` (the day rule, the paintings' files and shape, every light inside
47
- the painting and every pulse inside its tower, the banner, the role map, the load order);
48
- `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode, welcome and
49
- paintings in a browser: once a day, Skip, Esc, no double picker, the seat rule, no painting, reduced
50
- motion, the veil by day and night, the slow exit, and that the old drawn scene is gone.
50
+ - **No city painters** on the page: the root wears `.wb--bare`, the hour's sky gradient stands alone,
51
+ and everything else works.
52
+ - **The scene's colours are the hour's sky tokens only** (`--sky-now-*`); the welcome's ink, veil and
53
+ panels keep their fixed day/night literals, on `tests/hall_audit.mjs`'s literal exemptions.
54
+ - **Tests.** `tests/welcome_boot.mjs` (the day rule, that the painting and its lights are gone, the
55
+ scene's shapes with no door, no unprefixed id and no typed colour, the banner, the role map, the
56
+ load order); `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode,
57
+ welcome and city painters in a browser: once a day, Skip, Esc, no double picker, the seat rule, no
58
+ painters, reduced motion, the city in the hour's sky by day and night, the slow exit, and that the
59
+ painting's lights are gone.
@@ -219,7 +219,7 @@ module.exports = function buildTasksRouter() {
219
219
  // RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
220
220
  const claimNeeds = parseNeedsFilter(req.query.needs); if (claimNeeds && claimNeeds.bad) return badNeedsFilter(res); // task 1004950
221
221
  const claimLimit = posInt(req.query.limit, 'bad_limit', 1000);
222
- if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' });
222
+ if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' }); if (req.query.view === 'claims') return res.json({ active_claims: await db.listActiveClaims() }); // open claims only, no claimable feed: the street needs just these (task 1005310)
223
223
  const [feed, active] = await Promise.all([
224
224
  db.listClaimableTasks({
225
225
  versionId: version || null, builderId: req.builder.id,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.30",
3
+ "version": "1.25.32",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.30",
9
+ "version": "1.25.32",
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.25.30",
3
+ "version": "1.25.32",
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",
@@ -11955,5 +11955,17 @@
11955
11955
  "id": "1005307",
11956
11956
  "text": "Choosing Patron in game mode no longer reloads the hall endlessly; it settles on the city."
11957
11957
  }
11958
+ ],
11959
+ "1.25.31": [
11960
+ {
11961
+ "id": "1005310",
11962
+ "text": "The governor's street asks the server for less and gets its answers sooner."
11963
+ }
11964
+ ],
11965
+ "1.25.32": [
11966
+ {
11967
+ "id": "1005308",
11968
+ "text": "The old circuit-city painting is gone. The welcome screen and the game's start screens now open on the city in the clouds, coloured for the time of day: deep night with lit windows, a warm dawn, a bright midday."
11969
+ }
11958
11970
  ]
11959
11971
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.30'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.32'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,99 @@
1
+ // tests/docket_load_speed.mjs — the governor's street loads faster (task 1005310).
2
+ //
3
+ // Measured on the live hub: the street asked for GET /government/docket and
4
+ // GET /government/counts together, counts rebuilt the same docket, and each build
5
+ // waited for the government reads before it even started its sources. Three
6
+ // properties now hold, and each is driven here against the real docket.js:
7
+ // 1. two asks for the SAME viewer while a build runs share that one build, and
8
+ // nothing is kept once it finishes (no cache: the next ask builds afresh);
9
+ // 2. the sources start reading before the government reads have answered;
10
+ // 3. a government read that fails is never an unhandled rejection while the
11
+ // sources are still reading, and the docket still answers or fails as before.
12
+ // And the street's claims read (GET /tasks/claimable?view=claims) answers only the
13
+ // open claims, without building the claimable feed.
14
+ import { strict as assert } from 'node:assert';
15
+ import { test, after } from 'node:test';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { createRequire } from 'node:module';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const require = createRequire(import.meta.url);
23
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
24
+ const docket = require(path.join(ROOT, 'modules', 'government', 'docket.js'));
25
+ const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
26
+ const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
27
+ const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
28
+
29
+ const REAL = { perms: resolver.resolveBuilderPermissions, rules: board.decisionRulesView };
30
+ after(() => { resolver.resolveBuilderPermissions = REAL.perms; board.decisionRulesView = REAL.rules; });
31
+
32
+ const order = [];
33
+ let permReads = 0;
34
+ // The docket's own atom only: no government gate, so no government tables are read.
35
+ resolver.resolveBuilderPermissions = async () => { permReads += 1; return ['board.vote.cast']; };
36
+ board.decisionRulesView = () => new Promise((r) => setTimeout(() => { order.push('decisions'); r(null); }, 40));
37
+ api.contribute(docket.CONTRIBUTION_POINT, {
38
+ source: 'blockers', building: 'planning', gate: { anyBuilder: true },
39
+ read: async () => { order.push('source'); return []; },
40
+ });
41
+
42
+ test('two asks for one viewer while a build runs share it; the next ask builds afresh', async () => {
43
+ permReads = 0;
44
+ const viewer = { id: 7001, rank: 'metic' };
45
+ const [a, b] = await Promise.all([docket.docketFor(viewer), docket.docketFor({ ...viewer })]);
46
+ assert.equal(permReads, 1, 'one build for both asks');
47
+ assert.equal(a, b, 'both asks receive the same answer');
48
+ await docket.docketFor(viewer);
49
+ assert.equal(permReads, 2, 'a finished build is not kept: the next ask builds again');
50
+ await Promise.all([docket.docketFor({ id: 7002 }), docket.docketFor({ id: 7003 })]);
51
+ assert.equal(permReads, 4, 'different viewers never share a build');
52
+ });
53
+
54
+ test('the sources start reading before the government reads answer', async () => {
55
+ order.length = 0;
56
+ await docket.docketFor({ id: 7004, rank: 'metic' });
57
+ assert.deepEqual(order, ['source', 'decisions']);
58
+ });
59
+
60
+ const VIEWER = { id: 42, rank: 'metic' };
61
+ const NOW = Date.parse('2026-11-14T12:00:00Z');
62
+ const slowSource = (ms) => ({
63
+ sources: new Map([['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true },
64
+ read: () => new Promise((r) => setTimeout(() => r([{ id: 1, title: 'Pick a host', state: 'needs', rule: 'decide' }]), ms)) }]]),
65
+ readings: new Map(), floors: new Map(), histories: new Map(), desks: new Map(),
66
+ });
67
+
68
+ test('a government read handed in as a promise answers exactly as its value does', async () => {
69
+ const plain = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW, decisions: [], government: null, seats: null });
70
+ const later = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW,
71
+ decisions: Promise.resolve([]), government: Promise.resolve(null), seats: Promise.resolve(null) });
72
+ assert.deepEqual(later, plain);
73
+ });
74
+
75
+ test('a failed government read is never an unhandled rejection while the sources read', async () => {
76
+ const unhandled = [];
77
+ const onUnhandled = (e) => unhandled.push(e);
78
+ process.on('unhandledRejection', onUnhandled);
79
+ try {
80
+ const boom = new Error('government unreadable');
81
+ await assert.rejects(docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(30), now: NOW,
82
+ decisions: Promise.resolve(null), government: Promise.reject(boom), seats: Promise.resolve(null) }), /government unreadable/);
83
+ await new Promise((r) => setTimeout(r, 20));
84
+ assert.deepEqual(unhandled, []);
85
+ } finally {
86
+ process.off('unhandledRejection', onUnhandled);
87
+ }
88
+ });
89
+
90
+ test('the claims read answers the open claims without building the claimable feed', () => {
91
+ const src = read('modules/lifecycle/routes/tasks.js');
92
+ const at = src.indexOf("req.query.view === 'claims'");
93
+ assert.ok(at > 0, 'GET /tasks/claimable has a claims-only view');
94
+ assert.ok(at < src.indexOf('db.listClaimableTasks({', at - 2000), 'it answers before the claimable feed is built');
95
+ assert.match(src.slice(at, at + 200), /active_claims: await db\.listActiveClaims\(\)/);
96
+ for (const f of ['modules/hall-ui/public/gov-feeds.js', 'modules/hall-ui/public/city.js']) {
97
+ assert.match(read(f), /tasks\/claimable\?view=claims/, `${f} asks for the claims alone`);
98
+ }
99
+ });
@@ -488,7 +488,7 @@ async function bootLiving({ asks = false, answer = null } = {}) {
488
488
  const feeds = {
489
489
  '/api/bongos/government/docket': asks ? withAsks(docketRouted()) : docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
490
490
  '/api/bongos/government/leads': leadsRead(), '/api/bongos/government/treasury': fx('government__treasury'),
491
- '/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?limit=1': fx('tasks__claimable'),
491
+ '/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'),
492
492
  '/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/public/cost-summary': fx('public__cost-summary'),
493
493
  '/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'), '/api/bongos/blockers?limit=100': fx('blockers'),
494
494
  '/api/bongos/autonomy/fence': fx('autonomy__fence'), '/api/bongos/moving': fx('moving'),
@@ -172,7 +172,7 @@ test('in the harness COUNT_FEEDS, with the docket the page already read, give th
172
172
  assert.equal(L.countsOf(m, s).on_others, L.countsOf(m).on_others - 1);
173
173
  // and nothing the shell reads is a task list
174
174
  assert.ok(!F.COUNT_FEEDS.includes('tasks') && !F.COUNT_FEEDS.includes('goals'), 'the tally reads no task or goal list');
175
- assert.match(F.FEEDS.claims, /limit=1\b/, 'the claims ride the claimable feed at one row');
175
+ assert.match(F.FEEDS.claims, /[?&]view=claims\b/, 'the claims ride the claims-only view of the claimable route (task 1005310)');
176
176
  });
177
177
 
178
178
  // ── 3. every surface prints the same numbers ────────────────────────────────
@@ -197,7 +197,7 @@ const FEED = {
197
197
  '/api/bongos/government/docket': 'government__docket', '/api/bongos/government/counts': 'government__counts', '/api/bongos/goals': 'goals', '/api/bongos/goals/rollups': 'goals__rollups',
198
198
  '/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster', '/api/bongos/blockers': 'blockers',
199
199
  '/api/bongos/public/cost-summary': 'public__cost-summary', '/api/bongos/tasks/claimable': 'tasks__claimable',
200
- '/api/bongos/tasks/claimable?limit=1': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
200
+ '/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
201
201
  '/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
202
202
  '/api/bongos/government/constitution': 'government__constitution',
203
203
  // the hub's law book, as the route answers it (no law passed yet)
@@ -112,8 +112,9 @@ test('the sky tokens are art: no control, no text and no chrome spends one', ()
112
112
  const sel = m[1].trim();
113
113
  if (f === 'style.css' && /(^|\n):root$/.test(sel)) continue; // the token block itself
114
114
  // only the two scenes spend them: the City page's frame, stage and cloud; Home's scene, its
115
- // stage, and (in game mode) the ground of the Home view
116
- if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
115
+ // stage, and (in game mode) the ground of the Home view; and the cloud scene behind the welcome
116
+ // and the game's start screens (task 1005308)
117
+ if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|\.cloud-scene|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
117
118
  for (const d of m[2].split(';')) {
118
119
  const prop = d.split(':')[0].trim();
119
120
  if (/--sky-/.test(d) && (prop === 'color' || /^border|^outline|^caret|^text-/.test(prop))) hits.push(`${f}: ${sel} { ${prop} }`);
@@ -95,13 +95,14 @@ test('the load screen fills one segment per week, up to the week it is showing (
95
95
  assert.equal(G.segsDone(0, 0), 0);
96
96
  });
97
97
 
98
- test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
98
+ test('a fresh sign-in is handed to the start screen with the city in the clouds, and the craft picker waits for the world', () => {
99
99
  const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
100
100
  const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
101
101
  assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
102
102
  assert.equal(s.started.length, 1);
103
103
  assert.equal(s.started[0].name, 'Harbor');
104
- assert.match(s.started[0].art, /^\/builders\/welcome-(day|night)\.webp$/);
104
+ assert.equal(typeof s.started[0].scene, 'function', 'the scene is painted, not a picture (task 1005308)');
105
+ assert.equal(s.started[0].art, undefined, 'the circuit-city painting is retired');
105
106
  assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
106
107
  assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
107
108
  s.started[0].done();
@@ -411,7 +411,7 @@ const FEED = {
411
411
  '/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster',
412
412
  '/api/bongos/blockers': 'blockers', '/api/bongos/public/cost-summary': 'public__cost-summary',
413
413
  // the living model's reads, as gov-feeds.js names them (one read each per page)
414
- '/api/bongos/tasks/claimable?limit=1': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
414
+ '/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
415
415
  '/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
416
416
  '/api/bongos/government/constitution': 'government__constitution', '/api/bongos/government/leads': 'government__leads',
417
417
  };
@@ -471,7 +471,7 @@ async function bootPage({ withLiving = true } = {}) {
471
471
  const F = {
472
472
  '/api/bongos/government/docket': docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
473
473
  '/api/bongos/government/leads': leadsRead(),
474
- '/api/bongos/tasks/claimable?limit=1': fx('tasks__claimable'), '/api/bongos/tasks?status=active': fx('tasks__status-active'),
474
+ '/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'), '/api/bongos/tasks?status=active': fx('tasks__status-active'),
475
475
  '/api/bongos/public/cost-summary': fx('public__cost-summary'), '/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'),
476
476
  // the living model's reads, as gov-feeds.js names them
477
477
  '/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/blockers?limit=100': fx('blockers'),
@@ -493,7 +493,7 @@ test('the page draws the living street: the skyline, a pod per claim, the plates
493
493
  const p = await bootPage();
494
494
  const all = (id) => walk(p.byId.get(id));
495
495
  const cls = (id, c) => all(id).filter((e) => (e.className || '').split(' ').includes(c));
496
- for (const u of ['/api/bongos/tasks/claimable?limit=1', '/api/bongos/government/leads']) assert.ok(p.calls.includes(u), u);
496
+ for (const u of ['/api/bongos/tasks/claimable?view=claims', '/api/bongos/government/leads']) assert.ok(p.calls.includes(u), u);
497
497
  assert.ok(!p.calls.some((u) => /governor-overlay/.test(u)), 'no overlay is read');
498
498
  assert.equal(p.calls.filter((u) => u === '/api/bongos/me').length, 1, 'the living feeds are read once');
499
499
  assert.equal(cls('streetDepthNear', 'street-depth-sil__face').length, 1);
@@ -1,8 +1,8 @@
1
1
  // tests/welcome_boot.mjs — the welcome's pure parts (tasks 1004785, 1004818).
2
2
  //
3
3
  // WHY. The welcome decides things a reader would only notice going wrong over days: whether today
4
- // is the first visit, whether it is day or night (and so which painting), what the banner names,
5
- // where each role goes, and whether the motion still sits on the painting. Those are pure, so this
4
+ // is the first visit, whether it is day or night, what the banner names, where each role goes,
5
+ // and that the scene is the city in the clouds with its doors taken out (task 1005308). Those are pure, so this
6
6
  // runs the REAL welcome-boot.js (it is Node-require-safe). The overlay itself is
7
7
  // tests/welcome_boot_browser.mjs.
8
8
  import assert from 'node:assert/strict';
@@ -39,40 +39,48 @@ test('the greeting follows the local hour, and the name falls back sensibly', ()
39
39
  assert.equal(W.nameOf({}), 'builder');
40
40
  });
41
41
 
42
- test('day is 06:00 to 17:59 local, night the rest, and each has its painting (task 1004818)', () => {
42
+ test('day is 06:00 to 17:59 local, night the rest (the ink and the veil follow it)', () => {
43
43
  const at = (h, m = 0) => W.timeOfDay(new Date(2026, 9, 23, h, m));
44
44
  assert.deepEqual([at(5, 59), at(6), at(12), at(17, 59), at(18), at(23, 30), at(0)],
45
45
  ['night', 'day', 'day', 'day', 'night', 'night', 'night']);
46
- assert.equal(W.artFor('day'), '/builders/welcome-day.webp');
47
- assert.equal(W.artFor('night'), '/builders/welcome-night.webp');
48
46
  });
49
47
 
50
- test('both paintings ship, as WebP, in the one shape the motion is laid out for', () => {
51
- for (const url of Object.values(W.ART)) {
52
- const file = path.join(HALL, url.replace(/^\/builders\//, ''));
53
- const buf = fs.readFileSync(file);
54
- assert.equal(buf.toString('latin1', 0, 4) + buf.toString('latin1', 8, 12), 'RIFFWEBP', url + ' is a WebP');
55
- assert.ok(buf.length < 400 * 1024, url + ' stays small enough for a first paint');
56
- // The VP8X header carries the canvas size (24-bit, minus one) at bytes 24..29.
57
- assert.equal(buf.toString('latin1', 12, 16), 'VP8X', url + ' carries its size');
58
- const w = 1 + buf.readUIntLE(24, 3), h = 1 + buf.readUIntLE(27, 3);
59
- assert.deepEqual([w, h], [W.AW, W.AH], url + ' is the painting the lights were placed on');
48
+ // Task 1005308 (owner, 2026-11-14): the circuit-city painting is retired everywhere; the scene is the
49
+ // city in the clouds Home's own painters draw (task 1005196), as a backdrop.
50
+ test('the circuit-city painting is retired: no picture file, no lights pinned to its pixels', () => {
51
+ for (const name of ['welcome-day.webp', 'welcome-night.webp']) assert.equal(fs.existsSync(path.join(HALL, name)), false, name + ' is gone');
52
+ for (const gone of ['ART', 'artFor', 'TOWERS', 'LANES', 'AW', 'AH', 'pulseRuns', 'BEACONS']) assert.equal(W[gone], undefined, gone + ' went with the painting');
53
+ for (const file of ['welcome-boot.js', 'welcome-boot.css', 'shell-game.js', 'shell-game.css', 'index.html']) {
54
+ assert.doesNotMatch(fs.readFileSync(path.join(HALL, file), 'utf8'), /welcome-(day|night)\.webp|wb__img|gs__img|wb-motion/, file + ' names no part of the painting');
60
55
  }
61
56
  });
62
57
 
63
- test('every light sits inside the painting, and every pulse climbs inside its tower', () => {
64
- const inside = ([x, y]) => x >= 0 && x <= W.AW && y >= 0 && y <= W.AH;
65
- assert.equal(W.BEACONS, undefined, 'the blinking mast lights are gone (task 1005087)');
66
- for (const t of W.TOWERS) assert.ok(inside([t.x, t.y]) && inside([t.x + t.w, t.y + t.h]), 'tower inside the painting');
67
- const runs = W.pulseRuns(W.TOWERS, 3, 7);
68
- assert.equal(runs.length, W.TOWERS.length * 3);
69
- for (const r of runs) {
70
- const t = W.TOWERS[r.tower];
71
- assert.ok(r.x >= t.x && r.x <= t.x + t.w, 'a pulse runs up its own tower');
72
- assert.ok(r.y1 > r.y0 && r.y0 >= t.y && r.y1 <= t.y + t.h, 'bottom to top, within the tower');
73
- assert.ok(r.dur > 0 && r.delay >= 0);
58
+ test("the scene is the city in the clouds, drawn by Home's own painters, with every door taken out", async () => {
59
+ const vm = await import('node:vm');
60
+ const ctx = { window: {}, console };
61
+ vm.createContext(ctx);
62
+ vm.runInContext(fs.readFileSync(path.join(HALL, 'city-draw.js'), 'utf8'), ctx);
63
+ const Gov = ctx.window.GovCity;
64
+ const Draw = require(path.join(HALL, 'home-city-draw.js'));
65
+ assert.ok(Gov && Draw, 'the painters load');
66
+ const s = W.sceneShapes(Draw, Gov);
67
+ assert.match(s.viewBox, /^0 0 1000 \d+$/);
68
+ const all = [];
69
+ (function walk(n) { all.push(n); n.kids.forEach(walk); })({ tag: 'root', attrs: {}, kids: s.nodes });
70
+ assert.ok(all.some((n) => /hc-cloud/.test(n.attrs.class || '')), 'it stands in cloud');
71
+ assert.ok(all.filter((n) => n.tag === 'path').length > 50, 'the core and the museum are drawn');
72
+ assert.equal(all.filter((n) => n.tag === 'a').length, 0, 'no link: it is a backdrop');
73
+ for (const n of all) {
74
+ for (const k of ['href', 'tabindex', 'aria-label', 'data-tip', 'data-tip-body', 'data-goal']) assert.equal(n.attrs[k], undefined, k + ' is taken out');
75
+ if (n.attrs.id) assert.ok(n.attrs.id.startsWith(W.SCENE_ID), "its ids never meet Home's own city's");
76
+ for (const v of Object.values(n.attrs)) assert.doesNotMatch(String(v), /url\(#(?!cs-)/, 'every reference points at its own prefixed id');
77
+ assert.doesNotMatch(n.attrs.class || '', /hc-quiet/, 'nothing is faded: the core leads');
74
78
  }
75
- assert.deepEqual(W.pulseRuns(W.TOWERS, 3, 7), runs, 'seeded: every visit draws the same picture');
79
+ const json = JSON.stringify(s);
80
+ assert.doesNotMatch(json, /#[0-9a-f]{6}\b/i, "no typed colour: the hour's sky tokens only");
81
+ assert.match(json, /--sky-now-/, "it paints in the hour's sky");
82
+ assert.equal(W.sceneShapes(null, Gov), null, 'no painters: no city, and the sky stands alone');
83
+ assert.equal(W.paintScene(null), false);
76
84
  });
77
85
 
78
86
  test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
@@ -1,9 +1,9 @@
1
1
  // tests/welcome_boot_browser.mjs — the welcome in a real browser (tasks 1004785, 1004818).
2
2
  //
3
3
  // WHY. "Once a day", "Skip", "the craft picker does not also open", "Patron only with a seat", the day
4
- // and night paintings and the slow way out are behaviour across the real shell.js, craft-mode.js and
4
+ // and night veils over the city in the clouds and the slow way out are behaviour across the real shell.js, craft-mode.js and
5
5
  // welcome-boot.js together — the shell is what decides which of the two greets a builder — so this
6
- // serves the REAL Home markup, the REAL hall scripts and the REAL paintings from a routed origin
6
+ // serves the REAL Home markup and the REAL hall scripts (the city's painters too) from a routed origin
7
7
  // (localStorage needs one) in the ui-design kit's launcher. Every wait is on DOM state, never on a
8
8
  // flag that flips before the DOM settles (the task 1004780 CI flake). Skips when the machine has no
9
9
  // system Edge or Chrome, as ui_design_kit_fit does.
@@ -33,7 +33,9 @@ const INDEX = fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')
33
33
  .replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
34
34
  .replace(/\{\{[^}]*\}\}/g, '');
35
35
  const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
36
- const SCRIPTS = ['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
36
+ // The city's three painters draw the scene (task 1005308: the city in the clouds, not a painting).
37
+ const PAINTERS = ['sky-time.js', 'city-draw.js', 'home-city-draw.js'];
38
+ const SCRIPTS = ['dom-utils.js', ...PAINTERS, 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
37
39
  .map((f) => `<script src="/builders/${f}"></script>`).join('');
38
40
  const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
39
41
 
@@ -46,7 +48,7 @@ async function hall({ art = true, reducedMotion = 'no-preference', time = null }
46
48
  if (p === '/api/bongos/auth/web/start') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>github</title><p id="auth">sign in</p>' });
47
49
  if (p === '/studio' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
48
50
  const ext = path.extname(p);
49
- if (ext === '.webp' && !art) return route.fulfill({ status: 404, body: '' });
51
+ if (!art && PAINTERS.includes(path.basename(p))) return route.fulfill({ status: 404, body: '' });
50
52
  const f = path.join(HALL, p.replace(/^\/builders\//, ''));
51
53
  if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
52
54
  return route.fulfill({ status: 404, body: '' });
@@ -91,8 +93,8 @@ test('first visit of the day: the welcome, not the craft picker; Skip brings the
91
93
  'the welcome shows, the picker is marked asked and does not stack on it');
92
94
  assert.equal(await page.evaluate(() => document.querySelector('.wb__banner').tagName), 'DIV',
93
95
  'over a signed-in hall the shell keeps the one <header>; the banner is a plain block (task 1005080)');
94
- // The painting loads and its motion is laid over it.
95
- await page.waitForFunction(() => { const i = document.querySelector('#welcome-boot .wb__img'); return i && i.complete && i.naturalWidth === 1024; });
96
+ // The city in the clouds is drawn behind it (task 1005308).
97
+ await page.waitForSelector('#welcome-boot .cloud-scene .cloud-scene__svg path');
96
98
  assert.equal(await page.isVisible('[data-wb-skip]'), true);
97
99
  await page.click('[data-wb-skip]');
98
100
  await done(page);
@@ -227,17 +229,18 @@ test('roles: a seat holder sees Patron; Artisan stays on Home in Artisan', { ski
227
229
  await ctx.close();
228
230
  });
229
231
 
230
- test('no painting: the greeting and the choice still come up, over the sky colour', { skip }, async () => {
232
+ test('no city painters: the greeting and the choice still come up, over the sky colour', { skip }, async () => {
231
233
  const ctx = await hall({ art: false });
232
234
  const { page } = await open(ctx);
233
235
  await signIn(page);
234
236
  await done(page);
235
237
  const s = await page.evaluate(() => ({
236
238
  bare: document.getElementById('welcome-boot').classList.contains('wb--bare'),
237
- img: document.querySelector('#welcome-boot .wb__img').hidden,
239
+ city: !!document.querySelector('#welcome-boot .cloud-scene__svg'),
240
+ sky: !!document.querySelector('#welcome-boot .cloud-scene'),
238
241
  choices: document.querySelectorAll('.wb__choice').length,
239
242
  }));
240
- assert.deepEqual(s, { bare: true, img: true, choices: 2 });
243
+ assert.deepEqual(s, { bare: true, city: false, sky: true, choices: 2 });
241
244
  await ctx.close();
242
245
  });
243
246
 
@@ -256,15 +259,16 @@ const look = (page) => page.evaluate(() => {
256
259
  return {
257
260
  day: r.classList.contains('wb--day'), night: r.classList.contains('wb--night'),
258
261
  veil: getComputedStyle(r.querySelector('.wb__veil')).backgroundColor,
259
- art: r.querySelector('.wb__img').getAttribute('src'),
260
- beacons: r.querySelectorAll('.wb-beacon').length,
262
+ city: !!r.querySelector('.cloud-scene__svg'),
263
+ hour: document.documentElement.getAttribute('data-sky-hour'),
264
+ img: r.querySelectorAll('img:not(.wb__logo)').length,
261
265
  };
262
266
  });
263
267
 
264
- test('by day the day painting opens from a white veil; by night the night one from black, with its lights', { skip }, async () => {
268
+ test('by day the city opens from a white veil, by night from black, in the sky of that hour (task 1005308)', { skip }, async () => {
265
269
  for (const [time, want] of [
266
- [new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)', art: '/builders/welcome-day.webp', beacons: 0 }],
267
- [new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)', art: '/builders/welcome-night.webp', beacons: 0 }], // task 1005087 took the beacons out
270
+ [new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)', city: true, hour: '10', img: 0 }],
271
+ [new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)', city: true, hour: '22', img: 0 }],
268
272
  ]) {
269
273
  const ctx = await hall({ time });
270
274
  const { page } = await open(ctx);
@@ -305,19 +309,21 @@ test('the banner is the animated logo, the motion is laid on, and the drawn towe
305
309
  const ctx = await hall();
306
310
  const { page } = await open(ctx);
307
311
  await signIn(page);
308
- await page.waitForSelector('#welcome-boot.wb--lit .wb-motion');
312
+ await page.waitForSelector('#welcome-boot.wb--lit .cloud-scene__svg');
309
313
  const s = await page.evaluate(() => ({
310
314
  banner: document.querySelector('.wb__banner').textContent,
311
315
  name: document.querySelector('.wb__banner .wb__name').textContent,
312
316
  mark: !!document.querySelector('.wb__banner svg.wb__mark .wb__moonlet'),
313
317
  ringAnim: getComputedStyle(document.querySelector('.wb__ring')).animationName,
314
- pulses: document.querySelectorAll('#wb-fx .wb-pulse').length,
315
- cars: document.querySelectorAll('#wb-fx .wb-car').length,
316
- old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-scene, #wb-beam, #wb-city').length,
318
+ // anywhere on the page, under any container: the painting's lights must not come back
319
+ pulses: document.querySelectorAll('.wb-pulse, .wb-pulses').length,
320
+ cars: document.querySelectorAll('.wb-car').length,
321
+ // every earlier scene: the drawn tower and city, the painting and its light layer
322
+ old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-beam, #wb-city, #wb-fx, .wb-motion, .wb-moon, .wb-star, .wb__img').length,
317
323
  }));
318
324
  assert.doesNotMatch(s.banner, /Working on the core of/, 'task 1004953: the banner text is gone');
319
325
  assert.deepEqual({ name: s.name, mark: s.mark, ringAnim: s.ringAnim, pulses: s.pulses, cars: s.cars, old: s.old },
320
- { name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 21, cars: 3, old: 0 });
326
+ { name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 0, cars: 0, old: 0 }, 'the painting\'s lights went with it (task 1005308)');
321
327
  await ctx.close();
322
328
  });
323
329