@bongos/core 1.19.106 → 1.19.108
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 +12 -12
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +3 -1
- package/public-landing/projects.html +169 -22
- package/src/module-api.js +5 -1
- package/tests/module_loader.mjs +21 -3
- package/tests/projects_hub.mjs +94 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.108",
|
|
6
|
+
"core_contract": "1.19.108",
|
|
7
|
+
"source_commit": "bafad9a6b64800aa591f105f14a5a67cf39a54d9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-14T13:14:28.346Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 301,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1558,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "7130b0e01712f13175140a4ddc52041177b8f660fab0029699212a445ebf59ad",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4147,12 +4147,12 @@
|
|
|
4147
4147
|
{
|
|
4148
4148
|
"path": "package-lock.json",
|
|
4149
4149
|
"mode": "0000644",
|
|
4150
|
-
"sha256": "
|
|
4150
|
+
"sha256": "f205db41eab8846e667c61aad6739b8194db55326d38562f0e6a674ecb0cd8ce"
|
|
4151
4151
|
},
|
|
4152
4152
|
{
|
|
4153
4153
|
"path": "package.json",
|
|
4154
4154
|
"mode": "0000644",
|
|
4155
|
-
"sha256": "
|
|
4155
|
+
"sha256": "afc15cf17f555b46d2e616cea755061e6d6751e06b582ad9253ff9dc257a203d"
|
|
4156
4156
|
},
|
|
4157
4157
|
{
|
|
4158
4158
|
"path": "public-docs/index.html",
|
|
@@ -4392,7 +4392,7 @@
|
|
|
4392
4392
|
{
|
|
4393
4393
|
"path": "public-landing/assets/cosmos.css",
|
|
4394
4394
|
"mode": "0000644",
|
|
4395
|
-
"sha256": "
|
|
4395
|
+
"sha256": "e5bfd01db8cca1cb28c2d5c2382a401a2ee882cb1a219ed5553d578f2e3952b6"
|
|
4396
4396
|
},
|
|
4397
4397
|
{
|
|
4398
4398
|
"path": "public-landing/buddha.png",
|
|
@@ -4412,7 +4412,7 @@
|
|
|
4412
4412
|
{
|
|
4413
4413
|
"path": "public-landing/projects.html",
|
|
4414
4414
|
"mode": "0000644",
|
|
4415
|
-
"sha256": "
|
|
4415
|
+
"sha256": "e6e951af6fc202a2d90accef69e32da7d8a3456b80a6fcdbce469274edb49bce"
|
|
4416
4416
|
},
|
|
4417
4417
|
{
|
|
4418
4418
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5727,7 +5727,7 @@
|
|
|
5727
5727
|
{
|
|
5728
5728
|
"path": "src/module-api.js",
|
|
5729
5729
|
"mode": "0000644",
|
|
5730
|
-
"sha256": "
|
|
5730
|
+
"sha256": "309c33b07e1c5391726d8bf73d2c0d595cab3b9624244df0f7cd8fcbe068e3a5"
|
|
5731
5731
|
},
|
|
5732
5732
|
{
|
|
5733
5733
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7052,7 +7052,7 @@
|
|
|
7052
7052
|
{
|
|
7053
7053
|
"path": "tests/module_loader.mjs",
|
|
7054
7054
|
"mode": "0000644",
|
|
7055
|
-
"sha256": "
|
|
7055
|
+
"sha256": "092c9d9ffeed7faed34cfb49ffec6de64d86e45a8fda129444345924313b8fd7"
|
|
7056
7056
|
},
|
|
7057
7057
|
{
|
|
7058
7058
|
"path": "tests/module_manifest.mjs",
|
|
@@ -7252,7 +7252,7 @@
|
|
|
7252
7252
|
{
|
|
7253
7253
|
"path": "tests/projects_hub.mjs",
|
|
7254
7254
|
"mode": "0000644",
|
|
7255
|
-
"sha256": "
|
|
7255
|
+
"sha256": "6ddec55050ca5864670d02caad6ede76e5e0753afb1e5636036b75d4185e9427"
|
|
7256
7256
|
},
|
|
7257
7257
|
{
|
|
7258
7258
|
"path": "tests/provision.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.108",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.108",
|
|
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.
|
|
3
|
+
"version": "1.19.108",
|
|
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",
|
|
@@ -259,6 +259,8 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
259
259
|
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
260
260
|
.proj{border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:var(--bg-card);display:flex;gap:16px;align-items:flex-start;}
|
|
261
261
|
.proj.gone{opacity:0.78;}
|
|
262
|
+
/* click-to-manage landing glow: the sky's owned orb hands focus to this card */
|
|
263
|
+
.proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
|
|
262
264
|
.projBody{flex:1;min-width:0;}
|
|
263
265
|
.projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
264
266
|
.projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
|
|
@@ -503,7 +505,7 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
503
505
|
border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);}
|
|
504
506
|
:root[data-mode="dark"] .worldOrb[data-rel="builder"] .badge{color:var(--sun-lit);}
|
|
505
507
|
@media (prefers-color-scheme: dark){:root:not([data-mode="light"]) .worldOrb[data-rel="builder"] .badge{color:var(--sun-lit);}}
|
|
506
|
-
.worldOrb .chip{transform:scale(.88);}
|
|
508
|
+
.worldOrb .chip{transform:scale(.88);white-space:nowrap;}
|
|
507
509
|
|
|
508
510
|
/* what the only-mine filter shows when you are connected to nothing. Not a
|
|
509
511
|
hypothetical — GET /my-projects returns { owned: [], building: [] } for the
|
|
@@ -453,6 +453,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
453
453
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
454
454
|
card that changed while you were on the map is not stale */
|
|
455
455
|
if (v === 'mine' && me) refreshList();
|
|
456
|
+
/* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
|
|
457
|
+
if (v === 'map' && me) loadOwnedInstances().then(applyMapFilter);
|
|
456
458
|
/* focus the name field only on a deliberate move into the view — never on
|
|
457
459
|
boot, where it would steal focus from whatever the visitor arrived at */
|
|
458
460
|
if (v === 'new' && focus) $('newName').focus();
|
|
@@ -528,6 +530,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
528
530
|
var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
|
|
529
531
|
var catalog = [];
|
|
530
532
|
var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
|
|
533
|
+
var ownedInstances = []; /* the viewer's OWN provisioning rows (never torn_down) */
|
|
534
|
+
var ownedByHost = {}; /* lower(domain) → instance row — the dedupe join */
|
|
531
535
|
/* What the platform's own entry is FOR SEARCH. It is deliberately not pushed
|
|
532
536
|
into `catalog`: the catalog is the federated directory of other instances,
|
|
533
537
|
and a synthetic member of it would flow into accent assignment, orb slots
|
|
@@ -562,7 +566,24 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
562
566
|
.forEach(function (p, i) { p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length]; });
|
|
563
567
|
}
|
|
564
568
|
function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
|
|
565
|
-
|
|
569
|
+
/* the manage cards' accent rule — one project, one colour, in the sky AND
|
|
570
|
+
the list. Shared with orbFor(), which used to carry this hash inline. */
|
|
571
|
+
function slugAccent(slug) {
|
|
572
|
+
var h = 0, s = String(slug || '');
|
|
573
|
+
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
574
|
+
return h % ORB_ACCENTS.length;
|
|
575
|
+
}
|
|
576
|
+
function relOf(p) {
|
|
577
|
+
/* an instance the viewer provisioned is a server-resolved authority too —
|
|
578
|
+
own-scoped by owner_builder_id, no client inference (direction §2
|
|
579
|
+
principle 5). It outranks a membership row: owning the instance is the
|
|
580
|
+
stronger claim. The _inst annotation covers owned entries with no
|
|
581
|
+
domain yet; ownedByHost covers any un-annotated caller. */
|
|
582
|
+
if (p._inst) return 'owner';
|
|
583
|
+
var h = hostOf(p.origin);
|
|
584
|
+
if (ownedByHost[h]) return 'owner';
|
|
585
|
+
return mineOrigins[h] || 'none';
|
|
586
|
+
}
|
|
566
587
|
/* The platform's own relationship cannot come from mineOrigins, and not
|
|
567
588
|
because the data is merely missing: platform_identity_project_memberships
|
|
568
589
|
is written ONLY by the server-to-server check-in a FEDERATED instance posts
|
|
@@ -594,7 +615,11 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
594
615
|
}
|
|
595
616
|
function matchesMapQuery(p, q) {
|
|
596
617
|
if (!q) return true;
|
|
597
|
-
|
|
618
|
+
/* an owned entry also answers by slug and address — the same fields the
|
|
619
|
+
manage view's search reads, so the habit learned there works here */
|
|
620
|
+
var hay = String(p.name || '') + ' ' + String(p.tagline || '') +
|
|
621
|
+
(p._inst ? ' ' + String(p._inst.slug || '') + ' ' + String(p._inst.domain || '') : '');
|
|
622
|
+
return hay.toLowerCase().indexOf(q) !== -1;
|
|
598
623
|
}
|
|
599
624
|
|
|
600
625
|
function renderOrbs(list) {
|
|
@@ -608,24 +633,39 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
608
633
|
var a = document.createElement('a');
|
|
609
634
|
a.className = 'worldOrb';
|
|
610
635
|
a.setAttribute('data-generated', '');
|
|
611
|
-
|
|
636
|
+
var rel = relOf(p);
|
|
637
|
+
a.setAttribute('data-rel', rel);
|
|
638
|
+
var inst = p._inst;
|
|
612
639
|
var href = safeHttpUrl(p.origin);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
640
|
+
if (inst) {
|
|
641
|
+
/* an OWNED entry — catalog-annotated or owned-only, one code path.
|
|
642
|
+
Its job is MANAGE (direction §5): same tab, anchored on that
|
|
643
|
+
project. The public-site door still exists on the manage card; a
|
|
644
|
+
middle-click lands a fresh tab on the manage view, which is this
|
|
645
|
+
orb's honest destination. */
|
|
646
|
+
a.href = '?view=mine';
|
|
647
|
+
a.setAttribute('data-manage', String(inst.id));
|
|
648
|
+
} else {
|
|
649
|
+
/* The href STAYS, and stays honest, even though a plain click now opens
|
|
650
|
+
the card instead of following it: it is what makes the orb a real link
|
|
651
|
+
— focusable, Enter-activatable, and still ctrl/middle-clickable into a
|
|
652
|
+
new tab. The card is layered over that, not substituted for it. */
|
|
653
|
+
a.href = href || '#';
|
|
654
|
+
if (href) { a.target = '_blank'; a.rel = 'noopener'; }
|
|
655
|
+
}
|
|
619
656
|
a.setAttribute('data-origin', String(p.origin || ''));
|
|
620
657
|
a.title = p.tagline || p.name || '';
|
|
621
658
|
a.style.cssText = '--acc:' + accentOf(p) +
|
|
622
659
|
';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
|
|
623
|
-
var rel = relOf(p);
|
|
624
660
|
a.innerHTML =
|
|
625
661
|
(rel === 'owner' ? '<span class="badge">yours</span>' :
|
|
626
662
|
rel === 'builder' ? '<span class="badge">you build here</span>' : '') +
|
|
627
663
|
'<span class="disc"></span>' +
|
|
628
|
-
'<span class="nm">' + esc(p.name) + '</span>'
|
|
664
|
+
'<span class="nm">' + esc(p.name) + '</span>' +
|
|
665
|
+
/* the ONE chip site, deferring to the manage list's vocabulary — so a
|
|
666
|
+
project can never look alive in the sky and unreachable in the list
|
|
667
|
+
(C6: two views of one truth) */
|
|
668
|
+
(inst ? chipFor(inst) : '');
|
|
629
669
|
field.insertBefore(a, hint);
|
|
630
670
|
});
|
|
631
671
|
}
|
|
@@ -641,7 +681,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
641
681
|
var rel = relOf(p);
|
|
642
682
|
var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
|
|
643
683
|
: rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
|
|
644
|
-
return '<tr data-generated data-
|
|
684
|
+
return '<tr data-generated ' + (p._inst ? 'data-manage="' + esc(String(p._inst.id)) + '" ' : '') +
|
|
685
|
+
'data-origin="' + esc(String(p.origin || '')) + '">' +
|
|
645
686
|
'<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
|
|
646
687
|
accentOf(p) + '"></span>' +
|
|
647
688
|
'<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
|
|
@@ -653,10 +694,13 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
653
694
|
"we could not ask". */
|
|
654
695
|
'<td class="right num">' + (p.builders == null ? '—' : esc(String(p.builders))) + '</td>' +
|
|
655
696
|
'<td class="colTags">' + tagsHtml(p.tags) + '</td>' +
|
|
656
|
-
/* liveness on a
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
697
|
+
/* liveness on a CATALOG row is NOT probed by this surface — a chip
|
|
698
|
+
saying "live" there would claim more than anything verified (C6).
|
|
699
|
+
An OWNED row is different: the own-scoped feed answers for it, so
|
|
700
|
+
it carries chipFor()'s truth, the same chip its orb and its manage
|
|
701
|
+
card show (task 1002744). */
|
|
702
|
+
'<td class="right colStatus">' + (p._inst ? chipFor(p._inst)
|
|
703
|
+
: '<span class="chip calm"><span class="cdot"></span>—</span>') + '</td>' +
|
|
660
704
|
'<td class="colGo">' + (safeHttpUrl(p.origin)
|
|
661
705
|
? '<a class="btn small ghost" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
|
|
662
706
|
: '') + '</td>' +
|
|
@@ -667,7 +711,30 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
667
711
|
function applyMapFilter() {
|
|
668
712
|
var onlyMine = $('onlyMine').checked;
|
|
669
713
|
var q = $('mapQ').value.trim().toLowerCase();
|
|
670
|
-
|
|
714
|
+
/* ONE browse list feeds both sub-views (task 1002744): every catalog row
|
|
715
|
+
is annotated with the viewer's own instance when the hosts join — so a
|
|
716
|
+
project both featured and owned renders once, marked owned — and owned
|
|
717
|
+
instances the catalog does not carry are appended as first-class
|
|
718
|
+
entries. The sky, the table, the search box and the count read the same
|
|
719
|
+
list and can never disagree about what exists. */
|
|
720
|
+
var catalogHosts = {};
|
|
721
|
+
catalog.forEach(function (p) {
|
|
722
|
+
var h = hostOf(p.origin);
|
|
723
|
+
catalogHosts[h] = 1;
|
|
724
|
+
p._inst = ownedByHost[h] || null;
|
|
725
|
+
});
|
|
726
|
+
var browse = catalog.slice();
|
|
727
|
+
ownedInstances.forEach(function (inst) {
|
|
728
|
+
var h = inst.domain ? hostOf(inst.domain) : '';
|
|
729
|
+
if (h && catalogHosts[h]) return; /* annotated onto its catalog row above */
|
|
730
|
+
browse.push({
|
|
731
|
+
name: inst.slug, tagline: inst.domain || '',
|
|
732
|
+
origin: inst.domain ? 'https://' + inst.domain : '',
|
|
733
|
+
builders: null, tags: null,
|
|
734
|
+
_inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)]
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
var list = browse.filter(function (p) {
|
|
671
738
|
return matchesMapQuery(p, q) && (!onlyMine || relOf(p) !== 'none');
|
|
672
739
|
});
|
|
673
740
|
list.sort(function (x, y) {
|
|
@@ -777,16 +844,44 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
777
844
|
if (!a || !isPlainClick(ev)) return;
|
|
778
845
|
ev.preventDefault();
|
|
779
846
|
if (a.id === 'selfOrb') return openCard(SELF_PROJECT, selfRel(), a);
|
|
847
|
+
/* an owned orb's job is MANAGE (direction §5) — before the card path */
|
|
848
|
+
if (a.hasAttribute('data-manage')) return goToManage(a.getAttribute('data-manage'));
|
|
780
849
|
var p = projByOrigin(a.getAttribute('data-origin') || '');
|
|
781
850
|
if (p) openCard(p, relOf(p), a);
|
|
782
851
|
});
|
|
783
852
|
|
|
853
|
+
/* ── Click-to-manage (task 1002744): an owned orb opens YOUR side of the hub,
|
|
854
|
+
anchored on that project's card. The anchor is deferred — setView('mine')
|
|
855
|
+
triggers an async refreshList(), so the target `[data-inst]` node may not
|
|
856
|
+
exist yet; render() and refreshList() retry tryAnchor() once it does. ── */
|
|
857
|
+
var ANCHOR_GLOW_MS = 2200;
|
|
858
|
+
var pendingAnchor = null;
|
|
859
|
+
function goToManage(instId) {
|
|
860
|
+
pendingAnchor = String(instId || '');
|
|
861
|
+
setView('mine');
|
|
862
|
+
tryAnchor(); /* the list may already be rendered from an earlier visit */
|
|
863
|
+
}
|
|
864
|
+
function tryAnchor() {
|
|
865
|
+
if (!pendingAnchor) return;
|
|
866
|
+
var li = document.querySelector('li[data-inst="' + pendingAnchor + '"]');
|
|
867
|
+
if (!li) return;
|
|
868
|
+
pendingAnchor = null;
|
|
869
|
+
/* a search or state filter that hides the target would turn the anchor
|
|
870
|
+
into a silent no-op — clear them the same way the visitor would */
|
|
871
|
+
if (li.hidden) $('mineClear').click();
|
|
872
|
+
li.scrollIntoView({ block: 'center' });
|
|
873
|
+
li.classList.add('anchored');
|
|
874
|
+
setTimeout(function () { li.classList.remove('anchored'); }, ANCHOR_GLOW_MS);
|
|
875
|
+
}
|
|
876
|
+
|
|
784
877
|
$('mapRows').addEventListener('click', function (ev) {
|
|
785
878
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
786
879
|
if (ev.target.closest && ev.target.closest('a')) return;
|
|
787
880
|
var tr = ev.target.closest && ev.target.closest('tr');
|
|
788
881
|
if (!tr || !isPlainClick(ev)) return;
|
|
789
882
|
if (tr.id === 'selfRow') return openCard(SELF_PROJECT, selfRel(), null);
|
|
883
|
+
/* an owned row goes to MANAGE, exactly like its orb — two views, one job */
|
|
884
|
+
if (tr.hasAttribute('data-manage')) return goToManage(tr.getAttribute('data-manage'));
|
|
790
885
|
var p = projByOrigin(tr.getAttribute('data-origin') || '');
|
|
791
886
|
if (p) openCard(p, relOf(p), null);
|
|
792
887
|
});
|
|
@@ -846,6 +941,29 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
846
941
|
.catch(function () { mineOrigins = {}; });
|
|
847
942
|
}
|
|
848
943
|
|
|
944
|
+
/* The owner-scoped half of the sky (task 1002744). The public catalog
|
|
945
|
+
cannot answer "is this mine" — /projects/featured is rank:public and
|
|
946
|
+
varies by nobody — so the map merges the viewer's OWN provisioning rows:
|
|
947
|
+
GET /provisioning/instances is own-scoped by construction and carries the
|
|
948
|
+
same last_verified_* overlay the manage list renders. Signed out (401) and
|
|
949
|
+
a child instance with provisioning off (404) both read as "no owned
|
|
950
|
+
projects here", never as an error. */
|
|
951
|
+
function loadOwnedInstances() {
|
|
952
|
+
if (!me) { ownedInstances = []; ownedByHost = {}; return Promise.resolve(); }
|
|
953
|
+
return fetch(API + '/provisioning/instances', { credentials: 'same-origin' })
|
|
954
|
+
.then(function (res) { return res.ok ? res.json() : { instances: [] }; })
|
|
955
|
+
.then(function (d) {
|
|
956
|
+
/* a torn-down project is filed under "gone" in the manage list — the
|
|
957
|
+
sky does not float what no longer exists */
|
|
958
|
+
ownedInstances = (d.instances || []).filter(function (inst) { return inst.status !== 'torn_down'; });
|
|
959
|
+
ownedByHost = {};
|
|
960
|
+
ownedInstances.forEach(function (inst) {
|
|
961
|
+
if (inst.domain) ownedByHost[hostOf(inst.domain)] = inst;
|
|
962
|
+
});
|
|
963
|
+
})
|
|
964
|
+
.catch(function () { ownedInstances = []; ownedByHost = {}; });
|
|
965
|
+
}
|
|
966
|
+
|
|
849
967
|
$('onlyMine').addEventListener('change', applyMapFilter);
|
|
850
968
|
$('mapQ').addEventListener('input', applyMapFilter);
|
|
851
969
|
document.querySelectorAll('.tbl th[data-sort]').forEach(function (th) {
|
|
@@ -1043,9 +1161,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1043
1161
|
/* ── the orb: the gate's project sphere, glow keyed to verification ────── */
|
|
1044
1162
|
|
|
1045
1163
|
function orbFor(inst) {
|
|
1046
|
-
var
|
|
1047
|
-
for (var i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
|
|
1048
|
-
var k = h % ORB_ACCENTS.length;
|
|
1164
|
+
var k = slugAccent(inst.slug);
|
|
1049
1165
|
var cls = 'orb';
|
|
1050
1166
|
if (inst.status === 'torn_down' || inst.status === 'tearing_down') cls += ' ghost';
|
|
1051
1167
|
else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
|
|
@@ -1215,6 +1331,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1215
1331
|
});
|
|
1216
1332
|
recountMine();
|
|
1217
1333
|
applyMine();
|
|
1334
|
+
tryAnchor(); /* a click-to-manage jump may be waiting on this render */
|
|
1218
1335
|
}
|
|
1219
1336
|
|
|
1220
1337
|
function msg(id, text, cls) {
|
|
@@ -1304,6 +1421,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1304
1421
|
var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
|
|
1305
1422
|
if (!sameSet) { render(instances); return; }
|
|
1306
1423
|
instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
|
|
1424
|
+
tryAnchor(); /* same-set refresh renders no shells — retry here too */
|
|
1307
1425
|
})
|
|
1308
1426
|
.catch(function () { /* transient — next tick will try again */ });
|
|
1309
1427
|
}
|
|
@@ -1312,7 +1430,36 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1312
1430
|
while you browsed the sky would be spending requests on nothing */
|
|
1313
1431
|
function shouldPoll() { return !document.hidden && me && currentView === 'mine'; }
|
|
1314
1432
|
setInterval(function () { if (shouldPoll()) refreshList(); }, REFRESH_MS);
|
|
1315
|
-
document.addEventListener('visibilitychange', function () {
|
|
1433
|
+
document.addEventListener('visibilitychange', function () {
|
|
1434
|
+
if (shouldPoll()) refreshList();
|
|
1435
|
+
/* returning to a MAP tab re-reads the owned overlay once — event-driven,
|
|
1436
|
+
like the mine view's entry refresh, not a timer */
|
|
1437
|
+
if (!document.hidden && me && currentView === 'map') loadOwnedInstances().then(applyMapFilter);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
/* C6 freshness without C8's cost: once a minute the visible MAP refreshes
|
|
1441
|
+
its owned chips from data already in hand — ZERO requests — so "live ·
|
|
1442
|
+
checked 2m ago" keeps telling the truth and downgrades to live? once the
|
|
1443
|
+
probe ages past FRESH_MS. Chips only: rebuilding every orb and row to
|
|
1444
|
+
re-date a label would be pointless DOM churn. The re-FETCH happens only
|
|
1445
|
+
on view entry and tab return, never on this timer. */
|
|
1446
|
+
function refreshOwnedChips() {
|
|
1447
|
+
var byManageId = {};
|
|
1448
|
+
ownedInstances.forEach(function (inst) { byManageId[String(inst.id)] = inst; });
|
|
1449
|
+
document.querySelectorAll('#view-map [data-manage]').forEach(function (el) {
|
|
1450
|
+
var inst = byManageId[el.getAttribute('data-manage')];
|
|
1451
|
+
var chip = inst && el.querySelector('.chip');
|
|
1452
|
+
if (!chip) return;
|
|
1453
|
+
var wrap = document.createElement('span');
|
|
1454
|
+
wrap.innerHTML = chipFor(inst);
|
|
1455
|
+
chip.replaceWith(wrap.firstChild);
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
function mapChipTick() {
|
|
1459
|
+
if (document.hidden || currentView !== 'map' || !ownedInstances.length) return;
|
|
1460
|
+
refreshOwnedChips();
|
|
1461
|
+
}
|
|
1462
|
+
setInterval(mapChipTick, REFRESH_MS);
|
|
1316
1463
|
|
|
1317
1464
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
1318
1465
|
SAY exactly what the server said ── */
|
|
@@ -1509,7 +1656,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1509
1656
|
$('gate').hidden = true;
|
|
1510
1657
|
$('mine').hidden = false;
|
|
1511
1658
|
load();
|
|
1512
|
-
return loadMemberships();
|
|
1659
|
+
return Promise.all([loadMemberships(), loadOwnedInstances()]);
|
|
1513
1660
|
})
|
|
1514
1661
|
.catch(function (err) {
|
|
1515
1662
|
me = null;
|
package/src/module-api.js
CHANGED
|
@@ -639,8 +639,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
639
639
|
// landed since 1.19.104 with no explicit bump. run 31758526405. (task 1002620)
|
|
640
640
|
// 1.19.106 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
641
641
|
// landed since 1.19.105 with no explicit bump. run 31759315328. (task 1002620)
|
|
642
|
+
// 1.19.107 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
643
|
+
// landed since 1.19.106 with no explicit bump. run 31762781737. (task 1002620)
|
|
644
|
+
// 1.19.108 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
645
|
+
// landed since 1.19.107 with no explicit bump. run 31803763842. (task 1002620)
|
|
642
646
|
// ---------------------------------------------------------------------------
|
|
643
|
-
const CORE_VERSION = '1.19.
|
|
647
|
+
const CORE_VERSION = '1.19.108'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
644
648
|
|
|
645
649
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
646
650
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/module_loader.mjs
CHANGED
|
@@ -170,16 +170,34 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
170
170
|
// then status-ui (R84) + hall-ui (R85) the two web-surface carves; and
|
|
171
171
|
// provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting);
|
|
172
172
|
// platform-identity (ADR 0141) the hub IdP; and governance (ADR 0151 / BV1.R94) the
|
|
173
|
-
// configurable roles/permissions module
|
|
173
|
+
// configurable roles/permissions module; and agents (ADR 0083 / goal 1000038) the
|
|
174
|
+
// declarative agent registry — the first module to ship default:false, which is
|
|
175
|
+
// why it is DISCOVERED here (discovery is enablement-independent) while staying
|
|
176
|
+
// off in enabledModules(). All are discovered; none has validation
|
|
174
177
|
// errors or coreVersion mismatches. Order is the readdir sort.
|
|
175
178
|
//
|
|
176
179
|
// NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
|
|
177
180
|
// instance/content-excluded from the published core (ADR 0098) so they are not
|
|
178
|
-
// present here — the discovered set is the
|
|
181
|
+
// present here — the discovered set is the 18 always-shipped core-domain + feature
|
|
179
182
|
// modules below.
|
|
183
|
+
//
|
|
184
|
+
// ⚠ ADDING A MODULE? This roster is HARDCODED and you must extend it — otherwise
|
|
185
|
+
// this test reds and your PR cannot land (task 1002787: the agents manifest hit
|
|
186
|
+
// exactly this). It stays hardcoded on purpose: unlike the fitness roster check,
|
|
187
|
+
// which compares two INDEPENDENT disk reads (tests/helpers.mjs onDiskModuleKeys vs
|
|
188
|
+
// fitness.js discoveredModuleKeys), this assertion pins what the PUBLISHED CORE
|
|
189
|
+
// ships (ADR 0098 content-exclusion). Deriving it from the same disk the loader
|
|
190
|
+
// just read would make it tautological — it could no longer catch the loader
|
|
191
|
+
// silently dropping or duplicating a module, which is the thing it exists to catch.
|
|
192
|
+
//
|
|
193
|
+
// ⚠ AND CHECK WHICH TESTS FAIL, NOT HOW MANY FILES. On Windows, origin/main already
|
|
194
|
+
// fails ~21 test FILES, this one among them (see the dedupeRoots test below — a path
|
|
195
|
+
// -resolution env issue). A before/after run compared at FILE granularity therefore
|
|
196
|
+
// reads 21 = 21 and looks clean while a NEW failure hides inside an already-red file.
|
|
197
|
+
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
180
198
|
loader._reset();
|
|
181
199
|
const snap = loader.loaderSnapshot();
|
|
182
|
-
assert.deepEqual(snap.loaded, ['autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
200
|
+
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
183
201
|
assert.deepEqual(snap.errors, []);
|
|
184
202
|
assert.deepEqual(snap.skipped, []);
|
|
185
203
|
loader._reset();
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -269,6 +269,8 @@ test('the self entry is not pushed into the catalog array', () => {
|
|
|
269
269
|
test('the count includes the platform when it is shown', () => {
|
|
270
270
|
assert.doesNotMatch(SCRIPT, /mapCount'\)\.textContent = list\.length/,
|
|
271
271
|
'the count must not ignore the self entry it is rendering beside');
|
|
272
|
+
// owned entries live INSIDE `list` (the merged browse list) — a separate
|
|
273
|
+
// owned term here meant the table under-rendered what the count claimed
|
|
272
274
|
assert.match(SCRIPT, /var shown = list\.length \+ \(selfShown \? 1 : 0\)/);
|
|
273
275
|
});
|
|
274
276
|
|
|
@@ -541,3 +543,95 @@ test('no anchor in the table ships a placeholder href', () => {
|
|
|
541
543
|
const generated = (SCRIPT.match(/return '<tr data-generated([\s\S]*?)'<\/tr>';/) || [, ''])[1];
|
|
542
544
|
assert.doesNotMatch(generated, /href="#"/, 'generated rows must not ship placeholders either');
|
|
543
545
|
});
|
|
546
|
+
|
|
547
|
+
/* ── The owner-scoped sky (task 1002744) ────────────────────────────────────
|
|
548
|
+
/projects/featured is rank:public, takes _req, and structurally cannot say
|
|
549
|
+
"this one is yours". The sky's owner half comes from the viewer's OWN
|
|
550
|
+
provisioning rows instead: GET /provisioning/instances is own-scoped by
|
|
551
|
+
construction (owner_builder_id = the session's builder) and carries the same
|
|
552
|
+
last_verified_* overlay the manage list renders — one truth, two views (C6).
|
|
553
|
+
These pin the shape: merge client-side, dedupe against the catalog, mark
|
|
554
|
+
owned, chip with the SAME vocabulary, and a plain click goes to MANAGE. ── */
|
|
555
|
+
|
|
556
|
+
test('the sky learns ownership from the viewer\'s own instances, signed-in only', () => {
|
|
557
|
+
const fn = (SCRIPT.match(/function loadOwnedInstances\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
558
|
+
assert.ok(fn, 'loadOwnedInstances() must exist');
|
|
559
|
+
assert.match(fn, /if \(!me\)/, 'a signed-out visitor sees the catalog exactly as today');
|
|
560
|
+
assert.match(fn, /provisioning\/instances/, 'the own-scoped feed is the source');
|
|
561
|
+
assert.match(fn, /torn_down/, 'the sky must not float what no longer exists');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('a project both featured and owned renders once, marked owned', () => {
|
|
565
|
+
const fn = (SCRIPT.match(/function relOf\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
566
|
+
assert.ok(fn, 'relOf() must exist');
|
|
567
|
+
assert.match(fn, /ownedByHost/,
|
|
568
|
+
'the ownership join lives in the one predicate both browse views already read');
|
|
569
|
+
// the "once" itself: the browse merge ANNOTATES the catalog row and skips
|
|
570
|
+
// appending a duplicate — deleting either half double-renders the project
|
|
571
|
+
const filter = (SCRIPT.match(/function applyMapFilter\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
572
|
+
assert.ok(filter, 'applyMapFilter() must be extractable');
|
|
573
|
+
assert.match(filter, /p\._inst = ownedByHost/, 'catalog rows are annotated with the owned instance');
|
|
574
|
+
assert.match(filter, /if \(h && catalogHosts\[h\]\) return;/, 'an annotated project is never appended twice');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('owned truth reaches the TABLE too — same list, same chip, same manage job', () => {
|
|
578
|
+
// the count, the sky and the table read ONE merged browse list; an owned row
|
|
579
|
+
// carries chipFor()'s truth in Status and routes its click to manage
|
|
580
|
+
const generated = (SCRIPT.match(/return '<tr data-generated([\s\S]*?)'<\/tr>';/) || [, ''])[1];
|
|
581
|
+
assert.ok(generated, 'the generated-row template must keep its extractable shape');
|
|
582
|
+
assert.match(generated, /data-manage/, 'an owned row carries the manage route key');
|
|
583
|
+
assert.match(generated, /p\._inst \? chipFor\(p\._inst\)/, 'an owned row\'s Status is the overlay truth');
|
|
584
|
+
assert.match(generated, /chip calm/, 'an unprobed catalog row keeps the honest calm dash');
|
|
585
|
+
assert.match(SCRIPT, /tr\.hasAttribute\('data-manage'\)\) return goToManage/,
|
|
586
|
+
'an owned row click goes to manage, exactly like its orb');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('an owned orb\'s job is manage, not the public site', () => {
|
|
590
|
+
assert.match(SCRIPT, /'\?view=mine'/, 'the owned href is the manage deep-link, real and middle-clickable');
|
|
591
|
+
assert.match(SCRIPT, /hasAttribute\('data-manage'\)\) return goToManage/,
|
|
592
|
+
'a plain click on an owned orb routes to manage before the card path');
|
|
593
|
+
const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
|
|
594
|
+
assert.ok(orbs, 'renderOrbs must be extractable');
|
|
595
|
+
assert.match(orbs, /data-manage/, 'owned orbs carry the manage route key');
|
|
596
|
+
// data-inst is the MINE view's card-shell identity; an orb wearing it would
|
|
597
|
+
// shadow every unqualified [data-inst] lookup renderCard/applyMine make
|
|
598
|
+
assert.doesNotMatch(orbs, /data-inst/,
|
|
599
|
+
'the orb route key must not collide with the manage list\'s card identity');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('the orb chip is the manage list\'s truth, never a second story (C6)', () => {
|
|
603
|
+
const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
|
|
604
|
+
// exactly ONE chip site, and it defers to chipFor() — a second site could
|
|
605
|
+
// hardcode a claim while this assertion still found its token
|
|
606
|
+
assert.equal((orbs.match(/chipFor\(inst\)/g) || []).length, 1,
|
|
607
|
+
'renderOrbs must have exactly one chip call site');
|
|
608
|
+
assert.match(orbs, /\(inst \? chipFor\(inst\) : ''\)/,
|
|
609
|
+
'the chip is chipFor() or nothing — never a literal class');
|
|
610
|
+
assert.doesNotMatch(SCRIPT, /chip live[^']*'\s*\+\s*esc\(inst\./,
|
|
611
|
+
'no instance chip may be literal-classed live — chipMarkup() decides from the overlay');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('click-to-manage anchors the project and defers until the card exists', () => {
|
|
615
|
+
assert.match(SCRIPT, /function goToManage/);
|
|
616
|
+
assert.match(SCRIPT, /pendingAnchor/);
|
|
617
|
+
assert.match(SCRIPT, /scrollIntoView/);
|
|
618
|
+
const fn = (SCRIPT.match(/function tryAnchor\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
619
|
+
assert.ok(fn, 'tryAnchor() must be extractable');
|
|
620
|
+
assert.match(fn, /mineClear/, 'a filter that hides the anchored card is cleared, not silently no-op\'d');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('the map does not poll for owned truth — it re-reads on entry (C8)', () => {
|
|
624
|
+
assert.match(SCRIPT, /function shouldPoll\(\)[\s\S]*?currentView === 'mine'/);
|
|
625
|
+
assert.match(SCRIPT, /v === 'map' && me\) loadOwnedInstances/,
|
|
626
|
+
'entering the map refreshes the owned overlay once, never on a timer');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('the chip-freshness tick spends no requests — it re-renders what it has (C6+C8)', () => {
|
|
630
|
+
// without this tick "live · checked 2m ago" freezes and outlives FRESH_MS;
|
|
631
|
+
// with a FETCH in it the map would be polling, which C8 forbids
|
|
632
|
+
const fn = (SCRIPT.match(/function mapChipTick\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
633
|
+
assert.ok(fn, 'mapChipTick() must exist');
|
|
634
|
+
assert.match(fn, /currentView !== 'map'/, 'the tick is map-view-guarded');
|
|
635
|
+
assert.match(fn, /document\.hidden/, 'and does nothing in a hidden tab');
|
|
636
|
+
assert.doesNotMatch(fn, /fetch\(/, 'zero requests — the re-fetch belongs to entry and tab return only');
|
|
637
|
+
});
|