@bongos/core 1.19.584 → 1.19.585

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.584",
6
- "core_contract": "1.19.584",
7
- "source_commit": "7c7b294c8826106fad152689caf15014afbaf444",
5
+ "core_version": "1.19.585",
6
+ "core_contract": "1.19.585",
7
+ "source_commit": "9352691bb382c387bdccba87b372194ba8c689b0",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-07T21:57:27.222Z",
9
+ "built_at": "2026-09-07T22:05:13.505Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 453,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2056,
14
+ "functional_verbatim": 2057,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2533,
20
- "tree_sha256": "1091075083386a5cf37d1f5736153ed4f8cb23a152f278b1120d408446e48f14",
19
+ "file_count": 2534,
20
+ "tree_sha256": "afd5fa885788c749e7a91e2ae3761266dc7c5ebe26278185542576129ecf7dd2",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2707,7 +2707,7 @@
2707
2707
  {
2708
2708
  "path": "docs/module-api-changelog.md",
2709
2709
  "mode": "0000644",
2710
- "sha256": "2e124b22362c309eb08016d25db0dfbcd5d7ee2930db895c21b5053a4565a782"
2710
+ "sha256": "0aaa401d7dc70186f6b739772f0411eb9458e93e3266d1d4301e212ee7d9473b"
2711
2711
  },
