@bongos/core 1.19.95 → 1.19.97

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.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.95",
6
- "core_contract": "1.19.95",
7
- "source_commit": "e6e718ed6edb6fc01565339760fd0fd14cffa2fc",
5
+ "core_version": "1.19.97",
6
+ "core_contract": "1.19.97",
7
+ "source_commit": "98de965667e063aac8e2bd2de7a3d05f588aa7b5",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-13T21:44:20.853Z",
9
+ "built_at": "2026-08-13T22:16:31.509Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 298,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1549,
20
- "tree_sha256": "cb89e58c1034e1681a97ac75f367935e745215ceb83b1aa393639904a0d861db",
20
+ "tree_sha256": "5057e83f784804f0af10e3b926390ed83212d7b23656b12359d5bdfd429caa25",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4127,12 +4127,12 @@
4127
4127
  {
4128
4128
  "path": "package-lock.json",
4129
4129
  "mode": "0000644",
4130
- "sha256": "0ebfc1c5594ff344281bfc92013bb8cf8f871c7f02152c91ae847cde36f2de3d"
4130
+ "sha256": "20f03a27edf41b5de61cd36c58e61d2e0acad8cfc68a4bfa23c19786ead8e1f1"
4131
4131
  },
4132
4132
  {
4133
4133
  "path": "package.json",
4134
4134
  "mode": "0000644",
4135
- "sha256": "b192fbde61e9428fff6a9b7077f341abe1f6d930e009496b93c6fb44eca2a45c"
4135
+ "sha256": "136d49d62b377412dcb7ad5884c8a7ca439bfc41544be3d3a5233a4cd11100e7"
4136
4136
  },
