@bongos/core 1.19.106 → 1.19.107

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.106",
6
- "core_contract": "1.19.106",
7
- "source_commit": "9c4d81635ba2c3b6781454fef02a2b4fd03c313a",
5
+ "core_version": "1.19.107",
6
+ "core_contract": "1.19.107",
7
+ "source_commit": "85d496250f3a30925cf27a4601462f516e6de778",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-14T01:02:38.447Z",
9
+ "built_at": "2026-08-14T02:08:28.305Z",
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": "8dc050d8d7110c73665132a52cad7e280ab1b8db7053b35778cd87c03a4d9b6e",
20
+ "tree_sha256": "61133ba38baa892b82011a8424092f2ccd2d26b485d9d8043d9c20d0d3abae17",
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": "5ac251e031cc6f47a48545e288edb4c894b5832a1a27f80c4ff39abf23f17517"
4150
+ "sha256": "bd7f6faf78443413a2f0142bd9f72e4fff1eb31fefd2225697f0011599f1a218"
4151
4151
  },
4152
4152
  {
4153
4153
  "path": "package.json",
4154
4154
  "mode": "0000644",
4155
- "sha256": "ecc9e5581a436fcf4bafb8d8d9d64f688884d0344f7c484e5cfeadace6f4a198"
4155
+ "sha256": "d14fe248ed9e458a9ebc3a9c7c75101082f44861712860800f341d6218ccb61f"
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": "3ae9d1edc18eef49ebdf90d0c20c00defda76bfb393936c4a615720314a2886e"
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": "e9fd91527dd347041b35701f67fe4b245e99ec0cbe015a98bbae24cb9fcfea52"
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": "20c85ad2a6dc6be87060b034fb9aaa10712463f9f12164d836588f8a28cf3caf"
5730
+ "sha256": "8fb4486e1a47e0c244cec140b06db1175ce7c3b1418b7dd27d71073020a189ca"
5731
5731
  },
5732
5732
  {
5733
5733
  "path": "src/module-loader/catalog.js",
@@ -7252,7 +7252,7 @@
7252
7252
  {
7253
7253
  "path": "tests/projects_hub.mjs",
7254
7254
  "mode": "0000644",
7255
- "sha256": "1671466fb3301b1b549835ff7e7466a1374079a470f58773cdeddbe81ba3b478"
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.106",
3
+ "version": "1.19.107",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.106",
9
+ "version": "1.19.107",
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.106",
3
+ "version": "1.19.107",
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
- function relOf(p) { return mineOrigins[hostOf(p.origin)] || 'none'; }
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
- return (String(p.name || '') + ' ' + String(p.tagline || '')).toLowerCase().indexOf(q) !== -1;
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
- a.setAttribute('data-rel', relOf(p));
636
+ var rel = relOf(p);
637
+ a.setAttribute('data-rel', rel);
638
+ var inst = p._inst;
612
639
  var href = safeHttpUrl(p.origin);
613
- /* The href STAYS, and stays honest, even though a plain click now opens
614
- the card instead of following it: it is what makes the orb a real link
615
- — focusable, Enter-activatable, and still ctrl/middle-clickable into a
616
- new tab. The card is layered over that, not substituted for it. */
617
- a.href = href || '#';
618
- if (href) { a.target = '_blank'; a.rel = 'noopener'; }
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-origin="' + esc(String(p.origin || '')) + '">' +
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 catalog row is NOT probed by this surface. A chip that
657
- said "live" here would claim more than anything verified (C6); the
658
- verification overlay on owned orbs is its own task. */
659
- '<td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>' +
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
- var list = catalog.filter(function (p) {
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 h = 0, slug = String(inst.slug || '');
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 () { if (shouldPoll()) refreshList(); });
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,10 @@ 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)
642
644
  // ---------------------------------------------------------------------------
643
- const CORE_VERSION = '1.19.106'; // CI auto-patch carrier (ADR 0161); see changelog above
645
+ const CORE_VERSION = '1.19.107'; // CI auto-patch carrier (ADR 0161); see changelog above
644
646
 
645
647
  // A namespaced logger so a module's log lines are attributable + consistent.
646
648
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -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
+ });