@bongos/core 1.23.9 → 1.23.11

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 (48) hide show
  1. package/.bongos-core.json +106 -41
  2. package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +1 -0
  3. package/docs/copy-inventory.md +900 -725
  4. package/docs/copy-registry.json +3055 -1246
  5. package/docs/file-map.md +1 -0
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/page-inventory.json +49 -6
  8. package/docs/page-readings.json +1247 -1157
  9. package/modules/public-landing/public/account.html +12 -0
  10. package/modules/public-landing/public/assets/cosmos.css +9 -1
  11. package/modules/public-landing/public/assets/hub-bar.js +5 -3
  12. package/modules/public-landing/public/community-empty.states.json +15 -0
  13. package/modules/public-landing/public/community-recruiter.states.json +15 -0
  14. package/modules/public-landing/public/community.html +470 -0
  15. package/modules/public-landing/public/community.probes.json +92 -0
  16. package/modules/public-landing/public/community.states.json +25 -0
  17. package/modules/public-landing/public/connections-nosuggest.states.json +15 -0
  18. package/modules/public-landing/public/connections.html +33 -2
  19. package/modules/public-landing/public/connections.states.json +1 -0
  20. package/modules/public-landing/public/contact.html +12 -0
  21. package/modules/public-landing/public/guild.html +12 -0
  22. package/modules/public-landing/public/index.html +23 -2
  23. package/modules/public-landing/public/index.probes.json +1 -1
  24. package/modules/public-landing/public/invitations.html +12 -0
  25. package/modules/public-landing/public/privacy.html +12 -0
  26. package/modules/public-landing/public/projects.html +15 -1
  27. package/modules/public-landing/public/terms.html +12 -0
  28. package/modules/ui-design/kit/fixtures/community-leaderboard-empty.json +4 -0
  29. package/modules/ui-design/kit/fixtures/community-leaderboard.json +28 -0
  30. package/modules/ui-design/kit/fixtures/community-search.json +10 -0
  31. package/modules/ui-design/kit/fixtures/connections-nosuggest.json +12 -0
  32. package/modules/ui-design/kit/fixtures/scouting-recruiter.json +11 -0
  33. package/modules/ui-design/kit/fixtures/scouting.json +4 -0
  34. package/modules/ui-design/kit/serve.js +55 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/release-notes.json +12 -0
  38. package/src/bongos/serve-internal.js +4 -2
  39. package/src/module-api.js +1 -1
  40. package/src/platform-server.js +11 -0
  41. package/tests/hub_account_page.mjs +9 -1
  42. package/tests/hub_community_page.mjs +493 -0
  43. package/tests/hub_connections_page.mjs +53 -4
  44. package/tests/hub_guild_page.mjs +1 -0
  45. package/tests/landing_legal_pages.mjs +10 -1
  46. package/tests/landing_page.mjs +1 -1
  47. package/tests/render_check.mjs +2 -2
  48. package/tests/ui_design_kit.mjs +2 -2