2712
2712
  {
2713
2713
  "path": "docs/modules-contract.md",
@@ -6572,7 +6572,7 @@
6572
6572
  {
6573
6573
  "path": "modules/public-landing/public/assets/cosmos.css",
6574
6574
  "mode": "0000644",
6575
- "sha256": "51dc6eac59a85e24336597e554e84c7e76294e389b443cc40dd4e75b1684079d"
6575
+ "sha256": "17637a0fd4eafddcd3235dec51c0ff196a8b39360918ab7a4c0370fc21d3a20f"
6576
6576
  },
6577
6577
  {
6578
6578
  "path": "modules/public-landing/public/assets/verro-medallion.png",
@@ -6832,7 +6832,7 @@
6832
6832
  {
6833
6833
  "path": "modules/public-landing/public/projects.html",
6834
6834
  "mode": "0000644",
6835
- "sha256": "79b713d994571f5f2405605fa19bd1b0ec05546f2ff606227afa9c91392bd969"
6835
+ "sha256": "9c3a537d8c0ae0e5d8ddcdf2c69a859e1c72647203c8b177cc282dcad0c97e7f"
6836
6836
  },
6837
6837
  {
6838
6838
  "path": "modules/public-landing/public/projects.probes.json",
@@ -7577,12 +7577,12 @@
7577
7577
  {
7578
7578
  "path": "package-lock.json",
7579
7579
  "mode": "0000644",
7580
- "sha256": "1dedc1b50a922313443c0c20150eb85bff94a6b56d86009a39084220724e3080"
7580
+ "sha256": "995314658da0abc7f33ea3719d38b99ee78336573d89c5af94b6a752bb840ea4"
7581
7581
  },
7582
7582
  {
7583
7583
  "path": "package.json",
7584
7584
  "mode": "0000644",
7585
- "sha256": "03cb18f378e3d31ff342f11c865a8a97a9c08a1b6be530e35ca89fcb50632b3d"
7585
+ "sha256": "d48fe5ff721c79f7f5673e1c1a9fa8aff96c5bc0bb86af3795ec5b6bb15f0fe9"
7586
7586
  },
7587
7587
  {
7588
7588
  "path": "public-docs/index.html",
@@ -9292,7 +9292,7 @@
9292
9292
  {
9293
9293
  "path": "src/module-api.js",
9294
9294
  "mode": "0000644",
9295
- "sha256": "21ef11c8ad8fe92b37ecb6279f79cbbf8d61a25283bb2623d50cf6db210414f7"
9295
+ "sha256": "1f7f432992c33dfa9ab9c06722b952fd6f4ae1239b3ddb591be5f1c72ae77319"
9296
9296
  },
9297
9297
  {
9298
9298
  "path": "src/module-loader/catalog.js",
@@ -10914,6 +10914,11 @@
10914
10914
  "mode": "0000644",
10915
10915
  "sha256": "7d384cd84cdc93b8e7b57df291a4600a40f43346fd2a52168324ecde219f1388"
10916
10916
  },
10917
+ {
10918
+ "path": "tests/hub_map_orb_states.mjs",
10919
+ "mode": "0000644",
10920
+ "sha256": "cdbee4e7f252f01e4a72feb1b63a5e340d1110c3f7a88aa6db77da95bc1268cb"
10921
+ },
10917
10922
  {
10918
10923
  "path": "tests/hub_profile_leak.mjs",
10919
10924
  "mode": "0000644",
@@ -1617,5 +1617,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1617
1617
  landed since 1.19.582 with no explicit bump. run 34162998083. (task 1002620)
1618
1618
  1.19.584 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1619
1619
  landed since 1.19.583 with no explicit bump. run 34164936809. (task 1002620)
1620
+ 1.19.585 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1621
+ landed since 1.19.584 with no explicit bump. run 34165392436. (task 1002620)
1620
1622
  ---------------------------------------------------------------------------
1621
1623
  ```
@@ -50,6 +50,10 @@
50
50
 
51
51
  /* the gate's locked space palette (dark mode's sky) */
52
52
  --space-deep:#05060f; --space-mid:#0a0c20; --space-high:#0d0f28; --nebula:#2b2456;
53
+ /* the black hole's interior (task 1002328) — the one colour that is NOT
54
+ theme-relative: a black hole is the absence of a surface in either sky, so it
55
+ is declared once here and never restated dark. Explore's own --orb-void. */
56
+ --orb-void:#0b0b10;
53
57
  --sun-lit:#fb923c; --gold:#ffd98a; --gold-soft:#f6c976;
54
58
 
55
59
  /* semantic state tier — each is painted as 12px/700 chip TEXT on its own
@@ -822,6 +826,60 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
822
826
  .worldOrb[data-material="obsidian"][data-shape="blob"]{--tpl-src:var(--plate-obsidian-blob-cut);--tpl-ar:.794;--tpl-zoom:1.55448;--tpl-px:-.2697;--tpl-py:-.15965;}
823
827
  .worldOrb[data-material="obsidian"][data-shape="pair"]{--tpl-src:var(--plate-obsidian-pair-cut);--tpl-ar:2.23705;--tpl-zoom:1.0248;--tpl-px:-.01015;--tpl-py:-.30191;}
824
828
  .worldOrb[data-material="obsidian"][data-shape="bead"]{--tpl-src:var(--plate-obsidian-bead-cut);--tpl-ar:1.0027;--tpl-zoom:1.34698;--tpl-px:-.17053;--tpl-py:-.17255;}
829
+ /* ── THE VISIBILITY RENDER-TYPE (data-orb), a SECOND axis over the planet above
830
+ (BV1.R09, task 1002328; ADR 0182 D1, criterion proj-visibility-states). The
831
+ planet is what a project LOOKS like — the owner's chosen shape and material.
832
+ This is what its door SAYS, derived server-side and read off the feed. The
833
+ three readings are the Explore page's (task 1002326, modules/status-ui/public/
834
+ explore.html), so the two in-repo surfaces that draw this feed agree.
835
+
836
+ PUBLIC is the baseline and takes no rule: the planet as drawn is the public
837
+ planet. Only the two states that differ from it declare anything here. ── */
838
+
839
+ /* PRIVATE — a star: visible from anywhere, with no surface to land on. The object
840
+ keeps its own artwork and its own name (D1: private is on the map UNDER its
841
+ name; what it withholds is the door, not the identity) and gains a star's light.
842
+ A drop-shadow on the disc's own filter, the .worldOrb.match idiom below: the
843
+ object is a MASK, so an outline or a box-shadow would be clipped away. Composed
844
+ after the contact shadow, which dark states as a no-op filter for this reason.
845
+
846
+ Carried as a VARIABLE rather than written into the filter directly, because the
847
+ search spotlight below sets `filter` on the same element and is later in the
848
+ cascade: written inline it would be clobbered, and a private project would
849
+ quietly draw as a planet for as long as a query matched it. */
850
+ .worldOrb[data-orb="star"]{
851
+ --star-light:drop-shadow(0 0 7px color-mix(in srgb,var(--acc,var(--accent)) 75%,transparent))
852
+ drop-shadow(0 0 20px color-mix(in srgb,var(--acc,var(--accent)) 40%,transparent));}
853
+ .worldOrb[data-orb="star"] .disc{filter:var(--orb-shadow) var(--star-light);}
854
+ /* the corona sits BEHIND the object (the relationship ring's grammar, z-index:-1),
855
+ so the star reads as a point of light the object is standing in */
856
+ .worldOrb[data-orb="star"] .disc::after{content:"";position:absolute;
857
+ left:50%;top:50%;width:calc(100% + 34px);aspect-ratio:1;translate:-50% -50%;border-radius:50%;z-index:-1;
858
+ background:radial-gradient(circle,color-mix(in srgb,var(--acc,var(--accent)) 26%,transparent) 0 34%,transparent 70%);}
859
+ /* the ring wins the ::after when the viewer owns or builds on the project — the
860
+ relationship is the more useful thing to say, and the glow above still reads */
861
+ .worldOrb[data-orb="star"][data-rel="owner"] .disc::after,
862
+ .worldOrb[data-orb="star"][data-rel="builder"] .disc::after{background:none;}
863
+
864
+ /* STEALTH — a black hole: existence without identity. The object is REPLACED, not
865
+ decorated: ::before is where the planet plate is painted, so switching it off is
866
+ what makes this the one orb on the map carrying no artwork. Nothing here reads a
867
+ per-project value — every black hole is drawn identically, on the page's own
868
+ accent, because interchangeable is the whole point of the metaphor (D2a). With
869
+ the mask gone a box-shadow is no longer clipped, so the ring is a real ring. */
870
+ .worldOrb[data-orb="black-hole"]{cursor:default;}
871
+ .worldOrb[data-orb="black-hole"] .disc{border-radius:50%;background:var(--orb-void);
872
+ box-shadow:0 0 0 1.5px var(--accent),0 0 14px 2px color-mix(in srgb,var(--accent) 55%,transparent);}
873
+ .worldOrb[data-orb="black-hole"] .disc::before{display:none;}
874
+ /* it does not rise to meet the pointer: there is nothing behind it to open */
875
+ .worldOrb[data-orb="black-hole"]:hover .disc{translate:none;}
876
+ /* the placeholder name is not a project's name, and does not dress as one */
877
+ .worldOrb[data-orb="black-hole"] .nm{color:var(--ink-soft);font-style:italic;}
878
+ /* the table's anonymous row: same rule, and no pointer, since the row's click
879
+ target (its address) is exactly what the row does not have */
880
+ .tbl .tblRow.anon{cursor:default;}
881
+ .tbl .tblRow.anon .ptitle{color:var(--ink-soft);font-style:italic;}
882
+
825
883
  /* the relationship ring: yours takes the accent (the one live thing about the
826
884
  orb); building there is a hairline of it */
827
885
  .worldOrb[data-rel="owner"] .disc::after,.worldOrb[data-rel="builder"] .disc::after{content:"";position:absolute;
@@ -986,8 +1044,12 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
986
1044
  /* the spotlight is a drop-shadow in the accent on the disc's own filter — a
987
1045
  glow that follows the silhouette (the object is a mask, so an outline or a
988
1046
  box-shadow would be clipped away). Composed after the contact shadow, which
989
- dark states as a no-op filter for exactly this. */
990
- .worldOrb.match .disc{filter:var(--orb-shadow) drop-shadow(0 0 16px color-mix(in srgb,var(--acc,var(--accent)) 60%,transparent));}
1047
+ dark states as a no-op filter for exactly this.
1048
+
1049
+ A star composes its own light in after the spotlight (--star-light, set above;
1050
+ the empty fallback makes this a no-op for every other orb), so a private project
1051
+ the query matched does not stop looking private for as long as it matches. */
1052
+ .worldOrb.match .disc{filter:var(--orb-shadow) drop-shadow(0 0 16px color-mix(in srgb,var(--acc,var(--accent)) 60%,transparent)) var(--star-light,);}
991
1053
  .field .hoverCard{position:absolute;z-index:6;pointer-events:none;display:flex;flex-direction:column;gap:2px;
992
1054
  background:var(--bg-card);border:1px solid var(--rule);border-radius:var(--radius-sm);
993
1055
  padding:9px 13px;max-width:240px;}
@@ -1553,6 +1553,39 @@ summary{min-height:24px;padding:3px 0;}
1553
1553
  ORB_SHAPES.forEach(function (s) { ['tint', 'wash', 'chrome'].forEach(function (y) { ORB_TEMPLATES.push(s + '-' + y); }); });
1554
1554
  ORB_SHAPES.forEach(function (s) { ORB_MATERIALS.forEach(function (m) { ['tint', 'wash', 'plain'].forEach(function (y) { ORB_TEMPLATES.push(s + '-' + m + '-' + y); }); }); });
1555
1555
  var ORB_WALK = ['blob-tint', 'pair-gold-wash', 'bead-obsidian-plain', 'blob-frost-wash', 'pair-chrome', 'bead-gold-tint', 'blob-obsidian-wash', 'pair-frost-tint', 'bead-wash', 'blob-gold-plain', 'pair-obsidian-tint', 'bead-frost-wash'];
1556
+ /* THE VISIBILITY RENDER-TYPE — a SECOND axis, and not the planet above (BV1.R09,
1557
+ task 1002328; ADR 0182 D1, criterion proj-visibility-states). The planet is
1558
+ what a project LOOKS like — the owner's chosen shape and material. This is what
1559
+ its door SAYS: public draws a planet, private a star, stealth a black hole.
1560
+ Dark matter never arrives; the feed excludes it in SQL, because a client-side
1561
+ filter ships the husk to anyone who opens a network tab.
1562
+
1563
+ THE VALUE IS READ, NEVER DERIVED. `orb` is computed server-side in
1564
+ modules/platform-identity/projects-feed.js, which holds the only copy of the
1565
+ state→orb mapping in the repo; this page reads that field and does not own a
1566
+ `state` table of its own. The three words below are LABELS for the states, not
1567
+ a second mapping — and they are explore.js's words (R07, task 1002326), so the
1568
+ two in-repo surfaces say the same thing. */
1569
+ var ORB_STATE = { planet: 'Public', star: 'Private', 'black-hole': 'Stealth' };
1570
+ /* FAIL-CLOSED, the choice projects-feed.js makes for the same question: a row
1571
+ whose render-type this page does not recognise is NOT DRAWN. The risk is a
1572
+ fourth state shipped by a newer server and read by an older page — drawing it
1573
+ as the default would publish, as a public planet, a project that chose not to
1574
+ be one. Applied to CATALOG rows only: the viewer's own provisioning rows never
1575
+ came from the feed and carry no `orb`. */
1576
+ function drawableCatalogRow(p) {
1577
+ return Object.prototype.hasOwnProperty.call(ORB_STATE, String((p && p.orb) || ''));
1578
+ }
1579
+ /* Is this row anonymous on the map? The one predicate the sky and the table both
1580
+ ask, so neither can answer it differently (ADR 0182 D2a). */
1581
+ function isBlackHole(p) { return String((p && p.orb) || '') === 'black-hole'; }
1582
+ /* What a black hole is allowed to say. Deliberately takes no project argument:
1583
+ there is no identifying value in scope to leak by accident, now or when the
1584
+ feed grows a column. explore.js's anonymousCard() makes the same choice. */
1585
+ function anonymousName() { return 'A project'; }
1586
+ function anonymousBlurb() {
1587
+ return 'Something is here. Its name, and everything else about it, stay with its owner.';
1588
+ }
1556
1589
  var catalog = [];
1557
1590
  var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
1558
1591
  var ownedInstances = []; /* the viewer's OWN provisioning rows (never torn_down) */
@@ -1696,6 +1729,27 @@ summary{min-height:24px;padding:3px 0;}
1696
1729
  (spot ? list.concat(dimmed) : list).forEach(function (p, i) {
1697
1730
  var slot = ORB_SLOTS[i % ORB_SLOTS.length];
1698
1731
  var isDim = spot && i >= list.length;
1732
+ /* A BLACK HOLE IS DRAWN FROM NOTHING BUT ITS EXISTENCE (ADR 0182 D2a).
1733
+ A <span>, not an <a>: there is nowhere to go. No name, no address, no
1734
+ badge, no liveness chip — and no planet artwork, because the shape /
1735
+ material walk is an identity THIS PAGE assigns, and an identity assigned
1736
+ to rows that are supposed to be interchangeable is exactly what lets an
1737
+ observer follow one of them from yesterday's feed into today's. The
1738
+ server redacted the row; this is the page declining to re-identify it. */
1739
+ if (isBlackHole(p)) {
1740
+ var bh = document.createElement('span');
1741
+ bh.className = 'worldOrb blackHole' + (spot ? (isDim ? ' dim' : ' match') : '');
1742
+ bh.setAttribute('data-generated', '');
1743
+ bh.setAttribute('data-orb', 'black-hole');
1744
+ if (isDim) bh.setAttribute('aria-hidden', 'true');
1745
+ /* no --acc: the accent is per-project identity too, so a black hole takes
1746
+ the page's own and is the same colour as every other black hole */
1747
+ bh.style.cssText = '--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
1748
+ bh.title = anonymousBlurb();
1749
+ bh.innerHTML = '<span class="disc"></span><span class="nm">' + esc(anonymousName()) + '</span>';
1750
+ layer.appendChild(bh);
1751
+ return;
1752
+ }
1699
1753
  var a = document.createElement('a');
1700
1754
  a.className = 'worldOrb' + (spot ? (isDim ? ' dim' : ' match') : '');
1701
1755
  /* a dimmed orb is BACKDROP: at .25 opacity its label cannot carry
@@ -1705,6 +1759,12 @@ summary{min-height:24px;padding:3px 0;}
1705
1759
  a.setAttribute('data-generated', '');
1706
1760
  var rel = relOf(p);
1707
1761
  a.setAttribute('data-rel', rel);
1762
+ /* the door the project chose, as the feed derived it. An IDENTIFIED state —
1763
+ public or private — is on the map under its own name; what the state
1764
+ changes is how it is drawn and who may walk in, never whether it is named.
1765
+ An owned-only entry never came from the feed, so it claims no state. */
1766
+ var orbState = drawableCatalogRow(p) ? String(p.orb) : '';
1767
+ if (orbState) a.setAttribute('data-orb', orbState);
1708
1768
  /* the planet: shape, material and style as data attributes (shape[-material]-style;
1709
1769
  no material = chrome), cosmos.css does the rest */
1710
1770
  var tpl = templateOf(p), tplParts = tpl.split('-');
@@ -1731,7 +1791,10 @@ summary{min-height:24px;padding:3px 0;}
1731
1791
  if (href) { a.target = '_blank'; a.rel = 'noopener'; }
1732
1792
  }
1733
1793
  a.setAttribute('data-origin', String(p.origin || ''));
1734
- a.title = p.tagline || p.name || '';
1794
+ /* the state in words as well as in the drawing — a visitor should not have
1795
+ to have learned the shapes to tell a planet from a star (the explore
1796
+ page's rule: the orb is decorative, the plain word carries the meaning) */
1797
+ a.title = [p.tagline || p.name || '', ORB_STATE[orbState] || ''].filter(Boolean).join(' · ');
1735
1798
  a.style.cssText = '--acc:' + accentOf(p) +
1736
1799
  ';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
1737
1800
  a.innerHTML =
@@ -1772,7 +1835,21 @@ summary{min-height:24px;padding:3px 0;}
1772
1835
  not a peer in the catalog's sort order. */
1773
1836
  body.querySelectorAll('li[data-generated]').forEach(function (n) { n.remove(); });
1774
1837
  body.insertAdjacentHTML('beforeend', list.map(function (p) {
1838
+ /* The table is a PEER VIEW of the map, not a back door around D2a: a black
1839
+ hole says exactly as little here as it does in the sky. No name, no
1840
+ address, no facts, no doors — and no liveness chip, because a chip is a
1841
+ claim about a specific project. The row exists so the count is honest;
1842
+ that is the whole of what it publishes. */
1843
+ if (isBlackHole(p)) {
1844
+ return '<li class="tblRow anon" data-generated data-orb="black-hole">' +
1845
+ '<span class="worldOrb blackHole" data-orb="black-hole" style="--d:64px" aria-hidden="true">' +
1846
+ '<span class="disc"></span></span>' +
1847
+ '<div class="tWho"><span class="ptitle">' + esc(anonymousName()) + '</span></div>' +
1848
+ '<p class="ptag">' + esc(anonymousBlurb()) + '</p>' +
1849
+ '</li>';
1850
+ }
1775
1851
  var rel = relOf(p);
1852
+ var orbState = drawableCatalogRow(p) ? String(p.orb) : '';
1776
1853
  var pill = rel === 'owner' ? '<span class="pill">yours</span>'
1777
1854
  : rel === 'builder' ? '<span class="pill">you build here</span>' : '';
1778
1855
  /* the project's own planet, the object the sky and the Your-projects row
@@ -1782,6 +1859,7 @@ summary{min-height:24px;padding:3px 0;}
1782
1859
  var planet = '<span class="worldOrb"' + planetAttrs({ tpl: templateOf(p) }) +
1783
1860
  ' style="--acc:' + accentOf(p) + ';--d:64px" aria-hidden="true"><span class="disc"></span></span>';
1784
1861
  return '<li class="tblRow" data-generated ' + (p._inst ? 'data-manage="' + esc(String(p._inst.id)) + '" ' : '') +
1862
+ (orbState ? 'data-orb="' + esc(orbState) + '" ' : '') +
1785
1863
  'data-origin="' + esc(String(p.origin || '')) + '">' + planet +
1786
1864
  '<div class="tWho">' + (p._inst
1787
1865
  ? '<a class="ptitle" data-manage="' + esc(String(p._inst.id)) + '" href="?view=manage&id=' + esc(String(p._inst.id)) + '">' + esc(p.name) + '</a>'
@@ -1817,7 +1895,13 @@ summary{min-height:24px;padding:3px 0;}
1817
1895
  entries. The sky, the table, the search box and the count read the same
1818
1896
  list and can never disagree about what exists. */
1819
1897
  var catalogHosts = {};
1820
- catalog.forEach(function (p) {
1898
+ /* FAIL-CLOSED FIRST, before anything else reads these rows (task 1002328): a
1899
+ render-type this page does not recognise is not drawn, not counted, and —
1900
+ the reason it is filtered HERE rather than inside the renderers — not allowed
1901
+ to claim a host either, or it would suppress the viewer's own entry for that
1902
+ same project in the loop below. */
1903
+ var drawnCatalog = catalog.filter(drawableCatalogRow);
1904
+ drawnCatalog.forEach(function (p) {
1821
1905
  var h = hostOf(p.origin);
1822
1906
  catalogHosts[h] = 1;
1823
1907
  p._inst = ownedByHost[h] || null;
@@ -1825,7 +1909,7 @@ summary{min-height:24px;padding:3px 0;}
1825
1909
  create — their own instance row is a truthful source, never a guess */
1826
1910
  if (p.type == null && p._inst && p._inst.type) p.type = p._inst.type;
1827
1911
  });
1828
- var browse = catalog.slice();
1912
+ var browse = drawnCatalog.slice();
1829
1913
  ownedInstances.forEach(function (inst) {
1830
1914
  var h = inst.domain ? hostOf(inst.domain) : '';
1831
1915
  if (h && catalogHosts[h]) return; /* annotated onto its catalog row above */
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.584",
3
+ "version": "1.19.585",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.584",
9
+ "version": "1.19.585",
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.584",
3
+ "version": "1.19.585",
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",
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.584'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.585'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,255 @@
1
+ // tests/hub_map_orb_states.mjs — the hub map draws the VISIBILITY render-type,
2
+ // EXECUTED (BV1.R09, task 1002328; ADR 0182 D1/D2a, criterion proj-visibility-states).
3
+ //
4
+ // R06 (task 1002325) made the feed answer what each project draws as — `orb` is
5
+ // planet / star / black-hole, derived server-side from the join door, with stealth
6
+ // rows redacted before they leave the server and dark matter excluded in SQL. R07
7
+ // (task 1002326) dogfooded it on the Explore page. This file pins the surface a
8
+ // visitor actually lands on: the hub map at modules/public-landing/public/projects.html,
9
+ // in both its views — the sky and the table.
10
+ //
11
+ // The original R09 scoped this to a separate-repo static gate site; that gate was
12
+ // retired (src/platform-server.js: the apex 302 into /gate/ "is gone with the gate
13
+ // itself"), and the orb sky became a view inside the hub. So the criterion's last
14
+ // clause lands here.
15
+ //
16
+ // Harness: the tests/project_door_ui.mjs pattern — there is no jsdom in this repo,
17
+ // so the real region is sliced out of the page verbatim, run in a vm under a narrow
18
+ // DOM stub, and the markup it writes is parsed back. Both slice boundaries are
19
+ // asserted so a rename can never leave this file executing an empty string.
20
+ //
21
+ // What it proves, and why each matters:
22
+ // • the three states draw as three different things — a private project stopped
23
+ // being indistinguishable from a public one
24
+ // • a black hole is drawn from NOTHING but its existence: no name, no address,
25
+ // no per-project artwork, and nothing to click. The server already redacted the
26
+ // row; this asserts the page cannot re-identify it from what it kept
27
+ // • an unrecognised render-type is DROPPED, not drawn — the fail-closed choice
28
+ // projects-feed.js makes for the same question, so a fourth state added later
29
+ // can never inherit the public planet
30
+ // • the mapping is never re-derived client-side from `state` — one copy, server-side
31
+ // • the search box cannot surface a black hole
32
+ // • the viewer's OWN provisioning rows, which carry no feed `orb`, still draw —
33
+ // fail-closed must not delete the owner's own sky
34
+ //
35
+ // Run: node --test --test-reporter=tap tests/hub_map_orb_states.mjs
36
+
37
+ import assert from 'node:assert/strict';
38
+ import { test } from 'node:test';
39
+ import fs from 'node:fs';
40
+ import path from 'node:path';
41
+ import vm from 'node:vm';
42
+ import { fileURLToPath } from 'node:url';
43
+
44
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
45
+ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
46
+ const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8');
47
+ const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
48
+
49
+ // ── the region under test, sliced verbatim ───────────────────────────────────
50
+ const START = SCRIPT.indexOf('var ORB_SLOTS = [');
51
+ const END = SCRIPT.indexOf('function applyMapFilter(');
52
+ assert.ok(START > 0, 'the map-render region must start at ORB_SLOTS');
53
+ assert.ok(END > START, 'the map-render region must end before applyMapFilter');
54
+ const REGION = SCRIPT.slice(START, END);
55
+ for (const fn of ['var ORB_STATE = {', 'function drawableCatalogRow(', 'function renderOrbs(', 'function renderRows(']) {
56
+ assert.ok(REGION.includes(fn), `the region must carry ${fn}`);
57
+ }
58
+
59
+ // planetAttrs() lives further down the page (it is shared with the manage cards),
60
+ // so it is sliced in rather than re-typed — a hand-copied stub would drift from the
61
+ // thing whose output this file asserts about.
62
+ const PA_START = SCRIPT.indexOf('function planetAttrs(');
63
+ const PA_END = SCRIPT.indexOf('var PLANET_SHAPE_WORDS');
64
+ assert.ok(PA_START > 0 && PA_END > PA_START, 'planetAttrs must still be sliceable');
65
+ const PLANET_ATTRS = SCRIPT.slice(PA_START, PA_END);
66
+
67
+ // ── the DOM stub ─────────────────────────────────────────────────────────────
68
+ // Narrow on purpose: it supports exactly what the region touches, so it stays
69
+ // honest about what it can prove.
70
+
71
+ function makeNode(tag) {
72
+ return {
73
+ tagName: String(tag).toUpperCase(),
74
+ _attrs: {},
75
+ _children: [],
76
+ className: '',
77
+ tabIndex: 0,
78
+ style: { cssText: '' },
79
+ innerHTML: '',
80
+ setAttribute(k, v) { this._attrs[k] = String(v); },
81
+ getAttribute(k) { return Object.prototype.hasOwnProperty.call(this._attrs, k) ? this._attrs[k] : null; },
82
+ hasAttribute(k) { return Object.prototype.hasOwnProperty.call(this._attrs, k); },
83
+ appendChild(n) { this._children.push(n); return n; },
84
+ querySelectorAll() { return []; },
85
+ };
86
+ }
87
+
88
+ function boot() {
89
+ const sky = makeNode('div');
90
+ let rowsHtml = '';
91
+ const rows = makeNode('ul');
92
+ rows.insertAdjacentHTML = (_where, html) => { rowsHtml += html; };
93
+
94
+ const els = { skyCam: sky, mapRows: rows };
95
+ const sandbox = {
96
+ // safeHttpUrl() (inside the region) scheme-checks through the URL parser — a vm
97
+ // context has no globals of its own, so it is handed the real one
98
+ URL,
99
+ document: { createElement: makeNode, getElementById: (id) => els[id] || null },
100
+ $: (id) => els[id] || null,
101
+ esc: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
102
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
103
+ productName: 'Cloud Bongos',
104
+ // the surrounding page's own helpers, narrowed to what the region reads back
105
+ chipFor: () => '<span class="chip"></span>',
106
+ hallGlyphHtml: () => '',
107
+ shippedFigure: () => '',
108
+ listedDate: (iso) => String(iso),
109
+ };
110
+ vm.createContext(sandbox);
111
+ vm.runInContext(PLANET_ATTRS + '\n' + REGION, sandbox);
112
+ return {
113
+ sandbox,
114
+ sky,
115
+ rowsHtml: () => rowsHtml,
116
+ orbs: () => sky._children,
117
+ };
118
+ }
119
+
120
+ // A feed row as the server actually shapes it (projects-feed.js): `state` and `orb`
121
+ // on every row, and a stealth row nulled down to those two keys.
122
+ const PUBLIC_ROW = {
123
+ origin: 'https://demo.example.com', name: 'Demo', tagline: 'a demo project',
124
+ art_url: null, created_at: '2026-01-01T00:00:00.000Z', builders: 3, tags: [],
125
+ type: null, state: 'public', orb: 'planet',
126
+ };
127
+ const PRIVATE_ROW = {
128
+ origin: 'https://circles.example.com', name: 'Circles', tagline: 'invite only',
129
+ art_url: null, created_at: '2026-01-02T00:00:00.000Z', builders: 2, tags: [],
130
+ type: null, state: 'private', orb: 'star',
131
+ };
132
+ const STEALTH_ROW = {
133
+ origin: null, name: null, tagline: null, art_url: null, created_at: null,
134
+ builders: null, tags: null, type: null, state: 'stealth', orb: 'black-hole',
135
+ };
136
+
137
+ test('the three states draw as three different orbs', () => {
138
+ const h = boot();
139
+ h.sandbox.renderOrbs([PUBLIC_ROW, PRIVATE_ROW, STEALTH_ROW], []);
140
+ const drawn = h.orbs();
141
+ assert.equal(drawn.length, 3, 'every drawable row reaches the sky');
142
+ assert.deepEqual(drawn.map((n) => n.getAttribute('data-orb')),
143
+ ['planet', 'star', 'black-hole'],
144
+ 'each orb carries the render-type the feed derived');
145
+ });
146
+
147
+ test('a public project keeps its planet: name, address and artwork', () => {
148
+ const h = boot();
149
+ h.sandbox.renderOrbs([PUBLIC_ROW], []);
150
+ const [orb] = h.orbs();
151
+ assert.equal(orb.tagName, 'A', 'a public project is a real link');
152
+ assert.equal(orb.href, 'https://demo.example.com/');
153
+ assert.match(orb.innerHTML, /Demo/, 'it is named');
154
+ assert.ok(orb.getAttribute('data-template'), 'it carries its planet artwork');
155
+ });
156
+
157
+ test('a private project is a star, and is still itself', () => {
158
+ const h = boot();
159
+ h.sandbox.renderOrbs([PRIVATE_ROW], []);
160
+ const [orb] = h.orbs();
161
+ assert.equal(orb.getAttribute('data-orb'), 'star');
162
+ // identified, per ADR 0182 D1: private is on the map UNDER ITS NAME — what
163
+ // changes is the door, not the anonymity
164
+ assert.match(orb.innerHTML, /Circles/);
165
+ assert.equal(orb.href, 'https://circles.example.com/');
166
+ assert.ok(orb.getAttribute('data-template'), 'it keeps its planet artwork');
167
+ });
168
+
169
+ test('a black hole is existence without identity — nothing to read, nothing to click', () => {
170
+ const h = boot();
171
+ h.sandbox.renderOrbs([STEALTH_ROW], []);
172
+ const [orb] = h.orbs();
173
+ assert.equal(orb.getAttribute('data-orb'), 'black-hole');
174
+ assert.notEqual(orb.tagName, 'A', 'a black hole is not a link — there is nowhere to go');
175
+ assert.ok(!orb.href, 'and it carries no address');
176
+ assert.equal(orb.getAttribute('data-origin'), null, 'not even as a data attribute');
177
+ // no per-project artwork: the shape/material walk is an IDENTITY the page
178
+ // assigns, and assigning one to a black hole would make interchangeable rows
179
+ // individually recognisable across snapshots
180
+ for (const a of ['data-template', 'data-shape', 'data-material', 'data-style']) {
181
+ assert.equal(orb.getAttribute(a), null, `a black hole must not carry ${a}`);
182
+ }
183
+ assert.doesNotMatch(orb.innerHTML, /class="badge"|class="chip"/, 'no badge, no liveness chip');
184
+ });
185
+
186
+ test('an unrecognised render-type is dropped, not drawn (fail-closed)', () => {
187
+ const { sandbox } = boot();
188
+ for (const orb of ['planet', 'star', 'black-hole']) {
189
+ assert.equal(sandbox.drawableCatalogRow({ orb }), true, `${orb} is drawable`);
190
+ }
191
+ // the shape of the risk: a fourth state shipped by a newer server, read by an
192
+ // older page. It must not inherit the public planet.
193
+ for (const orb of ['wormhole', 'nebula', '', null, undefined]) {
194
+ assert.equal(sandbox.drawableCatalogRow({ orb }), false,
195
+ `an unknown render-type (${String(orb)}) is not drawn`);
196
+ }
197
+ assert.equal(sandbox.drawableCatalogRow({}), false, 'a row with no render-type at all is not drawn');
198
+ });
199
+
200
+ test('the drop is wired into the browse list, not left as an orphan predicate', () => {
201
+ // behaviour above proves the predicate; this proves the page actually asks it,
202
+ // and asks it of the CATALOG (the viewer's own provisioning rows carry no feed
203
+ // `orb` and must survive)
204
+ assert.match(SCRIPT, /catalog\.filter\(drawableCatalogRow\)/,
205
+ 'applyMapFilter must filter the catalog through drawableCatalogRow');
206
+ });
207
+
208
+ test("the viewer's own provisioning rows still draw — fail-closed must not empty the owner's sky", () => {
209
+ const h = boot();
210
+ // the synthetic row applyMapFilter pushes for an owned instance the catalog
211
+ // does not carry: no feed `orb`, because it never came from the feed
212
+ const owned = {
213
+ name: 'my-project', tagline: 'my-project.example.com',
214
+ origin: 'https://my-project.example.com', builders: null, tags: null, type: null,
215
+ _inst: { id: '42', slug: 'my-project', domain: 'my-project.example.com' },
216
+ };
217
+ h.sandbox.renderOrbs([owned], []);
218
+ const [orb] = h.orbs();
219
+ assert.equal(h.orbs().length, 1, 'an owned-only entry is drawn');
220
+ assert.equal(orb.getAttribute('data-orb'), null, 'and is not labelled with a state it never claimed');
221
+ assert.ok(orb.getAttribute('data-template'), 'it keeps its slug-walked planet');
222
+ });
223
+
224
+ test('the state→orb mapping is read from the feed, never re-derived here', () => {
225
+ // projects-feed.js owns ORB_BY_STATE. A second copy on the client is a copy
226
+ // that drifts — the explicit rule R07 already follows in explore.js.
227
+ assert.doesNotMatch(REGION, /['"]public['"]\s*:\s*['"]planet['"]/,
228
+ 'the page must not carry its own state→orb table');
229
+ assert.match(REGION, /p\.orb|\bORB_STATE\b/, 'it reads the feed’s own orb field');
230
+ });
231
+
232
+ test('a black hole cannot be surfaced by the search box', () => {
233
+ const { sandbox } = boot();
234
+ // it publishes no text, so there is nothing for a query to match — asserted
235
+ // rather than assumed, because a future row that gained a searchable field
236
+ // would silently make stealth projects probeable by guessing names
237
+ for (const q of ['a', 'project', 'demo', 'circles']) {
238
+ assert.equal(sandbox.matchesMapQuery(STEALTH_ROW, q), false,
239
+ `a query (${q}) must never match a black hole`);
240
+ }
241
+ });
242
+
243
+ test('the table view says the same three things as the sky', () => {
244
+ const h = boot();
245
+ h.sandbox.renderRows([PUBLIC_ROW, PRIVATE_ROW, STEALTH_ROW]);
246
+ const html = h.rowsHtml();
247
+ for (const orb of ['planet', 'star', 'black-hole']) {
248
+ assert.ok(html.includes('data-orb="' + orb + '"'), `the table marks its ${orb} row`);
249
+ }
250
+ // the anonymous row is anonymous HERE too: the table is a peer view of the map,
251
+ // not a back door around D2a
252
+ const anon = html.slice(html.indexOf('data-orb="black-hole"'));
253
+ assert.doesNotMatch(anon, /class="rowGo"/, 'a black hole has no "view" door');
254
+ assert.doesNotMatch(anon, /data-template=/, 'and no per-project artwork');
255
+ });