4137
4137
  {
4138
4138
  "path": "public-docs/index.html",
@@ -4392,7 +4392,7 @@
4392
4392
  {
4393
4393
  "path": "public-landing/projects.html",
4394
4394
  "mode": "0000644",
4395
- "sha256": "6edf67839bd80ba22672ebfb9f5c38a2bbce980157024b50d71ce9bae211c8db"
4395
+ "sha256": "3a77a5f30acaa793ef82b434bf6a87033d580923231f7e66673cdc533a6767d1"
4396
4396
  },
4397
4397
  {
4398
4398
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5702,7 +5702,7 @@
5702
5702
  {
5703
5703
  "path": "src/module-api.js",
5704
5704
  "mode": "0000644",
5705
- "sha256": "401742f230ab25aa8dbb410a1b8bf1bfbe02257a360362fa7488f3965a88c5ca"
5705
+ "sha256": "4b6c32f29637b84b0e11cd7e0fb7394e4f33d9b520f63c8c34c94de33aa65449"
5706
5706
  },
5707
5707
  {
5708
5708
  "path": "src/module-loader/loader.js",
@@ -7212,7 +7212,7 @@
7212
7212
  {
7213
7213
  "path": "tests/projects_hub.mjs",
7214
7214
  "mode": "0000644",
7215
- "sha256": "e3e3e3f8164da3f897166dca2b0502fac2519483b36282e91ef5645cfef6c18c"
7215
+ "sha256": "cffae7809fb64eec684987895dc2b82b7de758cd950bea5d013cb210db3c6220"
7216
7216
  },
7217
7217
  {
7218
7218
  "path": "tests/provision.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.95",
3
+ "version": "1.19.97",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.95",
9
+ "version": "1.19.97",
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.19.95",
3
+ "version": "1.19.97",
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",
@@ -173,7 +173,24 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
173
173
  <th class="colGo"></th>
174
174
  </tr>
175
175
  </thead>
176
- <tbody id="mapRows"></tbody>
176
+ <tbody id="mapRows">
177
+ <!-- The platform's own row. Static and apex-only for exactly the reason
178
+ the orb above is (stripApexOnly() is a string transform that cannot
179
+ reach anything a script renders later), and preserved across
180
+ re-renders by the same data-generated contract renderOrbs uses. -->
181
+ <!--apex-only:start-->
182
+ <tr id="selfRow">
183
+ <td class="colName"><span class="pname"><span class="pdot" style="--acc:var(--accent)"></span>
184
+ <span><span class="ptitle" data-brand-product-inline>Cloud Bongos</span><span id="selfPill"></span><br>
185
+ <span class="ptag">this platform</span></span></span></td>
186
+ <td class="right num">—</td>
187
+ <td class="colType">—</td>
188
+ <td class="colTags">—</td>
189
+ <td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>
190
+ <td class="colGo"><a class="btn small ghost" href="#" data-self-open>Open →</a></td>
191
+ </tr>
192
+ <!--apex-only:end-->
193
+ </tbody>
177
194
  </table>
178
195
  <p class="finePrint" style="text-align:center">Click a column to sort.</p>
179
196
  </div>
@@ -440,13 +457,30 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
440
457
  $('markPrefix').textContent = prefix;
441
458
  if (REDUCED || prefix !== 'cloud' || word !== 'bongos') return;
442
459
  var PREFIXES = ['cloud', 'claude', 'chatgpt', 'gemini'];
443
- var wi = 0, ci = PREFIXES[0].length, dir = -1;
444
- setInterval(function () {
445
- ci += dir;
446
- if (ci <= 0) { dir = 1; wi = (wi + 1) % PREFIXES.length; ci = 0; }
447
- else if (ci >= PREFIXES[wi].length) { dir = -1; ci = PREFIXES[wi].length; }
448
- $('markPrefix').textContent = PREFIXES[wi].slice(0, ci);
449
- }, 190);
460
+
461
+ /* Type, HOLD, erase — the landing hero's cadence (index.html), not a
462
+ symmetric ping-pong. The single reversing interval this replaced turned
463
+ around on the very tick it completed a word, so `claude bongos` finished
464
+ and was already being deleted: the reader never got to see the word the
465
+ whole animation exists to show. A hold is the only part of the cycle that
466
+ carries meaning; the typing is just how it arrives.
467
+
468
+ `cloud` rests longest because it is the real wordmark and the state the
469
+ mark should sit in most — the others are the joke, and the joke lands
470
+ once. Same two numbers the hero uses, so the two surfaces read as one
471
+ system rather than two animations that happen to share a font. */
472
+ var TYPE = 72, ERASE = 38, HOLD = 900, HOLD_HOME = 2100;
473
+ var el = $('markPrefix'), wi = 0;
474
+ (function cycle() {
475
+ var w = PREFIXES[wi++ % PREFIXES.length], i = 0;
476
+ (function type() {
477
+ if (i <= w.length) { el.textContent = w.slice(0, i++); setTimeout(type, TYPE); }
478
+ else setTimeout(function erase() {
479
+ if (el.textContent.length) { el.textContent = el.textContent.slice(0, -1); setTimeout(erase, ERASE); }
480
+ else cycle();
481
+ }, w === 'cloud' ? HOLD_HOME : HOLD);
482
+ })();
483
+ })();
450
484
  })();
451
485
 
452
486
  /* ══ THE MAP ═══════════════════════════════════════════════════════════════
@@ -472,6 +506,13 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
472
506
  var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
473
507
  var catalog = [];
474
508
  var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
509
+ /* What the platform's own entry is FOR SEARCH. It is deliberately not pushed
510
+ into `catalog`: the catalog is the federated directory of other instances,
511
+ and a synthetic member of it would flow into accent assignment, orb slots
512
+ and the sort comparator as if a row existed. This keeps the self entry a
513
+ view-level concern (the owner's call, 2026-08-13) while still letting the
514
+ search box match it by the same predicate every other project uses. */
515
+ var SELF_PROJECT = { name: productName, tagline: 'this platform' };
475
516
  var mapSort = { key: 'name', dir: 1 };
476
517
 
477
518
  /* Catalog origins become hrefs, so they are scheme-checked before they are
@@ -500,6 +541,23 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
500
541
  }
501
542
  function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
502
543
  function relOf(p) { return mineOrigins[hostOf(p.origin)] || 'none'; }
544
+ /* The platform's own relationship cannot come from mineOrigins, and not
545
+ because the data is merely missing: platform_identity_project_memberships
546
+ is written ONLY by the server-to-server check-in a FEDERATED instance posts
547
+ on sign-in (ADR 0141 §3), and the apex is not a federated client of itself
548
+ — reportMembershipCheckIn() returns early on the instance that IS the hub,
549
+ so no amount of signing in here will ever produce a row. Waiting on that
550
+ table for this one project waits forever.
551
+
552
+ The local session is the authority instead, and it is a stronger one: a
553
+ session on this instance EXISTS because sign-in created a builder row here,
554
+ so "signed in here" IS "builder here" — not a guess standing in for one.
555
+ Archon is the instance's owner. Signed out stays 'none' so a stranger's map
556
+ claims nothing. Membership stays the cross-instance federated view. */
557
+ function selfRel() {
558
+ if (!me) return 'none';
559
+ return me.rank === 'archon' ? 'owner' : 'builder';
560
+ }
503
561
  function matchesMapQuery(p, q) {
504
562
  if (!q) return true;
505
563
  return (String(p.name || '') + ' ' + String(p.tagline || '')).toLowerCase().indexOf(q) !== -1;
@@ -534,11 +592,17 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
534
592
  }
535
593
 
536
594
  function renderRows(list) {
537
- $('mapRows').innerHTML = list.map(function (p) {
595
+ var body = $('mapRows');
596
+ /* replace only the generated rows — the platform's own row is static
597
+ apex-only markup and must survive every re-render, for the same reason
598
+ #selfOrb does in the sky. It stays pinned first: it is "this platform",
599
+ not a peer in the catalog's sort order. */
600
+ body.querySelectorAll('tr[data-generated]').forEach(function (n) { n.remove(); });
601
+ body.insertAdjacentHTML('beforeend', list.map(function (p) {
538
602
  var rel = relOf(p);
539
603
  var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
540
604
  : rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
541
- return '<tr>' +
605
+ return '<tr data-generated>' +
542
606
  '<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
543
607
  accentOf(p) + '"></span>' +
544
608
  '<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
@@ -556,7 +620,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
556
620
  ? '<a class="btn small ghost" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
557
621
  : '') + '</td>' +
558
622
  '</tr>';
559
- }).join('');
623
+ }).join(''));
560
624
  }
561
625
 
562
626
  function applyMapFilter() {
@@ -571,13 +635,34 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
571
635
  });
