@bongos/core 1.19.530 → 1.19.531
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 +20 -15
- package/docs/copy-inventory.md +10 -8
- package/docs/copy-registry.json +41 -23
- package/docs/module-api-changelog.md +2 -0
- package/modules/status-ui/public/explore.html +56 -3
- package/modules/status-ui/public/explore.js +71 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/explore_visible_states.mjs +285 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.531",
|
|
6
|
+
"core_contract": "1.19.531",
|
|
7
|
+
"source_commit": "9044e13dacbf2160d02eef08915ffa74dbc669be",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-04T13:
|
|
9
|
+
"built_at": "2026-09-04T13:41:13.821Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 435,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1979,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2438,
|
|
20
|
+
"tree_sha256": "0d93f09e833a982f219a54f124fe11808be718651cd93d5258119ef9e50a7954",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1767,12 +1767,12 @@
|
|
|
1767
1767
|
{
|
|
1768
1768
|
"path": "docs/copy-inventory.md",
|
|
1769
1769
|
"mode": "0000644",
|
|
1770
|
-
"sha256": "
|
|
1770
|
+
"sha256": "729633c41036ec0026d3ef1ac787f68cb6fb7ab77e3eb578f855b5ec2d63280f"
|
|
1771
1771
|
},
|
|
1772
1772
|
{
|
|
1773
1773
|
"path": "docs/copy-registry.json",
|
|
1774
1774
|
"mode": "0000644",
|
|
1775
|
-
"sha256": "
|
|
1775
|
+
"sha256": "344a451940b630b1f6db2f97acb45eb387616311675b243dcc4381dc604ef77d"
|
|
1776
1776
|
},
|
|
1777
1777
|
{
|
|
1778
1778
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2617,7 +2617,7 @@
|
|
|
2617
2617
|
{
|
|
2618
2618
|
"path": "docs/module-api-changelog.md",
|
|
2619
2619
|
"mode": "0000644",
|
|
2620
|
-
"sha256": "
|
|
2620
|
+
"sha256": "fa1785c5b0009ea7431a5fcf3fd13dcb5b65563c481e8d417d62c2eea6050af6"
|
|
2621
2621
|
},
|
|
2622
2622
|
{
|
|
2623
2623
|
"path": "docs/modules-contract.md",
|
|
@@ -6887,12 +6887,12 @@
|
|
|
6887
6887
|
{
|
|
6888
6888
|
"path": "modules/status-ui/public/explore.html",
|
|
6889
6889
|
"mode": "0000644",
|
|
6890
|
-
"sha256": "
|
|
6890
|
+
"sha256": "7bdde42ca7bc23acaabf1966c928c6d0816e29677c243f82175a0e20bd995c07"
|
|
6891
6891
|
},
|
|
6892
6892
|
{
|
|
6893
6893
|
"path": "modules/status-ui/public/explore.js",
|
|
6894
6894
|
"mode": "0000644",
|
|
6895
|
-
"sha256": "
|
|
6895
|
+
"sha256": "a6311740e338d81cdeb9d5ce38733bd1dd4d978a6a5399c4f103017daf6f5cf2"
|
|
6896
6896
|
},
|
|
6897
6897
|
{
|
|
6898
6898
|
"path": "modules/status-ui/public/index.html",
|
|
@@ -7332,12 +7332,12 @@
|
|
|
7332
7332
|
{
|
|
7333
7333
|
"path": "package-lock.json",
|
|
7334
7334
|
"mode": "0000644",
|
|
7335
|
-
"sha256": "
|
|
7335
|
+
"sha256": "03ecbc56365c6b70ada0bdfd2ca666962f3ed8edcf95ef52acf93f2d0b82de51"
|
|
7336
7336
|
},
|
|
7337
7337
|
{
|
|
7338
7338
|
"path": "package.json",
|
|
7339
7339
|
"mode": "0000644",
|
|
7340
|
-
"sha256": "
|
|
7340
|
+
"sha256": "0eaf9886acaf585da2ecc97408dcb6f879a801ff20cead40a20bac221e8ea32f"
|
|
7341
7341
|
},
|
|
7342
7342
|
{
|
|
7343
7343
|
"path": "public-docs/index.html",
|
|
@@ -9017,7 +9017,7 @@
|
|
|
9017
9017
|
{
|
|
9018
9018
|
"path": "src/module-api.js",
|
|
9019
9019
|
"mode": "0000644",
|
|
9020
|
-
"sha256": "
|
|
9020
|
+
"sha256": "a534c8a215f1f744707ef9835905ac2850c991757a76545f77a191f8c3827e7b"
|
|
9021
9021
|
},
|
|
9022
9022
|
{
|
|
9023
9023
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9934,6 +9934,11 @@
|
|
|
9934
9934
|
"mode": "0000644",
|
|
9935
9935
|
"sha256": "d6a78357533fdeea3bc33fe97cfcd27b1d04747c600d0a7800ddfca5199dbb1f"
|
|
9936
9936
|
},
|
|
9937
|
+
{
|
|
9938
|
+
"path": "tests/explore_visible_states.mjs",
|
|
9939
|
+
"mode": "0000644",
|
|
9940
|
+
"sha256": "015588572bf3c75c1af00b3bb591f133dff7e604be1f9b40272f1f963b24bf71"
|
|
9941
|
+
},
|
|
9937
9942
|
{
|
|
9938
9943
|
"path": "tests/extract_json.mjs",
|
|
9939
9944
|
"mode": "0000644",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1647 strings** across 3 surfaces · 1429 distinct pieces of copy · 1647 certain, 0 heuristic · 154 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| `builders-hall` | `modules/hall-ui/public` | 73 | 1187 |
|
|
14
|
-
| `status-dashboard` | `modules/status-ui/public` | 4 |
|
|
14
|
+
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 360 |
|
|
16
16
|
|
|
17
17
|
## Confidence, and why it is on every row
|
|
@@ -1270,7 +1270,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1270
1270
|
| `7e1bb38d4777` | certain | 🔒 Request to join | `modules/hall-ui/public/goals-render.js:861` |
|
|
1271
1271
|
| `cce9c199e874` | certain | 🛡 Archon-mediated | `modules/hall-ui/public/goals-render.js:803` |
|
|
1272
1272
|
|
|
1273
|
-
## `status-dashboard` —
|
|
1273
|
+
## `status-dashboard` — 100 strings
|
|
1274
1274
|
|
|
1275
1275
|
| id | conf | text | where |
|
|
1276
1276
|
|---|---|---|---|
|
|
@@ -1278,6 +1278,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1278
1278
|
| `c5fbe5ea4335` | certain | , a third-party logistics house in Cleveland, Ohio. | `modules/status-ui/public/index.html:355` |
|
|
1279
1279
|
| `2045078fb115` | certain | 90 days ago | `modules/status-ui/public/status.js:477` |
|
|
1280
1280
|
| `e4848e2d9386` | certain | A new portrait is set down each time the city's stones are reshaped. Scroll to see how {{worldName}} has grown | `modules/status-ui/public/index.html:344` |
|
|
1281
|
+
| `390faeaffb47` | certain | A project | `modules/status-ui/public/explore.js:71` |
|
|
1281
1282
|
| `f4a664041da5` | certain | A shipping container has washed ashore upon the coast south of Athens. About it a new civilization rises. Set | `modules/status-ui/public/index.html:126` |
|
|
1282
1283
|
| `623d6b7ae6b7` | certain | A true-to-life portrait of {{worldName}} as she stands today, rendered from the city's own stones. | `modules/status-ui/public/index.html:332` |
|
|
1283
1284
|
| `155d14ab692a` | certain | All categories | `modules/status-ui/public/index.html:280` |
|
|
@@ -1290,11 +1291,10 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1290
1291
|
| `c8959bdc0e59` | certain | Breakdown filtered to {…}: {…} of {…} ({…}% of all spend). | `modules/status-ui/public/status.js:659` |
|
|
1291
1292
|
| `1fe8937399f5` | certain | By order of the Senate | `modules/status-ui/public/index.html:354` |
|
|
1292
1293
|
| `21f644463d57` | certain | Citizens who have set the most stone. | `modules/status-ui/public/index.html:201` |
|
|
1293
|
-
| `87af953708b3` | certain | Couldn't load projects right now. | `modules/status-ui/public/explore.html:
|
|
1294
|
+
| `87af953708b3` | certain | Couldn't load projects right now. | `modules/status-ui/public/explore.html:109` |
|
|
1294
1295
|
| `3aa071ef1142` | certain | Cross into the city → | `modules/status-ui/public/index.html:128` |
|
|
1295
|
-
| `
|
|
1296
|
-
| `
|
|
1297
|
-
| `4db99e47bc50` | certain | Featured projects built on the platform. | `modules/status-ui/public/explore.html:52` |
|
|
1296
|
+
| `f151456aedc1` | certain | Explore — projects on the map | `modules/status-ui/public/explore.html:6` |
|
|
1297
|
+
| `f151456aedc1` | certain | Explore — projects on the map | `modules/status-ui/public/explore.html:6` |
|
|
1298
1298
|
| `14d6b38bc659` | certain | Filter inscriptions by date range | `modules/status-ui/public/index.html:309` |
|
|
1299
1299
|
| `194f07002413` | certain | Filter spend by category | `modules/status-ui/public/index.html:279` |
|
|
1300
1300
|
| `183993d780be` | certain | Filter spend by source | `modules/status-ui/public/index.html:273` |
|
|
@@ -1315,15 +1315,17 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1315
1315
|
| `37b171a6195a` | certain | No architects yet — be the first to lay a stone. | `modules/status-ui/public/index.html:205` |
|
|
1316
1316
|
| `7816fbe0add8` | certain | No builders yet — be the first. | `modules/status-ui/public/status.js:1117` |
|
|
1317
1317
|
| `0b097f051d6f` | certain | No endeavors underway just now. | `modules/status-ui/public/status.js:516` |
|
|
1318
|
-
| `
|
|
1318
|
+
| `ed9b06f79234` | certain | No projects on the map yet — check back soon. | `modules/status-ui/public/explore.html:108` |
|
|
1319
1319
|
| `4b1a11bfdf10` | certain | No works yet inscribed in this window. | `modules/status-ui/public/status.js:947` |
|
|
1320
1320
|
| `7a8afe641683` | certain | No works yet inscribed. | `modules/status-ui/public/index.html:318` |
|
|
1321
1321
|
| `3e8205a358d9` | certain | No {…} yet attributed to a hand. | `modules/status-ui/public/status.js:746` |
|
|
1322
1322
|
| `c9efa62c36e7` | certain | No {…} yet spent. | `modules/status-ui/public/status.js:684` |
|
|
1323
1323
|
| `2df94faddc8c` | certain | Not enough shipped works yet to draw the curve. | `modules/status-ui/public/index.html:224` |
|
|
1324
1324
|
| `9d8692476139` | certain | Previous page | `modules/status-ui/public/index.html:321` |
|
|
1325
|
+
| `f1b103f87df2` | certain | Projects on the map, and how each one stands on it. | `modules/status-ui/public/explore.html:105` |
|
|
1325
1326
|
| `48473324e839` | certain | Sections of the ledger | `modules/status-ui/public/index.html:144` |
|
|
1326
1327
|
| `81cb441627b8` | certain | Show history of earlier portraits | `modules/status-ui/public/index.html:342` |
|
|
1328
|
+
| `abd2ce869517` | certain | Something is here. Its name, and everything else about it, stay with its owner. | `modules/status-ui/public/explore.js:74` |
|
|
1327
1329
|
| `0cc023b5f470` | certain | Split by hand — {{currency}} spent by each builder at the workshop. | `modules/status-ui/public/index.html:295` |
|
|
1328
1330
|
| `fddc39dcf1eb` | certain | Tablet being consulted… | `modules/status-ui/public/index.html:130` |
|
|
1329
1331
|
| `8f86e1c9ad44` | certain | Text size | `modules/status-ui/public/index.html:18` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1647,
|
|
7
|
+
"unique_ids": 1429,
|
|
8
|
+
"certain": 1647,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 154
|
|
11
11
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
20
20
|
"files": 4,
|
|
21
|
-
"strings":
|
|
21
|
+
"strings": 100
|
|
22
22
|
},
|
|
23
23
|
"landing": {
|
|
24
24
|
"at": "modules/public-landing/public",
|
|
@@ -693,9 +693,9 @@
|
|
|
693
693
|
]
|
|
694
694
|
},
|
|
695
695
|
{
|
|
696
|
-
"id": "
|
|
696
|
+
"id": "f151456aedc1",
|
|
697
697
|
"surface": "status-dashboard",
|
|
698
|
-
"text": "Explore —
|
|
698
|
+
"text": "Explore — projects on the map",
|
|
699
699
|
"occurrences": 2,
|
|
700
700
|
"at": [
|
|
701
701
|
"modules/status-ui/public/explore.html:6",
|
|
@@ -15603,6 +15603,15 @@
|
|
|
15603
15603
|
"origin": "html-text",
|
|
15604
15604
|
"confidence": "certain"
|
|
15605
15605
|
},
|
|
15606
|
+
{
|
|
15607
|
+
"id": "390faeaffb47",
|
|
15608
|
+
"surface": "status-dashboard",
|
|
15609
|
+
"text": "A project",
|
|
15610
|
+
"file": "modules/status-ui/public/explore.js",
|
|
15611
|
+
"line": 71,
|
|
15612
|
+
"origin": "js-text-assign",
|
|
15613
|
+
"confidence": "certain"
|
|
15614
|
+
},
|
|
15606
15615
|
{
|
|
15607
15616
|
"id": "f4a664041da5",
|
|
15608
15617
|
"surface": "status-dashboard",
|
|
@@ -15716,7 +15725,7 @@
|
|
|
15716
15725
|
"surface": "status-dashboard",
|
|
15717
15726
|
"text": "Couldn't load projects right now.",
|
|
15718
15727
|
"file": "modules/status-ui/public/explore.html",
|
|
15719
|
-
"line":
|
|
15728
|
+
"line": 109,
|
|
15720
15729
|
"origin": "html-text",
|
|
15721
15730
|
"confidence": "certain"
|
|
15722
15731
|
},
|
|
@@ -15730,32 +15739,23 @@
|
|
|
15730
15739
|
"confidence": "certain"
|
|
15731
15740
|
},
|
|
15732
15741
|
{
|
|
15733
|
-
"id": "
|
|
15742
|
+
"id": "f151456aedc1",
|
|
15734
15743
|
"surface": "status-dashboard",
|
|
15735
|
-
"text": "Explore —
|
|
15744
|
+
"text": "Explore — projects on the map",
|
|
15736
15745
|
"file": "modules/status-ui/public/explore.html",
|
|
15737
15746
|
"line": 6,
|
|
15738
15747
|
"origin": "html-text",
|
|
15739
15748
|
"confidence": "certain"
|
|
15740
15749
|
},
|
|
15741
15750
|
{
|
|
15742
|
-
"id": "
|
|
15751
|
+
"id": "f151456aedc1",
|
|
15743
15752
|
"surface": "status-dashboard",
|
|
15744
|
-
"text": "Explore —
|
|
15753
|
+
"text": "Explore — projects on the map",
|
|
15745
15754
|
"file": "modules/status-ui/public/explore.html",
|
|
15746
15755
|
"line": 6,
|
|
15747
15756
|
"origin": "html-title",
|
|
15748
15757
|
"confidence": "certain"
|
|
15749
15758
|
},
|
|
15750
|
-
{
|
|
15751
|
-
"id": "4db99e47bc50",
|
|
15752
|
-
"surface": "status-dashboard",
|
|
15753
|
-
"text": "Featured projects built on the platform.",
|
|
15754
|
-
"file": "modules/status-ui/public/explore.html",
|
|
15755
|
-
"line": 52,
|
|
15756
|
-
"origin": "html-text",
|
|
15757
|
-
"confidence": "certain"
|
|
15758
|
-
},
|
|
15759
15759
|
{
|
|
15760
15760
|
"id": "14d6b38bc659",
|
|
15761
15761
|
"surface": "status-dashboard",
|
|
@@ -15937,11 +15937,11 @@
|
|
|
15937
15937
|
"confidence": "certain"
|
|
15938
15938
|
},
|
|
15939
15939
|
{
|
|
15940
|
-
"id": "
|
|
15940
|
+
"id": "ed9b06f79234",
|
|
15941
15941
|
"surface": "status-dashboard",
|
|
15942
|
-
"text": "No
|
|
15942
|
+
"text": "No projects on the map yet — check back soon.",
|
|
15943
15943
|
"file": "modules/status-ui/public/explore.html",
|
|
15944
|
-
"line":
|
|
15944
|
+
"line": 108,
|
|
15945
15945
|
"origin": "html-text",
|
|
15946
15946
|
"confidence": "certain"
|
|
15947
15947
|
},
|
|
@@ -15999,6 +15999,15 @@
|
|
|
15999
15999
|
"origin": "html-attr:aria-label",
|
|
16000
16000
|
"confidence": "certain"
|
|
16001
16001
|
},
|
|
16002
|
+
{
|
|
16003
|
+
"id": "f1b103f87df2",
|
|
16004
|
+
"surface": "status-dashboard",
|
|
16005
|
+
"text": "Projects on the map, and how each one stands on it.",
|
|
16006
|
+
"file": "modules/status-ui/public/explore.html",
|
|
16007
|
+
"line": 105,
|
|
16008
|
+
"origin": "html-text",
|
|
16009
|
+
"confidence": "certain"
|
|
16010
|
+
},
|
|
16002
16011
|
{
|
|
16003
16012
|
"id": "48473324e839",
|
|
16004
16013
|
"surface": "status-dashboard",
|
|
@@ -16017,6 +16026,15 @@
|
|
|
16017
16026
|
"origin": "html-text",
|
|
16018
16027
|
"confidence": "certain"
|
|
16019
16028
|
},
|
|
16029
|
+
{
|
|
16030
|
+
"id": "abd2ce869517",
|
|
16031
|
+
"surface": "status-dashboard",
|
|
16032
|
+
"text": "Something is here. Its name, and everything else about it, stay with its owner.",
|
|
16033
|
+
"file": "modules/status-ui/public/explore.js",
|
|
16034
|
+
"line": 74,
|
|
16035
|
+
"origin": "js-text-assign",
|
|
16036
|
+
"confidence": "certain"
|
|
16037
|
+
},
|
|
16020
16038
|
{
|
|
16021
16039
|
"id": "0cc023b5f470",
|
|
16022
16040
|
"surface": "status-dashboard",
|
|
@@ -1497,5 +1497,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1497
1497
|
landed since 1.19.528 with no explicit bump. run 33877197857. (task 1002620)
|
|
1498
1498
|
1.19.530 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1499
1499
|
landed since 1.19.529 with no explicit bump. run 33878817005. (task 1002620)
|
|
1500
|
+
1.19.531 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1501
|
+
landed since 1.19.530 with no explicit bump. run 33879403011. (task 1002620)
|
|
1500
1502
|
---------------------------------------------------------------------------
|
|
1501
1503
|
```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Explore —
|
|
6
|
+
<title>Explore — projects on the map</title>
|
|
7
7
|
<style>
|
|
8
8
|
/* Self-contained so the showcase renders correctly whether or not the host
|
|
9
9
|
injects theme tokens. Warm Cloud Bongos defaults; overridable by the pack. */
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
salmon --accent is a fill and measures 2.1:1 as type on the card. */
|
|
16
16
|
--bg: #edecee; --card: #fbfafc; --ink: #17181a; --ink-soft: #55565a;
|
|
17
17
|
--rule: #c6c6c8; --accent: #fb9569; --accent-deep: #a3481a;
|
|
18
|
+
/* The black hole's own colour, and deliberately NOT a pack token: --ink
|
|
19
|
+
inverts between the schemes and would paint the void white in dark mode.
|
|
20
|
+
A hole is the one thing on this page that is the same colour in both. */
|
|
21
|
+
--orb-void: #0b0b10;
|
|
18
22
|
}
|
|
19
23
|
* { box-sizing: border-box; }
|
|
20
24
|
body {
|
|
@@ -39,6 +43,55 @@
|
|
|
39
43
|
.card .origin { color: var(--accent-deep); font-size: .82rem; word-break: break-all; margin-top: auto; }
|
|
40
44
|
.card .art { width: 100%; height: 120px; object-fit: cover; border-radius: 10px; margin-bottom: .3rem; }
|
|
41
45
|
.empty, .err { text-align: center; color: var(--ink-soft); padding: 3rem 1rem; }
|
|
46
|
+
|
|
47
|
+
/* THE THREE ORBS (BV1.R07, task 1002326; ADR 0182 D1) — how a project stands on
|
|
48
|
+
the map, DRAWN. The label beside each one is the plain word (Public / Private /
|
|
49
|
+
Stealth): the space words belong to the shape, not to the copy, which is the
|
|
50
|
+
goal's own interview decision. The orb is aria-hidden, so that plain word is
|
|
51
|
+
its accessible name. */
|
|
52
|
+
.state { display: flex; align-items: center; gap: .5rem; }
|
|
53
|
+
.state-label {
|
|
54
|
+
font-size: .74rem; letter-spacing: .09em; text-transform: uppercase;
|
|
55
|
+
color: var(--ink-soft);
|
|
56
|
+
}
|
|
57
|
+
.orb { width: 22px; height: 22px; border-radius: 50%; flex: 0 0 auto; position: relative; }
|
|
58
|
+
/* planet — identified and open: a lit sphere, ringed so you can pick it out */
|
|
59
|
+
.orb-planet {
|
|
60
|
+
background: radial-gradient(circle at 33% 30%, #ffffff 0 9%, var(--accent) 42%, var(--accent-deep) 100%);
|
|
61
|
+
}
|
|
62
|
+
.orb-planet::after {
|
|
63
|
+
content: ''; position: absolute; left: -26%; right: -26%; top: 40%; height: 24%;
|
|
64
|
+
border: 1.5px solid var(--accent-deep); border-radius: 50%;
|
|
65
|
+
transform: rotate(-20deg); opacity: .8;
|
|
66
|
+
}
|
|
67
|
+
/* star — identified but closed: visible from anywhere, with no surface to land on */
|
|
68
|
+
.orb-star {
|
|
69
|
+
background: radial-gradient(circle, #ffffff 0 15%, var(--accent) 38%, transparent 70%);
|
|
70
|
+
}
|
|
71
|
+
/* the rays taper rather than end, so the orb reads as a point of light and not
|
|
72
|
+
as a plus sign */
|
|
73
|
+
.orb-star::before, .orb-star::after {
|
|
74
|
+
content: ''; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
|
|
75
|
+
}
|
|
76
|
+
.orb-star::before {
|
|
77
|
+
width: 2px; height: 155%;
|
|
78
|
+
background: linear-gradient(to bottom, transparent, var(--accent) 50%, transparent);
|
|
79
|
+
}
|
|
80
|
+
.orb-star::after {
|
|
81
|
+
width: 155%; height: 2px;
|
|
82
|
+
background: linear-gradient(to right, transparent, var(--accent) 50%, transparent);
|
|
83
|
+
}
|
|
84
|
+
/* black hole — existence without identity: you are shown the ring, never the object */
|
|
85
|
+
.orb-black-hole {
|
|
86
|
+
background: var(--orb-void);
|
|
87
|
+
box-shadow: 0 0 0 1.5px var(--accent), 0 0 10px 1px var(--accent);
|
|
88
|
+
}
|
|
89
|
+
/* an anonymous card has nothing to click and nothing to name, so it does not
|
|
90
|
+
pretend otherwise — no lift on hover, and its heading reads as the placeholder
|
|
91
|
+
it is rather than as a project's name */
|
|
92
|
+
.card.anon:hover { transform: none; box-shadow: none; }
|
|
93
|
+
.card.anon h2 { color: var(--ink-soft); font-weight: 600; }
|
|
94
|
+
|
|
42
95
|
@media (prefers-color-scheme: dark) {
|
|
43
96
|
:root { --bg: #000000; --card: #101012; --ink: #ffffff; --ink-soft: #c0c0c0; --rule: rgba(255,255,255,.16);
|
|
44
97
|
--accent-deep: color-mix(in srgb, var(--accent) 45%, #ffffff); }
|
|
@@ -49,10 +102,10 @@
|
|
|
49
102
|
<div class="wrap">
|
|
50
103
|
<header>
|
|
51
104
|
<h1>Explore</h1>
|
|
52
|
-
<p class="sub">
|
|
105
|
+
<p class="sub">Projects on the map, and how each one stands on it.</p>
|
|
53
106
|
</header>
|
|
54
107
|
<div id="grid" class="grid" hidden></div>
|
|
55
|
-
<div id="empty" class="empty" hidden>No
|
|
108
|
+
<div id="empty" class="empty" hidden>No projects on the map yet — check back soon.</div>
|
|
56
109
|
<div id="err" class="err" hidden>Couldn't load projects right now.</div>
|
|
57
110
|
</div>
|
|
58
111
|
<script src="/explore.js"></script>
|
|
@@ -1,15 +1,39 @@
|
|
|
1
|
-
// explore.js — the public Explore showcase (ADR 0141 §5, task #1002110)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// explore.js — the public Explore showcase (ADR 0141 §5, task #1002110), and since
|
|
2
|
+
// BV1.R07 (task 1002326) the in-repo surface that DOGFOODS the visible-projects feed:
|
|
3
|
+
// every project on the map, drawn as the state it stands in.
|
|
4
|
+
//
|
|
5
|
+
// Fetches the hub's public projects feed and renders one card each. On a NON-hub
|
|
6
|
+
// instance (platform-identity off) the endpoint 404s / returns nothing, so the page
|
|
7
|
+
// shows a graceful empty state.
|
|
8
|
+
//
|
|
9
|
+
// WHAT A CARD SAYS ABOUT VISIBILITY (ADR 0182 D1/D2a). Each feed row carries the
|
|
10
|
+
// render type the server derived from the project's join door — planet, star or
|
|
11
|
+
// black hole. This page draws that shape and labels it with the plain word
|
|
12
|
+
// (Public / Private / Stealth), because the space words are the drawn orb's and
|
|
13
|
+
// not the copy's. A black hole is ANONYMOUS: the feed already nulled everything
|
|
14
|
+
// identifying server-side, and the branch that renders it here reads no
|
|
15
|
+
// identifying field at all, so a server that regressed still could not put a name
|
|
16
|
+
// on this page. Dark matter — a project that has not said what it is — is excluded
|
|
17
|
+
// in SQL and never arrives; a row this page cannot classify is dropped rather than
|
|
18
|
+
// guessed at, which is the same fail-closed rule one layer down.
|
|
4
19
|
//
|
|
5
20
|
// Project fields come from the network, so every value is set via textContent /
|
|
6
21
|
// attribute assignment — never innerHTML — so a project name/tagline can't inject markup.
|
|
22
|
+
//
|
|
23
|
+
// Proof: ../../../tests/explore_visible_states.mjs
|
|
7
24
|
(function () {
|
|
8
25
|
'use strict';
|
|
9
26
|
var grid = document.getElementById('grid');
|
|
10
27
|
var empty = document.getElementById('empty');
|
|
11
28
|
var err = document.getElementById('err');
|
|
12
29
|
|
|
30
|
+
// The map's render types → the plain word shown beside each one. Keyed on the
|
|
31
|
+
// feed's own `orb` field rather than re-derived from `state`: the state→orb
|
|
32
|
+
// mapping has exactly one home (modules/platform-identity/projects-feed.js) and a
|
|
33
|
+
// second copy here is a copy that drifts. An orb missing from this table is a row
|
|
34
|
+
// this page cannot honestly draw, so it draws nothing.
|
|
35
|
+
var ORB_LABEL = { planet: 'Public', star: 'Private', 'black-hole': 'Stealth' };
|
|
36
|
+
|
|
13
37
|
function show(el) { if (el) el.hidden = false; }
|
|
14
38
|
|
|
15
39
|
// Only http(s) origins are linkified — never javascript:/data: etc.
|
|
@@ -20,12 +44,50 @@
|
|
|
20
44
|
} catch (_) { return null; }
|
|
21
45
|
}
|
|
22
46
|
|
|
47
|
+
// The orb + its plain word. The shape is decorative (aria-hidden) — the label is
|
|
48
|
+
// what a screen reader gets, because a shape it cannot see is not a name.
|
|
49
|
+
function stateRow(orb) {
|
|
50
|
+
var row = document.createElement('div');
|
|
51
|
+
row.className = 'state';
|
|
52
|
+
var dot = document.createElement('span');
|
|
53
|
+
dot.className = 'orb orb-' + orb;
|
|
54
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
55
|
+
row.appendChild(dot);
|
|
56
|
+
var label = document.createElement('span');
|
|
57
|
+
label.className = 'state-label';
|
|
58
|
+
label.textContent = ORB_LABEL[orb];
|
|
59
|
+
row.appendChild(label);
|
|
60
|
+
return row;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A black hole publishes EXISTENCE and nothing else (ADR 0182 D2a). Deliberately
|
|
64
|
+
// takes no project argument: there is no identifying value in scope to leak by
|
|
65
|
+
// accident, now or when the feed grows a column.
|
|
66
|
+
function anonymousCard() {
|
|
67
|
+
var el = document.createElement('div');
|
|
68
|
+
el.className = 'card anon';
|
|
69
|
+
el.appendChild(stateRow('black-hole'));
|
|
70
|
+
var h = document.createElement('h2');
|
|
71
|
+
h.textContent = 'A project';
|
|
72
|
+
el.appendChild(h);
|
|
73
|
+
var p = document.createElement('p');
|
|
74
|
+
p.textContent = 'Something is here. Its name, and everything else about it, stay with its owner.';
|
|
75
|
+
el.appendChild(p);
|
|
76
|
+
return el;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// One feed row → one card, or null when the row is not drawable.
|
|
23
80
|
function card(p) {
|
|
81
|
+
var orb = String((p && p.orb) || '');
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(ORB_LABEL, orb)) return null;
|
|
83
|
+
if (orb === 'black-hole') return anonymousCard();
|
|
84
|
+
|
|
24
85
|
var href = safeHref(p.origin);
|
|
25
86
|
var el = document.createElement(href ? 'a' : 'div');
|
|
26
87
|
el.className = 'card';
|
|
27
88
|
if (href) { el.href = href; el.target = '_blank'; el.rel = 'noopener noreferrer'; }
|
|
28
89
|
|
|
90
|
+
el.appendChild(stateRow(orb));
|
|
29
91
|
if (p.art_url && safeHref(p.art_url)) {
|
|
30
92
|
var img = document.createElement('img');
|
|
31
93
|
img.className = 'art';
|
|
@@ -55,8 +117,12 @@
|
|
|
55
117
|
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
|
|
56
118
|
.then(function (data) {
|
|
57
119
|
var projects = (data && Array.isArray(data.projects)) ? data.projects : [];
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
// Filtered BEFORE the count is read: a feed of rows none of which can be drawn
|
|
121
|
+
// is an empty map, and a visitor is owed the empty state rather than a grid
|
|
122
|
+
// with nothing in it.
|
|
123
|
+
var cards = projects.map(card).filter(Boolean);
|
|
124
|
+
if (cards.length === 0) { show(empty); return; }
|
|
125
|
+
cards.forEach(function (el) { grid.appendChild(el); });
|
|
60
126
|
show(grid);
|
|
61
127
|
})
|
|
62
128
|
.catch(function () {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.531",
|
|
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.531",
|
|
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.531",
|
|
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
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.531'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// tests/explore_visible_states.mjs — the Explore page draws the three visible
|
|
2
|
+
// states off the R06 feed (BV1.R07, task 1002326; goal 1000046, criterion
|
|
3
|
+
// `proj-visibility-states`; ADR 0182 D1/D2a).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS FILE EXECUTES THE PAGE RATHER THAN GREPPING IT. explore.js builds every
|
|
6
|
+
// card with createElement/appendChild — there is no markup in explore.html to pin.
|
|
7
|
+
// A static test could only assert that the words "planet" and "black-hole" appear
|
|
8
|
+
// somewhere in the source, which a page that renders nothing would also pass. So
|
|
9
|
+
// this runs the REAL file in `vm` under a small DOM stub (the tests/idea_detail_page.mjs
|
|
10
|
+
// precedent — there is no jsdom in this repo) and asserts over the tree a visitor
|
|
11
|
+
// would actually get.
|
|
12
|
+
//
|
|
13
|
+
// The three things R07 owes, as assertions:
|
|
14
|
+
// 1. each visible state draws ITS orb — planet / star / black hole — off the
|
|
15
|
+
// feed's own `orb` field, with the plain word (Public / Private / Stealth)
|
|
16
|
+
// as the label, because the space words are the drawn orb's, not the copy's;
|
|
17
|
+
// 2. a stealth row is ANONYMOUS on the page: no name, no address, no picture,
|
|
18
|
+
// no link — and stays anonymous even if a regressed server sends identity,
|
|
19
|
+
// which is the client half of the feed's server-side allow-list (D2a);
|
|
20
|
+
// 3. DARK MATTER NEVER DRAWS. It is excluded in SQL and must never reach here;
|
|
21
|
+
// if it ever does, a row this page cannot classify is dropped, not guessed at.
|
|
22
|
+
//
|
|
23
|
+
// Run: node --test --test-reporter=tap tests/explore_visible_states.mjs
|
|
24
|
+
|
|
25
|
+
import test from 'node:test';
|
|
26
|
+
import assert from 'node:assert/strict';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import vm from 'node:vm';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
// The state→orb mapping is the feed's, not this page's (projects-feed.js is the
|
|
33
|
+
// only copy of it in the repo). Importing it here means a rename on the server
|
|
34
|
+
// fails this file rather than silently leaving Explore drawing the old words.
|
|
35
|
+
import { createRequire } from 'node:module';
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const { ORB_BY_STATE, feedShapeFor } = require('../modules/platform-identity/projects-feed.js');
|
|
38
|
+
|
|
39
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
|
+
const PAGE = path.join(ROOT, 'modules/status-ui/public');
|
|
41
|
+
const SRC = fs.readFileSync(path.join(PAGE, 'explore.js'), 'utf8');
|
|
42
|
+
const HTML = fs.readFileSync(path.join(PAGE, 'explore.html'), 'utf8');
|
|
43
|
+
|
|
44
|
+
// ---- the DOM stub ---------------------------------------------------------
|
|
45
|
+
// Only what explore.js touches: createElement, appendChild, textContent, className,
|
|
46
|
+
// setAttribute and plain property assignment (href/src/alt/loading/target/rel).
|
|
47
|
+
|
|
48
|
+
function makeElement(tag) {
|
|
49
|
+
return {
|
|
50
|
+
tagName: String(tag).toUpperCase(),
|
|
51
|
+
className: '',
|
|
52
|
+
textContent: '',
|
|
53
|
+
hidden: false,
|
|
54
|
+
children: [],
|
|
55
|
+
attrs: {},
|
|
56
|
+
appendChild(child) { this.children.push(child); return child; },
|
|
57
|
+
setAttribute(k, v) { this.attrs[k] = String(v); },
|
|
58
|
+
getAttribute(k) { return Object.prototype.hasOwnProperty.call(this.attrs, k) ? this.attrs[k] : null; },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Every node in the subtree, root first.
|
|
63
|
+
function nodes(el) {
|
|
64
|
+
return [el, ...el.children.flatMap(nodes)];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Everything a visitor can READ on this subtree.
|
|
68
|
+
function textOf(el) {
|
|
69
|
+
return nodes(el).map((n) => n.textContent || '').join(' ').replace(/\s+/g, ' ').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Everything a visitor's BROWSER can reach: text plus every attribute and every
|
|
73
|
+
// URL-bearing property. This is what an anonymity assertion has to look at — a
|
|
74
|
+
// name hidden in an href or an <img src> has still left the building.
|
|
75
|
+
function reachable(el) {
|
|
76
|
+
return nodes(el)
|
|
77
|
+
.flatMap((n) => [n.textContent, n.className, n.href, n.src, n.alt, ...Object.values(n.attrs)])
|
|
78
|
+
.filter((v) => typeof v === 'string')
|
|
79
|
+
.join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const classed = (el, cls) => nodes(el).filter((n) => String(n.className).split(/\s+/).includes(cls));
|
|
83
|
+
|
|
84
|
+
// Boot explore.js against the stub with `projects` as the feed's answer.
|
|
85
|
+
function render(projects, { ok = true, body = null } = {}) {
|
|
86
|
+
const els = new Map(['grid', 'empty', 'err'].map(
|
|
87
|
+
(id) => [id, Object.assign(makeElement('div'), { id, hidden: true })]));
|
|
88
|
+
|
|
89
|
+
const documentObj = {
|
|
90
|
+
getElementById: (id) => els.get(id) || null,
|
|
91
|
+
createElement: (tag) => makeElement(tag),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const requests = [];
|
|
95
|
+
const sandbox = {
|
|
96
|
+
document: documentObj,
|
|
97
|
+
URL,
|
|
98
|
+
console,
|
|
99
|
+
setTimeout,
|
|
100
|
+
fetch(url, opts) {
|
|
101
|
+
requests.push({ url, opts });
|
|
102
|
+
return Promise.resolve({
|
|
103
|
+
ok,
|
|
104
|
+
status: ok ? 200 : 500,
|
|
105
|
+
json: () => Promise.resolve(body === null ? { projects } : body),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
sandbox.globalThis = sandbox;
|
|
110
|
+
vm.createContext(sandbox);
|
|
111
|
+
vm.runInContext(SRC, sandbox, { filename: 'explore.js' });
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
requests,
|
|
115
|
+
grid: () => els.get('grid'),
|
|
116
|
+
empty: () => els.get('empty'),
|
|
117
|
+
err: () => els.get('err'),
|
|
118
|
+
cards: () => els.get('grid').children,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// fetch → .then(json) → .then(render) is three microtask hops; a macrotask drains them all.
|
|
123
|
+
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
124
|
+
|
|
125
|
+
// ---- fixtures: rows exactly as GET /projects/featured shapes them ----------
|
|
126
|
+
// Built through the REAL feedShapeFor(), so the stealth row is redacted by the
|
|
127
|
+
// server's own allow-list rather than by this test's idea of it.
|
|
128
|
+
|
|
129
|
+
const IDENTIFIED = {
|
|
130
|
+
origin: 'https://example-project.test',
|
|
131
|
+
name: 'Example Project',
|
|
132
|
+
tagline: 'A tagline that says what it is.',
|
|
133
|
+
art_url: 'https://example-project.test/art.png',
|
|
134
|
+
type: 'game',
|
|
135
|
+
template: null,
|
|
136
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const publicRow = feedShapeFor({ ...IDENTIFIED, visibility: 'public' });
|
|
140
|
+
const privateRow = feedShapeFor({
|
|
141
|
+
...IDENTIFIED, origin: 'https://closed-project.test', name: 'Closed Project', visibility: 'private',
|
|
142
|
+
});
|
|
143
|
+
const stealthRow = feedShapeFor({
|
|
144
|
+
...IDENTIFIED, origin: 'https://hidden-project.test', name: 'Hidden Project', visibility: 'stealth',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---- 1. each visible state draws its orb ----------------------------------
|
|
148
|
+
|
|
149
|
+
test('the feed mapping this page reads is still planet / star / black-hole', () => {
|
|
150
|
+
assert.deepEqual({ ...ORB_BY_STATE }, { public: 'planet', private: 'star', stealth: 'black-hole' });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('each of the three visible states draws its own orb, with the plain word beside it', async () => {
|
|
154
|
+
const r = render([publicRow, privateRow, stealthRow]);
|
|
155
|
+
await settle();
|
|
156
|
+
assert.equal(r.cards().length, 3, 'all three visible states are on the map');
|
|
157
|
+
assert.equal(r.grid().hidden, false, 'the grid is shown');
|
|
158
|
+
assert.equal(r.empty().hidden, true, 'and the empty state is not');
|
|
159
|
+
|
|
160
|
+
const expected = [['orb-planet', 'Public'], ['orb-star', 'Private'], ['orb-black-hole', 'Stealth']];
|
|
161
|
+
r.cards().forEach((card, i) => {
|
|
162
|
+
const [orbClass, label] = expected[i];
|
|
163
|
+
assert.equal(classed(card, orbClass).length, 1, `card ${i} draws exactly one ${orbClass}`);
|
|
164
|
+
assert.ok(classed(card, 'state-label').some((n) => n.textContent === label),
|
|
165
|
+
`card ${i} is labelled "${label}"`);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the drawn orb is decorative — the plain word beside it is its accessible name', async () => {
|
|
170
|
+
const r = render([publicRow]);
|
|
171
|
+
await settle();
|
|
172
|
+
const [orb] = classed(r.cards()[0], 'orb');
|
|
173
|
+
assert.equal(orb.getAttribute('aria-hidden'), 'true',
|
|
174
|
+
'a screen reader hears "Public", never a shape it cannot see');
|
|
175
|
+
assert.equal(orb.textContent, '', 'the orb carries no text of its own');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('no card writes the space words as copy — the metaphor is drawn, not said', async () => {
|
|
179
|
+
const r = render([publicRow, privateRow, stealthRow]);
|
|
180
|
+
await settle();
|
|
181
|
+
for (const card of r.cards()) {
|
|
182
|
+
assert.doesNotMatch(textOf(card), /\b(planet|star|black hole|black-hole)\b/i,
|
|
183
|
+
'the goal\'s interview decision: plain labels only — Public, Private, Stealth');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---- 2. a stealth row is anonymous ----------------------------------------
|
|
188
|
+
|
|
189
|
+
test('a stealth card names nothing: no name, no address, no picture, no link', async () => {
|
|
190
|
+
const r = render([stealthRow]);
|
|
191
|
+
await settle();
|
|
192
|
+
const card = r.cards()[0];
|
|
193
|
+
|
|
194
|
+
assert.equal(card.tagName, 'DIV', 'an anonymous card is not a link — there is nowhere to send anyone');
|
|
195
|
+
assert.equal(nodes(card).filter((n) => n.tagName === 'IMG').length, 0, 'and carries no picture');
|
|
196
|
+
assert.equal(nodes(card).filter((n) => n.tagName === 'A').length, 0, 'and no link inside it either');
|
|
197
|
+
assert.equal(classed(card, 'origin').length, 0, 'and shows no address');
|
|
198
|
+
|
|
199
|
+
const seen = reachable(card);
|
|
200
|
+
for (const secret of ['Hidden Project', 'hidden-project.test', 'art.png', 'A tagline']) {
|
|
201
|
+
assert.ok(!seen.includes(secret), `"${secret}" never reaches the page`);
|
|
202
|
+
}
|
|
203
|
+
assert.ok(/something is here/i.test(textOf(card)),
|
|
204
|
+
'it says what a black hole says: something is there, and that is all');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('a stealth row stays anonymous even if a regressed server sends identity', async () => {
|
|
208
|
+
// The feed nulls these server-side (ADR 0182 D2a). This is the client half of
|
|
209
|
+
// the same allow-list: the anonymous branch never READS an identifying field,
|
|
210
|
+
// so a server that started sending one again could not put it on the page.
|
|
211
|
+
const leaky = { ...stealthRow, name: 'Hidden Project', origin: 'https://hidden-project.test',
|
|
212
|
+
tagline: 'A tagline that says what it is.', art_url: 'https://hidden-project.test/art.png' };
|
|
213
|
+
const r = render([leaky]);
|
|
214
|
+
await settle();
|
|
215
|
+
const seen = reachable(r.cards()[0]);
|
|
216
|
+
for (const secret of ['Hidden Project', 'hidden-project.test', 'A tagline', 'art.png']) {
|
|
217
|
+
assert.ok(!seen.includes(secret), `"${secret}" is dropped by the page too`);
|
|
218
|
+
}
|
|
219
|
+
assert.equal(classed(r.cards()[0], 'orb-black-hole').length, 1, 'it is still a black hole');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('an identified card still shows its name, tagline, picture and address', async () => {
|
|
223
|
+
const r = render([publicRow]);
|
|
224
|
+
await settle();
|
|
225
|
+
const card = r.cards()[0];
|
|
226
|
+
assert.equal(card.tagName, 'A', 'a planet is somewhere you can go');
|
|
227
|
+
assert.equal(card.href, 'https://example-project.test/');
|
|
228
|
+
assert.ok(textOf(card).includes('Example Project'));
|
|
229
|
+
assert.ok(textOf(card).includes('A tagline that says what it is.'));
|
|
230
|
+
assert.equal(nodes(card).filter((n) => n.tagName === 'IMG')[0].src, 'https://example-project.test/art.png');
|
|
231
|
+
assert.ok(classed(card, 'origin')[0].textContent.includes('example-project.test'));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ---- 3. dark matter never draws -------------------------------------------
|
|
235
|
+
|
|
236
|
+
test('a row this page cannot classify is not drawn', async () => {
|
|
237
|
+
// Dark matter is excluded in SQL and has no orb to draw; the same drop covers a
|
|
238
|
+
// fourth state added later and not taught to this page. Fail-closed: a card is
|
|
239
|
+
// never guessed at from a row the feed did not classify.
|
|
240
|
+
const unclassifiable = [
|
|
241
|
+
{ ...publicRow, state: 'dark-matter', orb: null },
|
|
242
|
+
{ ...publicRow, orb: 'quasar' },
|
|
243
|
+
{ ...publicRow, orb: undefined },
|
|
244
|
+
];
|
|
245
|
+
const r = render([...unclassifiable, publicRow]);
|
|
246
|
+
await settle();
|
|
247
|
+
assert.equal(r.cards().length, 1, 'only the one row with a known orb is drawn');
|
|
248
|
+
assert.equal(classed(r.cards()[0], 'orb-planet').length, 1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('a feed of nothing drawable shows the empty state, not an empty grid', async () => {
|
|
252
|
+
const r = render([{ ...publicRow, state: 'dark-matter', orb: null }]);
|
|
253
|
+
await settle();
|
|
254
|
+
assert.equal(r.empty().hidden, false, 'the visitor is told the map is empty');
|
|
255
|
+
assert.equal(r.grid().hidden, true, 'rather than shown a grid with nothing in it');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('an empty feed and a failed fetch both land on the empty state', async () => {
|
|
259
|
+
const none = render([]);
|
|
260
|
+
await settle();
|
|
261
|
+
assert.equal(none.empty().hidden, false);
|
|
262
|
+
|
|
263
|
+
const down = render([], { ok: false });
|
|
264
|
+
await settle();
|
|
265
|
+
assert.equal(down.empty().hidden, false, 'a non-hub instance 404s here — not an error to a visitor');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ---- the page it draws into ------------------------------------------------
|
|
269
|
+
|
|
270
|
+
test('explore.html paints all three orbs and defines the void colour', () => {
|
|
271
|
+
for (const cls of ['.orb-planet', '.orb-star', '.orb-black-hole']) {
|
|
272
|
+
assert.ok(HTML.includes(`${cls} {`) || HTML.includes(`${cls},`) || HTML.includes(`${cls}:`),
|
|
273
|
+
`${cls} has a rule — a class name with no paint draws nothing`);
|
|
274
|
+
}
|
|
275
|
+
assert.match(HTML, /--orb-void:\s*#[0-9a-fA-F]{6}/,
|
|
276
|
+
'the hole has its own colour, in both schemes — the ink token inverts and would paint it white in dark mode');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('the page reads the public feed and asks for JSON', async () => {
|
|
280
|
+
const r = render([]);
|
|
281
|
+
await settle();
|
|
282
|
+
assert.equal(r.requests.length, 1);
|
|
283
|
+
assert.equal(r.requests[0].url, '/api/gds/projects/featured');
|
|
284
|
+
assert.equal(r.requests[0].opts.headers.Accept, 'application/json');
|
|
285
|
+
});
|