@@ -0,0 +1,28 @@
1
+ {
2
+ "_": "GET /community/leaderboard for the kit stub (task 1003363): one ranked list per window, already in the route's order (credits, ships, karma, handle). The stub filters by discipline and numbers `place`. Names are invented; the long one tests the row's wrap.",
3
+ "boards": {
4
+ "all": [
5
+ { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "credits": 18420, "tasks_shipped": 212, "karma": 64, "disciplines": ["engineer", "ui"] },
6
+ { "handle": "tomas-ek", "github_login": "tomasek", "display_name": "Tomas Ek", "avatar_url": "/__kit/avatar.svg", "credits": 9310, "tasks_shipped": 131, "karma": 40, "disciplines": ["engineer"] },
7
+ { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 6125, "tasks_shipped": 77, "karma": 52, "disciplines": ["artist"] },
8
+ { "handle": "lin-wei", "github_login": "linwei", "display_name": "Lin Wei", "avatar_url": null, "credits": 2480, "tasks_shipped": 31, "karma": 18, "disciplines": ["ideator", "engineer"] },
9
+ { "handle": "maximiliana-van-der-berghe-lindqvist", "github_login": "mvdbl", "display_name": "Maximiliana van der Berghe-Lindqvist", "avatar_url": "/__kit/avatar.svg", "credits": 940, "tasks_shipped": 12, "karma": 7, "disciplines": ["ui"] },
10
+ { "handle": "jo-park", "github_login": "jopark", "display_name": null, "avatar_url": "/__kit/avatar.svg", "credits": 310, "tasks_shipped": 1, "karma": 0, "disciplines": ["ideator"] }
11
+ ],
12
+ "365d": [
13
+ { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "credits": 12200, "tasks_shipped": 150, "karma": 41, "disciplines": ["engineer", "ui"] },
14
+ { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 6125, "tasks_shipped": 77, "karma": 52, "disciplines": ["artist"] },
15
+ { "handle": "tomas-ek", "github_login": "tomasek", "display_name": "Tomas Ek", "avatar_url": "/__kit/avatar.svg", "credits": 4020, "tasks_shipped": 58, "karma": 22, "disciplines": ["engineer"] },
16
+ { "handle": "lin-wei", "github_login": "linwei", "display_name": "Lin Wei", "avatar_url": null, "credits": 2480, "tasks_shipped": 31, "karma": 18, "disciplines": ["ideator", "engineer"] }
17
+ ],
18
+ "30d": [
19
+ { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 1340, "tasks_shipped": 19, "karma": 9, "disciplines": ["artist"] },
20
+ { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "credits": 880, "tasks_shipped": 11, "karma": 4, "disciplines": ["engineer", "ui"] },
21
+ { "handle": "jo-park", "github_login": "jopark", "display_name": null, "avatar_url": "/__kit/avatar.svg", "credits": 310, "tasks_shipped": 1, "karma": 0, "disciplines": ["ideator"] }
22
+ ],
23
+ "7d": [
24
+ { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "credits": 420, "tasks_shipped": 6, "karma": 3, "disciplines": ["artist"] },
25
+ { "handle": "tomas-ek", "github_login": "tomasek", "display_name": "Tomas Ek", "avatar_url": "/__kit/avatar.svg", "credits": -15, "tasks_shipped": 0, "karma": 1, "disciplines": ["engineer"] }
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "_": "GET /community/search for the kit stub (task 1003363): the public, existence-visible builders the stub matches by name or handle. Sam Rivera hides their numbers, so `disciplines` is null: found by name, dropped by any craft filter (builder-search.js).",
3
+ "directory": [
4
+ { "handle": "priya-nair", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "disciplines": ["engineer", "ui"] },
5
+ { "handle": "priyanka-shah", "github_login": "pshah", "display_name": "Priyanka Shah", "avatar_url": "/__kit/avatar.svg", "disciplines": ["artist"] },
6
+ { "handle": "tomas-ek", "github_login": "tomasek", "display_name": "Tomas Ek", "avatar_url": "/__kit/avatar.svg", "disciplines": ["engineer"] },
7
+ { "handle": "ada-okafor", "github_login": "adaokafor", "display_name": "Ada Okafor", "avatar_url": "/__kit/avatar.svg", "disciplines": ["artist"] },
8
+ { "handle": "sam-rivera", "github_login": "samrivera", "display_name": "Sam Rivera", "avatar_url": "/__kit/avatar.svg", "disciplines": null }
9
+ ]
10
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "_": "The connections graph with nothing to suggest (task 1004256): one connection, nothing pending, no builder you may know. Opened by the Community menu's item (/connections#connSuggest), the panel says so rather than vanishing.",
3
+ "connections": [
4
+ { "id": "41", "connected_at": "2026-09-30T10:00:00Z", "handle": "mira", "display_name": "Mira Okafor", "avatar_url": "/__kit/avatar.svg" }
5
+ ],
6
+ "incoming": [],
7
+ "outgoing": [],
8
+ "suggestions": [],
9
+ "directory": [
10
+ { "handle": "nadia", "display_name": "Nadia Haddad", "avatar_url": "/__kit/avatar.svg" }
11
+ ]
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "_": "GET /scouting for a viewer the recruiting door admits — a project owner (task 1003363). `builders` is the ranked tier, which the community page never draws (the leaderboard already shows those numbers to everyone); `name_only` is ADR 0334's hidden-but-open-to-recruiters tier, login, name and avatar only, alphabetical by login as listScoutingNameOnly sends it.",
3
+ "builders": [
4
+ { "github_id": "9001", "github_login": "priyanair", "display_name": "Priya Nair", "avatar_url": "/__kit/avatar.svg", "project_count": 3, "total_credits": 18420, "total_tasks_shipped": 212, "total_karma": 64, "last_active": "2026-10-20T10:00:00Z", "disciplines": ["engineer", "ui"] }
5
+ ],
6
+ "name_only": [
7
+ { "github_login": "kemi-a", "display_name": "Kemi Adeyemi", "avatar_url": "/__kit/avatar.svg" },
8
+ { "github_login": "samrivera", "display_name": "Sam Rivera", "avatar_url": "/__kit/avatar.svg" },
9
+ { "github_login": "yuki-t", "display_name": null, "avatar_url": null }
10
+ ]
11
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "_": "GET /scouting for a signed-in viewer the recruiting door does not admit (task 1003363): most builders own no project, so the default answer is the unchanged permission refusal (403). scouting-recruiter.json is an admitted reader's answer.",
3
+ "refused": true
4
+ }
@@ -684,6 +684,58 @@ function connRemove(res, id) {
684
684
  return refuse(res, 404, 'connection_not_found', 'connection_not_found');
685
685
  }
686
686
 
687
+ // The community page (task 1003363): the two public reads and the recruiter door, the
688
+ // routes' answers mirrored (modules/platform-identity/routes/community-leaderboard.js,
689
+ // community-search.js, scouting.js). The board fixture holds one ranked list per window;
690
+ // the stub filters it by discipline and numbers `place` the way listLeaderboard does. The
691
+ // search matches a name or handle, case-folded, and a craft filter drops every row whose
692
+ // disciplines are null (a hidden account: findable by name, never by craft). GET /scouting
693
+ // answers 401 signed out, and signed in whatever its fixture says: `scouting.json` is the
694
+ // unchanged permission refusal (most viewers own no project), `scouting-recruiter.json` an
695
+ // admitted reader's { builders, name_only }. A fixture's "stub_status" answers that status.
696
+ // The caps are copied (a kit module may not import one).
697
+ const STUB_COMMUNITY_WINDOWS = ['7d', '30d', '365d', 'all'];
698
+ const STUB_COMMUNITY_DISCIPLINE_MAX = 32;
699
+ const STUB_SEARCH_MIN = 2;
700
+ const STUB_SEARCH_MAX = 100;
701
+ function stubStatus(res, f) {
702
+ if (!f || !f.stub_status) return false;
703
+ json(res, f.stub_status, { error: { code: 'stub_failure', message: `stub_status ${f.stub_status}` } });
704
+ return true;
705
+ }
706
+ function communityBoard(res, u, origin) {
707
+ const f = heldFixture('community-leaderboard');
708
+ if (stubStatus(res, f)) return;
709
+ const window = u.searchParams.get('window') || 'all';
710
+ if (!STUB_COMMUNITY_WINDOWS.includes(window)) return refuse(res, 400, 'bad_window', `window must be one of: ${STUB_COMMUNITY_WINDOWS.join(', ')}`);
711
+ const disc = (u.searchParams.get('discipline') || '').trim();
712
+ if (disc.length > STUB_COMMUNITY_DISCIPLINE_MAX) return refuse(res, 400, 'discipline_too_long', 'discipline must be at most 32 characters');
713
+ const rows = ((f.boards || {})[window] || []).filter((r) => !disc || (r.disciplines || []).includes(disc));
714
+ const results = rows.map((r, i) => ({ place: i + 1, ...withAvatar(r, origin) }));
715
+ return json(res, 200, { window, since: window === 'all' ? null : stamp('$now-1d'), discipline: disc || null, count: results.length, results, page: { limit: 25, offset: 0 } });
716
+ }
717
+ function communitySearch(res, u, origin) {
718
+ const f = heldFixture('community-search');
719
+ if (stubStatus(res, f)) return;
720
+ const q = (u.searchParams.get('q') || '').trim();
721
+ if (!q) return refuse(res, 400, 'bad_request', 'q (a non-empty search string) is required');
722
+ if (q.length < STUB_SEARCH_MIN) return refuse(res, 400, 'query_too_short', 'q must be at least 2 characters');
723
+ if (q.length > STUB_SEARCH_MAX) return refuse(res, 400, 'query_too_long', 'q must be at most 100 characters');
724
+ const disc = (u.searchParams.get('discipline') || '').trim();
725
+ const needle = q.toLowerCase();
726
+ const results = (f.directory || [])
727
+ .filter((r) => [r.handle, r.display_name].some((s) => String(s || '').toLowerCase().includes(needle)))
728
+ .filter((r) => !disc || (Array.isArray(r.disciplines) && r.disciplines.includes(disc)))
729
+ .map((r) => withAvatar(r, origin));
730
+ return json(res, 200, { query: q, discipline: disc || null, count: results.length, results, page: { limit: 20, offset: 0 } });
731
+ }
732
+ function scoutingRead(res, origin) {
733
+ const f = heldFixture('scouting');
734
+ if (stubStatus(res, f)) return;
735
+ if (f.refused) return refuse(res, 403, 'forbidden', 'requires permission project.curate');
736
+ return json(res, 200, { builders: (f.builders || []).map((r) => withAvatar(r, origin)), name_only: (f.name_only || []).map((r) => withAvatar(r, origin)) });
737
+ }
738
+
687
739
  const server = http.createServer((req, res) => {
688
740
  const u = new URL(req.url, 'http://x');
689
741
  const p = u.pathname;
@@ -712,6 +764,9 @@ const server = http.createServer((req, res) => {
712
764
  if (guild && guild[2] === '/engagements' && req.method === 'POST') return AUTH ? readBody(req, (body) => guildEngagementPost(res, guild[1], body)) : json(res, 401, UNAUTH);
713
765
  if (rest === '/me/guilds' && req.method === 'GET') return AUTH ? myGuilds(res) : json(res, 401, UNAUTH);
714
766
  if (rest === '/me/guild-requests' && req.method === 'GET') return AUTH ? myGuildRequests(res) : json(res, 401, UNAUTH);
767
+ if (rest === '/community/leaderboard' && req.method === 'GET') return communityBoard(res, u, origin);
768
+ if (rest === '/community/search' && req.method === 'GET') return communitySearch(res, u, origin);
769
+ if (rest === '/scouting' && req.method === 'GET') return AUTH ? scoutingRead(res, origin) : json(res, 401, UNAUTH);
715
770
  if (rest === '/connections' && req.method === 'GET') return AUTH ? connRead(res, 'list', origin) : json(res, 401, UNAUTH);
716
771
  if (rest === '/connections/requests' && req.method === 'GET') return AUTH ? connRead(res, 'requests', origin) : json(res, 401, UNAUTH);
717
772
  if (rest === '/connections/suggestions' && req.method === 'GET') return AUTH ? connRead(res, 'suggestions', origin) : json(res, 401, UNAUTH);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.9",
3
+ "version": "1.23.11",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.23.9",
9
+ "version": "1.23.11",
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.23.9",
3
+ "version": "1.23.11",
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",
@@ -10441,5 +10441,17 @@
10441
10441
  "id": "1004897",
10442
10442
  "text": "The record of this session's work on goal 1000110 is written down for the next person who picks it up."
10443
10443
  }
10444
+ ],
10445
+ "1.23.10": [
10446
+ {
10447
+ "id": "1003363",
10448
+ "text": "cloudbongos.com now has a Community menu: a leaderboard of builders across every project (by week, month, year or all time, and by craft), a search for builders by name, and, for project owners, a list of builders who keep the"
10449
+ }
10450
+ ],
10451
+ "1.23.11": [
10452
+ {
10453
+ "id": "1004256",
10454
+ "text": "Signed-in builders now find 'Builders you may know' in the Community menu; it opens their suggestions on the Connections page, which says so plainly when there's no one to suggest yet."
10455
+ }
10444
10456
  ]
10445
10457
  }
@@ -1005,8 +1005,10 @@ function mountInternalSurfaces(app) {
1005
1005
  // /people itself (owner call, 2026-09-25 — ADR 0337 D5): cross-project
1006
1006
  // discovery belongs to the hub, and /roster is the hall's directory. Both URLs
1007
1007
  // survive as redirects to /roster so a bookmark lands somewhere instead of a
1008
- // 404. 302, not 301: /roster is interim until the hub community page (task
1009
- // 1003363) ships, and a browser caches a 301 past any re-point. No page gate:
1008
+ // 404. They stay on /roster now the hub's community pages exist (task 1003363):
1009
+ // each hall is its own instance and only the hub serves /community, so a child
1010
+ // hall has nowhere else to send them. 302, not 301, so a re-point is never
1011
+ // stuck in a browser cache. No page gate:
1010
1012
  // the redirect discloses nothing, and /roster is deliberately public.
1011
1013
  // GET /scouting's own gate (platform-identity) is untouched by this.
1012
1014
  const RETIRED_DIRECTORY_RE = /^\/builders\/(?:people|scouting)(?:\.html)?\/?$/;
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.23.9'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.23.11'; // 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');
@@ -337,6 +337,17 @@ function buildPlatformApp() {
337
337
  if (!html) return next();
338
338
  res.type('html').send(html);
339
339
  });
340
+ // THE COMMUNITY PAGES (task 1003363, ADR 0217 D1, ADR 0337 D1/D3): who is building
341
+ // across every project — the leaderboard, the builder search (the same for every
342
+ // viewer) and the recruiter list GET /scouting's own door admits. One file serves all
343
+ // three; its script shows the one the path names. Hub-only for the /account reason:
344
+ // every read it makes is a platform-identity route.
345
+ app.get(['/community', '/community/find', '/community/recruiters'], (req, res, next) => {
346
+ if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
347
+ const html = landingPage('community.html');
348
+ if (!html) return next();
349
+ res.type('html').send(html);
350
+ });
340
351
  // THE GUILD PAGE (task 1002301, R25; ADR 0336 D4/D6): /g/<slug>, the namespace
341
352
  // D6 reserved. Hub-only for the /account reason — every read it makes is a
342
353
  // platform-identity route. ONE shell for every slug: the page reads the public
@@ -135,6 +135,9 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
135
135
  assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProjects')), [
136
136
  ['Sky', '/projects?view=map'], ['Table', '/projects?view=map&amp;sub=table'],
137
137
  ['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
138
+ // the Community menu's three sub-pages, folded into the band under 480px (task 1003363)
139
+ ['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
140
+ ['Builders you may know', '/connections#connSuggest'],
138
141
  ], 'this page has no views of its own, so every item names /projects in full');
139
142
  assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
140
143
  ['My projects', '/projects?view=mine'], ['Invitations', '/invitations'], ['Connections', '/connections'], ['Account', '/account'], ['Sign out', '/api/bongos/auth/logout'],
@@ -211,6 +214,8 @@ function boot(answer) {
211
214
  // The bar: the two triggers, and each band with its links in markup order. An
212
215
  // item's cell is its <li>; only #mineCell carries an id, so only it can hide.
213
216
  const trigs = [...els.values()].filter((e) => /\btrig\b/.test(e.attrs.class || ''));
217
+ // the signed-in-only "Builders you may know" cells (task 1004256), by class: a band and its phone fold
218
+ const knowCells = [...PAGE.matchAll(/<li class="bandCell(?: bandFold)? knowCell" hidden>/g)].map(() => ({ hidden: true }));
214
219
  const bands = [...PAGE.matchAll(/<div class="menuBand" id="(\w+)"[\s\S]*?\n<\/div>/g)].map((m) => {
215
220
  const band = els.get(m[1]);
216
221
  band.items = [...m[0].matchAll(/<li class="bandCell"(?: id="(\w+)")?><a class="([^"]+)" href="([^"]+)"([^>]*)>([^<]+)<\/a>/g)].map((a) => ({
@@ -241,7 +246,7 @@ function boot(answer) {
241
246
  });
242
247
  const calls = [];
243
248
  const dom = {
244
- els, radios, calls, focused: null, assigned: null,
249
+ els, radios, calls, knowCells, focused: null, assigned: null,
245
250
  active: () => document.activeElement,
246
251
  // a click bubbles: the target's own listeners, its band's, then the document's
247
252
  click(target, detail = 1) {
@@ -272,6 +277,7 @@ function boot(answer) {
272
277
  },
273
278
  querySelectorAll(sel) {
274
279
  if (sel === '#profileForm [data-link]') return links;
280
+ if (sel === '.knowCell') return knowCells;
275
281
  if (sel === '.trig') return trigs;
276
282
  if (sel === '.menuBand') return bands;
277
283
  const names = [...sel.matchAll(/input\[name="(\w+)"\]/g)].map((m) => m[1]);
@@ -309,6 +315,7 @@ test('signed out: a 401 opens the door and shows nothing else', async () => {
309
315
  assert.equal(dom.els.get('who').hidden, true, 'no chip without a session');
310
316
  assert.equal(dom.els.get('signinTop').hidden, false, 'the sign-in door takes its slot');
311
317
  assert.equal(dom.els.get('mineCell').hidden, true, 'and the Projects band offers no Your projects');
318
+ assert.deepEqual(dom.knowCells.map((c) => c.hidden), [true, true], 'nor Builders you may know, in the Community band or its fold');
312
319
  });
313
320
 
314
321
  test('signed in: every control is painted from the server’s answer', async () => {
@@ -678,6 +685,7 @@ test('the chip is the hub’s identity read: GET /me fills the trigger and the p
678
685
  assert.equal(dom.els.get(id).hidden, false);
679
686
  }
680
687
  assert.equal(dom.els.get('mineCell').hidden, false);
688
+ assert.deepEqual(dom.knowCells.map((c) => c.hidden), [false, false]);
681
689
  });
682
690
 
683
691
  test('an account with no avatar gets a chip with no picture rather than a broken one', async () => {