572
636
  renderOrbs(list);
573
637
  renderRows(list);
574
- var self = $('selfOrb');
575
- if (self) self.hidden = onlyMine; /* the platform is nobody's project */
576
- $('orbField').classList.toggle('isEmpty', list.length === 0 && (onlyMine || !!q));
638
+
639
+ /* The platform's own entry, in BOTH views from one predicate — it used to
640
+ be hidden outright under "only mine" ("the platform is nobody's project"),
641
+ which is what dropped Cloud Bongos out of a signed-in archon's own filter.
642
+ It is somebody's: see selfRel(). It still answers the search box, and it
643
+ still vanishes for a signed-out visitor, who owns nothing here. */
644
+ var rel = selfRel();
645
+ var selfShown = (rel !== 'none' || !onlyMine) && matchesMapQuery(SELF_PROJECT, q);
646
+ var badge = rel === 'owner' ? 'yours' : rel === 'builder' ? 'you build here' : '';
647
+ var orb = $('selfOrb'), row = $('selfRow');
648
+ if (orb) {
649
+ orb.hidden = !selfShown;
650
+ orb.setAttribute('data-rel', rel);
651
+ var b = orb.querySelector('.badge');
652
+ if (badge && !b) { b = document.createElement('span'); b.className = 'badge'; orb.insertBefore(b, orb.firstChild); }
653
+ if (b) { b.textContent = badge; b.hidden = !badge; }
654
+ }
655
+ if (row) {
656
+ row.hidden = !selfShown;
657
+ $('selfPill').innerHTML = badge ? ' <span class="pill">' + badge + '</span>' : '';
658
+ }
659
+
660
+ var shown = list.length + (selfShown ? 1 : 0);
661
+ $('orbField').classList.toggle('isEmpty', shown === 0 && (onlyMine || !!q));
577
662
  $('emptyMsg').textContent = onlyMine
578
663
  ? 'You don’t own or build on anything here yet.'
579
664
  : 'Nothing here matches that.';
580
- $('mapCount').textContent = list.length + (list.length === 1 ? ' project' : ' projects');
665
+ $('mapCount').textContent = shown + (shown === 1 ? ' project' : ' projects');
581
666
  }
582
667
 
583
668
  function loadCatalog() {
package/src/module-api.js CHANGED
@@ -617,8 +617,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
617
617
  // landed since 1.19.93 with no explicit bump. run 31742308532. (task 1002620)
618
618
  // 1.19.95 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
619
619
  // landed since 1.19.94 with no explicit bump. run 31746921919. (task 1002620)
620
+ // 1.19.96 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
621
+ // landed since 1.19.95 with no explicit bump. run 31748829423. (task 1002620)
622
+ // 1.19.97 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
623
+ // landed since 1.19.96 with no explicit bump. run 31749223878. (task 1002620)
620
624
  // ---------------------------------------------------------------------------
621
- const CORE_VERSION = '1.19.95'; // CI auto-patch carrier (ADR 0161); see changelog above
625
+ const CORE_VERSION = '1.19.97'; // CI auto-patch carrier (ADR 0161); see changelog above
622
626
 
623
627
  // A namespaced logger so a module's log lines are attributable + consistent.
624
628
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -203,3 +203,107 @@ test('/create still answers — the hub links it rather than absorbing it', () =
203
203
  assert.match(SCRIPT, /location\.assign\('\/create'/);
204
204
  assert.match(HUB, /href="\/create"/, 'the empty state must still point at the wizard');
205
205
  });
206
+
207
+ /* ── The platform's own entry (task 1002760) ────────────────────────────────
208
+ The apex had an orb for itself but no table row and no membership, so a
209
+ signed-in archon's own "only mine" filter hid the very project they were
210
+ standing on. These pin the shape of the fix: the self entry is a VIEW-level
211
+ concern present in both views, and its relationship comes from the local
212
+ session because the membership table structurally cannot answer for it. ── */
213
+
214
+ test('the platform has a self entry in the TABLE, not only in the sky', () => {
215
+ // the sky's orb and the table's row are both static markup, for the same
216
+ // reason: stripApexOnly() is a string transform and cannot reach JS output
217
+ assert.match(HUB, /id="selfOrb"/, 'the sky keeps its self orb');
218
+ assert.match(HUB, /id="selfRow"/, 'the table needs one too — this is the bug');
219
+ // and the row must sit INSIDE the tbody the renderer owns
220
+ const tbody = (HUB.match(/<tbody id="mapRows">([\s\S]*?)<\/tbody>/) || [, ''])[1];
221
+ assert.match(tbody, /id="selfRow"/, 'the self row belongs in #mapRows');
222
+ });
223
+
224
+ test('both self entries are apex-only — a child instance has no "this platform"', () => {
225
+ for (const id of ['selfOrb', 'selfRow']) {
226
+ const at = HUB.indexOf(`id="${id}"`);
227
+ const before = HUB.slice(0, at);
228
+ const opens = (before.match(/<!--apex-only:start-->/g) || []).length;
229
+ const closes = (before.match(/<!--apex-only:end-->/g) || []).length;
230
+ assert.equal(opens - closes, 1, `${id} must sit inside an apex-only sentinel pair`);
231
+ }
232
+ });
233
+
234
+ test('the renderer preserves the self row exactly as it preserves the self orb', () => {
235
+ // renderRows used to blow the whole tbody away with innerHTML=, which would
236
+ // delete a static row on the first catalog paint
237
+ assert.doesNotMatch(SCRIPT, /\$\('mapRows'\)\.innerHTML\s*=/,
238
+ 'a wholesale innerHTML= on #mapRows destroys the static self row');
239
+ assert.match(SCRIPT, /querySelectorAll\('tr\[data-generated\]'\)/,
240
+ 'generated rows must be removed selectively, like .worldOrb[data-generated]');
241
+ assert.match(SCRIPT, /<tr data-generated>/, 'generated rows must be markable');
242
+ });
243
+
244
+ test('"only mine" no longer hides the platform from a signed-in builder', () => {
245
+ // the exact regression: `if (self) self.hidden = onlyMine;`
246
+ assert.doesNotMatch(SCRIPT, /self\.hidden\s*=\s*onlyMine/,
247
+ 'hiding the self entry whenever the filter is on was the bug');
248
+ assert.match(SCRIPT, /selfRel\s*\(\s*\)/, 'a single predicate decides both views');
249
+ assert.match(SCRIPT, /selfShown/, 'and both views read the same computed visibility');
250
+ });
251
+
252
+ test('the self relationship comes from the SESSION, never from mineOrigins', () => {
253
+ const fn = (SCRIPT.match(/function selfRel\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
254
+ assert.ok(fn, 'selfRel() must exist');
255
+ assert.doesNotMatch(fn, /mineOrigins/,
256
+ 'memberships are written only by federated check-in; the apex is not a client of itself');
257
+ assert.match(fn, /if \(!me\) return 'none'/, 'a signed-out stranger owns nothing here');
258
+ assert.match(fn, /archon/, 'the instance archon is its owner');
259
+ });
260
+
261
+ test('the self entry is not pushed into the catalog array', () => {
262
+ // it would flow into accent assignment, orb slots and the sort comparator as
263
+ // though a federated row existed
264
+ assert.doesNotMatch(SCRIPT, /catalog\.push|catalog\.unshift|catalog\s*=\s*\[SELF/,
265
+ 'the catalog is the federated directory of OTHER instances');
266
+ assert.match(SCRIPT, /var SELF_PROJECT\s*=/, 'the self entry is a view-level constant');
267
+ });
268
+
269
+ test('the count includes the platform when it is shown', () => {
270
+ assert.doesNotMatch(SCRIPT, /mapCount'\)\.textContent = list\.length/,
271
+ 'the count must not ignore the self entry it is rendering beside');
272
+ assert.match(SCRIPT, /var shown = list\.length \+ \(selfShown \? 1 : 0\)/);
273
+ });
274
+
275
+ /* ── The wordmark's cadence (task 1002765) ──────────────────────────────────
276
+ The mark completed a word and erased it on the same tick, so the word the
277
+ animation exists to show was never legible. Owner-reported, twice. ── */
278
+
279
+ test('the wordmark HOLDS the finished word before erasing it', () => {
280
+ const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
281
+ assert.ok(mark, 'typeMark() must exist');
282
+ // the regression shape: one interval that flips direction the moment it
283
+ // completes a word, so there is no dwell at full length
284
+ assert.doesNotMatch(mark, /setInterval/,
285
+ 'a single reversing interval cannot hold — it turns around on the tick it completes');
286
+ assert.match(mark, /HOLD\s*=\s*\d+/, 'the hold must be a named duration, not an accident of timing');
287
+ // and it must actually be a pause worth reading, not a couple of frames
288
+ const hold = Number((mark.match(/HOLD\s*=\s*(\d+)/) || [, 0])[1]);
289
+ assert.ok(hold >= 600, `the hold must be legible; got ${hold}ms`);
290
+ });
291
+
292
+ test('the wordmark rests on the real product name, not on a joke', () => {
293
+ const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
294
+ assert.match(mark, /w === 'cloud' \? HOLD_HOME : HOLD/,
295
+ 'the canonical prefix should dwell longest — it is the wordmark, the others are the gag');
296
+ const home = Number((mark.match(/HOLD_HOME\s*=\s*(\d+)/) || [, 0])[1]);
297
+ const hold = Number((mark.match(/HOLD\s*=\s*(\d+)/) || [, 0])[1]);
298
+ assert.ok(home > hold, 'the resting state must outlast the alternates');
299
+ });
300
+
301
+ test('the hub and the landing hero share one typing cadence', () => {
302
+ // two surfaces, one system — a divergence here is a visible inconsistency
303
+ const LANDING = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
304
+ const heroHolds = [...LANDING.matchAll(/\?\s*(\d+)\s*:\s*(\d+)\)/g)].map(m => [m[1], m[2]]).flat();
305
+ const mark = (SCRIPT.match(/function typeMark\(\)([\s\S]*?)\n \}\)\(\);/) || [, ''])[1];
306
+ for (const n of [mark.match(/HOLD_HOME\s*=\s*(\d+)/)[1], mark.match(/HOLD\s*=\s*(\d+)/)[1]]) {
307
+ assert.ok(heroHolds.includes(n), `hold ${n}ms should match the hero's cadence (${heroHolds.join('/')})`);
308
+ }
309
+ });