@bongos/core 1.19.577 → 1.19.579
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 +28 -18
- package/docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md +4 -1
- package/docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md +128 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +3 -4
- package/docs/copy-registry.json +4 -13
- package/docs/module-api-changelog.md +4 -0
- package/modules/public-landing/public/projects.html +96 -61
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/manage_manifest_shared_read.mjs +205 -0
- package/tests/projects_hub.mjs +55 -4
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.579",
|
|
6
|
+
"core_contract": "1.19.579",
|
|
7
|
+
"source_commit": "8b206e9f473df316fc1981854a479d1ba509d76e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-07T17:52:17.407Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 452,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2047,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2523,
|
|
20
|
+
"tree_sha256": "bd13bef2ae141fa766ed5c8a67894be21c62263079037e763bc1533ff500afdc",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1792,7 +1792,7 @@
|
|
|
1792
1792
|
{
|
|
1793
1793
|
"path": "docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md",
|
|
1794
1794
|
"mode": "0000644",
|
|
1795
|
-
"sha256": "
|
|
1795
|
+
"sha256": "9240caa363815ca9bbd49993bd73ecda85c6f91280ac152f344f69c77928bd22"
|
|
1796
1796
|
},
|
|
1797
1797
|
{
|
|
1798
1798
|
"path": "docs/adr/0256-the-visibility-ceiling-is-the-sign-in-gate-too-and-the-reader-moves-to-the-composer.md",
|
|
@@ -1809,10 +1809,15 @@
|
|
|
1809
1809
|
"mode": "0000644",
|
|
1810
1810
|
"sha256": "28f3644d3d9dcd2985b7e3f42cf2135af86203eee35b6e509d1ff98f630f1228"
|
|
1811
1811
|
},
|
|
1812
|
+
{
|
|
1813
|
+
"path": "docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md",
|
|
1814
|
+
"mode": "0000644",
|
|
1815
|
+
"sha256": "772b9153d882f6bbd5000ee90b83e05d550ee4f04d4f5392db36e223d9a58c59"
|
|
1816
|
+
},
|
|
1812
1817
|
{
|
|
1813
1818
|
"path": "docs/adr/README.md",
|
|
1814
1819
|
"mode": "0000644",
|
|
1815
|
-
"sha256": "
|
|
1820
|
+
"sha256": "bf52fa642d9a576a88f961b46fbb68352e3fd8dd79004440f5e4182e549039ed"
|
|
1816
1821
|
},
|
|
1817
1822
|
{
|
|
1818
1823
|
"path": "docs/api-reference.md",
|
|
@@ -1847,12 +1852,12 @@
|
|
|
1847
1852
|
{
|
|
1848
1853
|
"path": "docs/copy-inventory.md",
|
|
1849
1854
|
"mode": "0000644",
|
|
1850
|
-
"sha256": "
|
|
1855
|
+
"sha256": "da770863626202ff2566de7f0a152eebc086662c74d86423794d378fca2d83e6"
|
|
1851
1856
|
},
|
|
1852
1857
|
{
|
|
1853
1858
|
"path": "docs/copy-registry.json",
|
|
1854
1859
|
"mode": "0000644",
|
|
1855
|
-
"sha256": "
|
|
1860
|
+
"sha256": "423614f0519d1e94e605e439b53a2bac7a63c2ae8005b0021c71e6c81f38edfc"
|
|
1856
1861
|
},
|
|
1857
1862
|
{
|
|
1858
1863
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2697,7 +2702,7 @@
|
|
|
2697
2702
|
{
|
|
2698
2703
|
"path": "docs/module-api-changelog.md",
|
|
2699
2704
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2705
|
+
"sha256": "cb7bb494b4b2ad905f1067b6f3c11e6331faa8beaad533c70d4a5009c456a807"
|
|
2701
2706
|
},
|
|
2702
2707
|
{
|
|
2703
2708
|
"path": "docs/modules-contract.md",
|
|
@@ -6812,7 +6817,7 @@
|
|
|
6812
6817
|
{
|
|
6813
6818
|
"path": "modules/public-landing/public/projects.html",
|
|
6814
6819
|
"mode": "0000644",
|
|
6815
|
-
"sha256": "
|
|
6820
|
+
"sha256": "0154fab4db489db2eb1955bffdd9b0585ff7fe28607897e75c26d83c7edfc3e0"
|
|
6816
6821
|
},
|
|
6817
6822
|
{
|
|
6818
6823
|
"path": "modules/public-landing/public/projects.probes.json",
|
|
@@ -7557,12 +7562,12 @@
|
|
|
7557
7562
|
{
|
|
7558
7563
|
"path": "package-lock.json",
|
|
7559
7564
|
"mode": "0000644",
|
|
7560
|
-
"sha256": "
|
|
7565
|
+
"sha256": "ce40726e213f4a94fd3983d81c5919e4cc435fda1d666e7cf598fc2f963b8741"
|
|
7561
7566
|
},
|
|
7562
7567
|
{
|
|
7563
7568
|
"path": "package.json",
|
|
7564
7569
|
"mode": "0000644",
|
|
7565
|
-
"sha256": "
|
|
7570
|
+
"sha256": "6ce1fcd5c0378c8001b8249d8359d04ecfa5730e07dc3e21cda4952fcf1deeae"
|
|
7566
7571
|
},
|
|
7567
7572
|
{
|
|
7568
7573
|
"path": "public-docs/index.html",
|
|
@@ -9267,7 +9272,7 @@
|
|
|
9267
9272
|
{
|
|
9268
9273
|
"path": "src/module-api.js",
|
|
9269
9274
|
"mode": "0000644",
|
|
9270
|
-
"sha256": "
|
|
9275
|
+
"sha256": "c1da85daf5425cfb3bbd7d8e29e6995e90ead6088565476973954ca366ef14f5"
|
|
9271
9276
|
},
|
|
9272
9277
|
{
|
|
9273
9278
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11179,6 +11184,11 @@
|
|
|
11179
11184
|
"mode": "0000644",
|
|
11180
11185
|
"sha256": "9a1813d6036886ea07d1563e71a466e66c28554b75b22605464c795fd9fa2ebb"
|
|
11181
11186
|
},
|
|
11187
|
+
{
|
|
11188
|
+
"path": "tests/manage_manifest_shared_read.mjs",
|
|
11189
|
+
"mode": "0000644",
|
|
11190
|
+
"sha256": "419234978bb7688f83ec14bb2db7fb4be1eec99205595795c705befe5620b89c"
|
|
11191
|
+
},
|
|
11182
11192
|
{
|
|
11183
11193
|
"path": "tests/manage_settings_shared_read.mjs",
|
|
11184
11194
|
"mode": "0000644",
|
|
@@ -11642,7 +11652,7 @@
|
|
|
11642
11652
|
{
|
|
11643
11653
|
"path": "tests/projects_hub.mjs",
|
|
11644
11654
|
"mode": "0000644",
|
|
11645
|
-
"sha256": "
|
|
11655
|
+
"sha256": "d32fd31e368acdaaf1d4f31b775f62ef8e20ea4f5810a37fddb8ef1dba1487b6"
|
|
11646
11656
|
},
|
|
11647
11657
|
{
|
|
11648
11658
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
@@ -150,7 +150,10 @@ Stated plainly, because a privacy fix that oversells itself is worse than none:
|
|
|
150
150
|
than the ordering one and it is untouched here: it is inherent in publishing a named list
|
|
151
151
|
at all, and closing it needs a different mechanism (a delay, a decoy, or accepting that a
|
|
152
152
|
project's *departure* from the public list is public). Filed separately rather than
|
|
153
|
-
silently implied to be fixed
|
|
153
|
+
silently implied to be fixed — and **now decided**: the owner chose to accept it and say
|
|
154
|
+
so in the copy ([ADR 0259](<redacted>.md),
|
|
155
|
+
[task 1003672](https://cloudbongos.com/builders#/task/1003672)), which also carries the
|
|
156
|
+
§6.3 residual below.
|
|
154
157
|
3. **Position is stable within one epoch.** Two snapshots taken the same day can still tell
|
|
155
158
|
black holes apart, though not name them. The rotation bounds this to the epoch.
|
|
156
159
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ADR 0259 — A project's departure from the public list is public, and the copy says so
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-07
|
|
5
|
+
- **Task:** [task 1003672](https://cloudbongos.com/builders#/task/1003672) (BONGOS-V1, goal 1000046 — *Project creation*)
|
|
6
|
+
- **Deciders:** the owner (choosing between accept / delay / decoys), Claude
|
|
7
|
+
- **Related:** [ADR 0255](<redacted>.md) §6.2 (which filed this leak rather than implying it fixed) · [ADR 0182](<redacted>.md) D2a (the stealth redaction and its `STEALTH_ANONYMOUS_KEEPS` allow-list) · [ADR 0253](<redacted>.md) (the `visibility_source` provenance this feed joins)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. The channel
|
|
12
|
+
|
|
13
|
+
The public projects feed publishes a named list plus a count of anonymous rows. When a
|
|
14
|
+
project flips to stealth, its named row **leaves the identified block** and the anonymous
|
|
15
|
+
count **rises by one**. An observer who keeps yesterday's feed and diffs it against today's
|
|
16
|
+
reads that off directly:
|
|
17
|
+
|
|
18
|
+
> *Mercury is gone, and there is one more black hole.*
|
|
19
|
+
|
|
20
|
+
That names the project that went stealth, and the moment it happened, with no reference to
|
|
21
|
+
ordering at all.
|
|
22
|
+
|
|
23
|
+
**Everything ADR 0182 D2a does is defeated by it.** That ADR takes real trouble to keep any
|
|
24
|
+
stable per-project key off an anonymous row — `STEALTH_ANONYMOUS_KEEPS = ['state', 'orb']`
|
|
25
|
+
is an allow-list precisely so a column added next year is anonymous without anyone
|
|
26
|
+
remembering to come back to it. But the identity was never carried *on* the anonymous row.
|
|
27
|
+
It is carried by the **absence** of the named row that used to sit beside it.
|
|
28
|
+
|
|
29
|
+
**It is larger than the leak [ADR 0255](<redacted>.md)
|
|
30
|
+
closed.** The ordering channel needed repeated sampling and correlation to be useful. This
|
|
31
|
+
one is a single diff of two snapshots and it is exact.
|
|
32
|
+
|
|
33
|
+
## 2. Why no ordering fix reaches it
|
|
34
|
+
|
|
35
|
+
It is not an ordering property, so no `ORDER BY` change touches it. It is inherent in
|
|
36
|
+
publishing a named list at all: any list that names its members leaks a member's departure
|
|
37
|
+
to anyone who read it twice. The three ways out are all product decisions with visible
|
|
38
|
+
costs, which is why 0255 filed this rather than fixing it:
|
|
39
|
+
|
|
40
|
+
| Option | Cost |
|
|
41
|
+
|---|---|
|
|
42
|
+
| **Accept it, and say so** | An observer archiving the list learns who went stealth, and when. |
|
|
43
|
+
| **Delay** — keep the named row for a window | A privacy control that takes days to take effect, which is the wrong direction for a privacy control. |
|
|
44
|
+
| **Decoys** — pad the anonymous block | The count stops being truthful. It is truthful today. |
|
|
45
|
+
|
|
46
|
+
## 3. Decision
|
|
47
|
+
|
|
48
|
+
**Accept it, and make the copy say so.**
|
|
49
|
+
|
|
50
|
+
The promise stealth actually keeps is about the project's *contents*, not about the
|
|
51
|
+
*transition*. An anonymous row stays anonymous: no name, no tagline, no art, no origin, no
|
|
52
|
+
stable key, nothing that survives to the next snapshot. What is not private is the act of
|
|
53
|
+
stepping off the named list, because the named list is published and readers may keep
|
|
54
|
+
copies of it.
|
|
55
|
+
|
|
56
|
+
Two reasons this is the right end of the trade, and not merely the cheapest:
|
|
57
|
+
|
|
58
|
+
1. **It is already the honest reading of the existing promise.** ADR 0182 D2a made existence
|
|
59
|
+
and count public *by design* — the map shows that hidden projects are there. A count that
|
|
60
|
+
is public and truthful is a count whose changes are public. Delay and decoys both work by
|
|
61
|
+
making a currently-honest number less honest.
|
|
62
|
+
|
|
63
|
+
2. **The alternatives damage the control they defend.** A delay means an owner clicks the
|
|
64
|
+
privacy setting and is not private yet, which is a worse failure than the one it fixes —
|
|
65
|
+
the owner believes they are hidden while they are not. Decoys spend the count's
|
|
66
|
+
truthfulness on a guarantee that is still only probabilistic.
|
|
67
|
+
|
|
68
|
+
### 3.1 What "say so" means concretely
|
|
69
|
+
|
|
70
|
+
The owner-facing copy for Stealth previously read:
|
|
71
|
+
|
|
72
|
+
> The map shows only that something is there. Its name, and everything else about it, stay
|
|
73
|
+
> yours — and nobody can ask to join.
|
|
74
|
+
|
|
75
|
+
Every clause is true and the paragraph as a whole still oversells, because a reader takes
|
|
76
|
+
"its name stays yours" to cover the switch itself. The copy now names the boundary in both
|
|
77
|
+
places an owner meets this setting — the choice's blurb and the explanatory fold — in the
|
|
78
|
+
plain register [ADR 0182](<redacted>.md)'s interview fixed for this
|
|
79
|
+
card (the space metaphor belongs to the drawn orb, never to the words).
|
|
80
|
+
|
|
81
|
+
**A privacy control that oversells is worse than one that admits a limit**, which is the
|
|
82
|
+
same standard 0255 §6 set for itself.
|
|
83
|
+
|
|
84
|
+
## 4. What a future fix must not do
|
|
85
|
+
|
|
86
|
+
Recorded so a later attempt does not undo the two guarantees that *are* held:
|
|
87
|
+
|
|
88
|
+
- **No stable per-project key on an anonymous row** — the `STEALTH_ANONYMOUS_KEEPS`
|
|
89
|
+
allow-list ([ADR 0182](<redacted>.md) D2a). A decoy scheme that
|
|
90
|
+
needs to tell its own padding apart from real rows is the classic way this gets
|
|
91
|
+
reintroduced.
|
|
92
|
+
- **No anonymous ordering an observer can predict or correlate**
|
|
93
|
+
([ADR 0255](<redacted>.md) §3) — the trailing
|
|
94
|
+
block's keyed, rotating permutation.
|
|
95
|
+
|
|
96
|
+
## 5. The residual this does not address either
|
|
97
|
+
|
|
98
|
+
Within one 24h epoch the anonymous block's permutation is stable, so two snapshots taken the
|
|
99
|
+
same day can tell black holes apart — though not name them. Bounded by the rotation, and
|
|
100
|
+
carried here from [ADR 0255](<redacted>.md) §6.3 so
|
|
101
|
+
both residuals live in one place. Accepted on the same reasoning: the fix is a per-request
|
|
102
|
+
permutation, which breaks pagination exactly the way `ORDER BY random()` was rejected for in
|
|
103
|
+
0255.
|
|
104
|
+
|
|
105
|
+
## 6. Consequences
|
|
106
|
+
|
|
107
|
+
- No behaviour changes in `modules/platform-identity/projects-feed.js`. The feed, the
|
|
108
|
+
redaction allow-list and the permutation are all untouched — this ADR decides that they
|
|
109
|
+
are *finished*, not that they need more.
|
|
110
|
+
- The Stealth copy carries the boundary in both surfaces, and
|
|
111
|
+
`tests/projects_hub.mjs` pins that it does, so the sentence cannot quietly drop out of a
|
|
112
|
+
future copy edit and leave the page overselling again.
|
|
113
|
+
- [ADR 0255](<redacted>.md) §6.2's "filed
|
|
114
|
+
separately" is now resolved, and points here.
|
|
115
|
+
|
|
116
|
+
## Alternatives rejected
|
|
117
|
+
|
|
118
|
+
- **A delay window before the named row is dropped.** Rejected: the setting would not take
|
|
119
|
+
effect when the owner sets it, and the owner would not know that. It converts an
|
|
120
|
+
information leak into a false belief, which is worse.
|
|
121
|
+
- **Decoy rows padding the anonymous block.** Rejected: it spends the count's honesty, and a
|
|
122
|
+
decoy scheme needs some way to distinguish padding from real rows — the exact stable key
|
|
123
|
+
D2a forbids.
|
|
124
|
+
- **Dropping the named list.** Rejected: the map's whole purpose is showing what is being
|
|
125
|
+
built here; a list nobody can read protects a privacy nobody asked for at the cost of the
|
|
126
|
+
feature.
|
|
127
|
+
- **Saying nothing and leaving the copy as it was.** Rejected: the copy would keep implying a
|
|
128
|
+
guarantee the system does not make. That is the failure mode this ADR exists to close.
|
package/docs/adr/README.md
CHANGED
|
@@ -350,3 +350,4 @@ This keeps the decision history honest and traceable.
|
|
|
350
350
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|
|
351
351
|
| 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](<redacted>.md) chose deliberately instead of repairing the door).](<redacted>.md) | hall-ui / admission / front door |
|
|
352
352
|
| 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](<redacted>.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](<redacted>.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](<redacted>.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](<redacted>.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](<redacted>.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](<redacted>.md) | cli / distribution / public surface |
|
|
353
|
+
| 0259 | [**A project’s departure from the public list is public, and the copy says so** ([task 1003672](https://cloudbongos.com/builders#/task/1003672) · goal 1000046 — *Project creation*). Decides the leak [ADR 0255](<redacted>.md) §6.2 deliberately filed rather than implied fixed. The public projects feed publishes a named list plus a count of anonymous rows, so when a project flips to stealth its named row LEAVES the identified block while the count RISES BY ONE — *“Mercury is gone, and there is one more black hole”* names it, and the moment, from a single diff of two snapshots. **Everything [ADR 0182](<redacted>.md) D2a does is defeated by it**, because the identity was never carried ON the anonymous row (`STEALTH_ANONYMOUS_KEEPS` is an allow-list precisely so a column added next year is anonymous by default) — it is carried by the ABSENCE of the named row that used to sit beside it. **Larger than the channel 0255 closed**, which needed repeated sampling and correlation; this one is exact and needs two reads. No `ORDER BY` change touches it: it is inherent in publishing a named list at all, so every fix is a product decision with a visible cost and the owner’s call, not a builder’s. **Decision: accept it, and make the copy say so.** Two reasons it is the right end of the trade and not merely the cheapest — (1) it is ALREADY the honest reading of the existing promise, since D2a made existence and count public *by design*, and a count that is public and truthful is a count whose CHANGES are public; (2) both alternatives damage the control they defend. A **delay window** means the owner clicks the privacy setting and is not private yet *without knowing it*, converting an information leak into a false belief — strictly worse, and the wrong direction for a privacy control. **Decoys** spend the count’s truthfulness (it is truthful today) on a guarantee that is still only probabilistic, and a decoy scheme needs some way to tell padding from real rows — the exact stable per-project key D2a forbids. The promise stealth actually keeps is about the project’s CONTENTS, not the TRANSITION: an anonymous row stays anonymous (no name, tagline, art, origin, or key surviving to the next snapshot); what is not private is stepping off a published list readers may keep copies of. So the copy — which read *“its name, and everything else about it, stay yours”*, every clause true and the paragraph as a whole overselling, because a reader takes it to cover the switch — now names the boundary in BOTH places an owner meets the setting (the choice’s blurb and the what-changes fold), in the plain register 0182’s interview fixed for this card. **A privacy control that oversells is worse than one that admits a limit**, the same standard 0255 §6 set for itself. **No behaviour changes** in `projects-feed.js` — the feed, the redaction allow-list and the keyed rotating permutation are decided FINISHED, not insufficient. `tests/projects_hub.mjs` pins the sentence in both surfaces (the copy IS the fix, so a future edit tightening the blurb must not quietly drop it) and re-applies the no-space-metaphor ban to it. Carries 0255 §6.3’s smaller residual so both live in one place: within one 24h epoch the permutation is stable, so same-day snapshots tell black holes apart (not name them) — accepted on the same reasoning, since the fix is a per-request permutation, which breaks pagination exactly as `ORDER BY random()` was rejected for. Guards recorded for any future attempt: no stable per-project key on an anonymous row, and no anonymous ordering an observer can predict or correlate. Rejected: the delay; decoys; dropping the named list (protects a privacy nobody asked for at the cost of the feature); and saying nothing, which is the failure mode this closes.](<redacted>.md) | platform-identity / privacy boundary / copy honesty |
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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
|
+
**1706 strings** across 3 surfaces · 1475 distinct pieces of copy · 1706 certain, 0 heuristic · 162 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| `builders-hall` | `modules/hall-ui/public` | 78 | 1234 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
|
-
| `landing` | `modules/public-landing/public` | 5 |
|
|
15
|
+
| `landing` | `modules/public-landing/public` | 5 | 372 |
|
|
16
16
|
|
|
17
17
|
## Confidence, and why it is on every row
|
|
18
18
|
|
|
@@ -1422,7 +1422,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1422
1422
|
| `7667eb552781` | certain | {…}: {…}% ({…}/{…} checks) | `modules/status-ui/public/status.js:468` |
|
|
1423
1423
|
| `6d0d64f10bd9` | certain | ‹ earlier page | `modules/status-ui/public/index.html:321` |
|
|
1424
1424
|
|
|
1425
|
-
## `landing` —
|
|
1425
|
+
## `landing` — 372 strings
|
|
1426
1426
|
|
|
1427
1427
|
| id | conf | text | where |
|
|
1428
1428
|
|---|---|---|---|
|
|
@@ -1788,7 +1788,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1788
1788
|
| `5becfcad07c3` | certain | — people ask from the project’s hall, you approve each one from the hall’s Watch page, and then they sign in. | `modules/public-landing/public/projects.html:809` |
|
|
1789
1789
|
| `9ac56aa4e540` | certain | — projects hosted by the platform run on servers rented from DigitalOcean, with their web address and traffic | `modules/public-landing/public/privacy.html:65` |
|
|
1790
1790
|
| `39719e6e1d56` | certain | — shipped as | `modules/public-landing/public/projects.html:778` |
|
|
1791
|
-
| `78a1684630fa` | certain | — the map shows only that something is there; its name, and everything else about it, stay yours, and nobody c | `modules/public-landing/public/projects.html:828` |
|
|
1792
1791
|
| `b5bac008ba11` | certain | — the project is on the map under its name, and anyone who finds it can join. | `modules/public-landing/public/projects.html:828` |
|
|
1793
1792
|
| `728661ec4c4a` | certain | — when a shipped task is graded, the change it made (the code diff) is sent to the model the grader runs on, A | `modules/public-landing/public/privacy.html:64` |
|
|
1794
1793
|
| `ff1318762145` | certain | — whoever signs in with GitHub becomes a member at once, at the newcomer rank. | `modules/public-landing/public/projects.html:809` |
|
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": 1706,
|
|
7
|
+
"unique_ids": 1475,
|
|
8
|
+
"certain": 1706,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 162
|
|
11
11
|
},
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"landing": {
|
|
24
24
|
"at": "modules/public-landing/public",
|
|
25
25
|
"files": 5,
|
|
26
|
-
"strings":
|
|
26
|
+
"strings": 372
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"blind_spots": [
|
|
@@ -16093,15 +16093,6 @@
|
|
|
16093
16093
|
"origin": "html-text",
|
|
16094
16094
|
"confidence": "certain"
|
|
16095
16095
|
},
|
|
16096
|
-
{
|
|
16097
|
-
"id": "78a1684630fa",
|
|
16098
|
-
"surface": "landing",
|
|
16099
|
-
"text": "— the map shows only that something is there; its name, and everything else about it, stay yours, and nobody can ask to join. Whichever you pick, a project reaches the map only once it has a name and a description. This is separate from “Who can see it” above — that one is the door on the project’s own pages. Changing it restarts the project to pick it up.",
|
|
16100
|
-
"file": "modules/public-landing/public/projects.html",
|
|
16101
|
-
"line": 828,
|
|
16102
|
-
"origin": "html-text",
|
|
16103
|
-
"confidence": "certain"
|
|
16104
|
-
},
|
|
16105
16096
|
{
|
|
16106
16097
|
"id": "b5bac008ba11",
|
|
16107
16098
|
"surface": "landing",
|
|
@@ -1603,5 +1603,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1603
1603
|
landed since 1.19.575 with no explicit bump. run 34132732488. (task 1002620)
|
|
1604
1604
|
1.19.577 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1605
1605
|
landed since 1.19.576 with no explicit bump. run 34142767912. (task 1002620)
|
|
1606
|
+
1.19.578 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1607
|
+
landed since 1.19.577 with no explicit bump. run 34146835487. (task 1002620)
|
|
1608
|
+
1.19.579 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1609
|
+
landed since 1.19.578 with no explicit bump. run 34149317186. (task 1002620)
|
|
1606
1610
|
---------------------------------------------------------------------------
|
|
1607
1611
|
```
|
|
@@ -825,7 +825,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
825
825
|
<div id="mDoor"><p class="finePrint">Reading the project’s settings…</p></div>
|
|
826
826
|
<details class="finePrint" id="mDoorWhat">
|
|
827
827
|
<summary>What each choice changes</summary>
|
|
828
|
-
<p><b>Public</b> — the project is on the map under its name, and anyone who finds it can join. <b>Private</b> — it is on the map under its name, but nobody joins on their own: people ask, and you decide. <b>Stealth</b> — the map shows only that something is there; its name, and everything else about it, stay yours, and nobody can ask to join. Whichever you pick, a project reaches the map only once it has a name and a description. This is separate from “Who can see it” above — that one is the door on the project’s own pages. Changing it restarts the project to pick it up.</p>
|
|
828
|
+
<p><b>Public</b> — the project is on the map under its name, and anyone who finds it can join. <b>Private</b> — it is on the map under its name, but nobody joins on their own: people ask, and you decide. <b>Stealth</b> — the map shows only that something is there; its name, and everything else about it, stay yours, and nobody can ask to join. Switching to Stealth is itself visible: someone comparing the map before and after can tell which project went quiet. Whichever you pick, a project reaches the map only once it has a name and a description. This is separate from “Who can see it” above — that one is the door on the project’s own pages. Changing it restarts the project to pick it up.</p>
|
|
829
829
|
</details>
|
|
830
830
|
</div>
|
|
831
831
|
|
|
@@ -5590,6 +5590,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5590
5590
|
three cards below share ONE read of the row (task 1003582), so
|
|
5591
5591
|
dropping the memo here is what keeps each entry a fresh read */
|
|
5592
5592
|
if (!keepRepo) forgetSettings();
|
|
5593
|
+
if (!keepRepo) forgetManifest();
|
|
5593
5594
|
if (!keepRepo) loadVisibility(inst);
|
|
5594
5595
|
if (!keepRepo) loadJoinability(inst);
|
|
5595
5596
|
if (!keepRepo) loadDoor();
|
|
@@ -5892,6 +5893,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5892
5893
|
if (!inst || !$('mVisRuns')) return;
|
|
5893
5894
|
if (visPushOpen(inst)) {
|
|
5894
5895
|
mVisPushSeen = true;
|
|
5896
|
+
forgetManifest(); /* stale from the moment a push is queued — and by the
|
|
5897
|
+
time it settles both cards share one fresh read */
|
|
5895
5898
|
visRuns(inst.open_intent.state === 'running'
|
|
5896
5899
|
? 'Restarting the project to apply a settings change…'
|
|
5897
5900
|
: 'A settings change is queued — the project restarts to pick it up. Until then it keeps its current setting.');
|
|
@@ -5900,43 +5903,81 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5900
5903
|
if (mVisPushSeen) { mVisPushSeen = false; loadVisRuns(inst); }
|
|
5901
5904
|
}
|
|
5902
5905
|
|
|
5906
|
+
/* ── the project's own manifest, read ONCE per manage entry (task 1003554) ─
|
|
5907
|
+
Two cards render off the SAME cross-origin GET of the project's manifest —
|
|
5908
|
+
how the project is reached (brand.project.platformVisibility) and how
|
|
5909
|
+
people join (brand.project.joinability) — and each used to ask for itself:
|
|
5910
|
+
two identical round trips to somebody else's server on every entry.
|
|
5911
|
+
|
|
5912
|
+
Memoised like ensureSettings() and keyed on the project the same way, so a
|
|
5913
|
+
switch is never served the last one's answer. The difference that matters
|
|
5914
|
+
is WHEN it is dropped. The settings row is read once per entry and a memo
|
|
5915
|
+
that lives for that entry is right; this answer instead goes stale the
|
|
5916
|
+
moment a settings push is queued, and the re-read after that push settles
|
|
5917
|
+
is the ONLY thing that can say the new value took. So forgetManifest() is
|
|
5918
|
+
called from the push-open branch of renderVisState/renderJoinState, which
|
|
5919
|
+
runs on every repaint while the push is open and reads nothing itself: by
|
|
5920
|
+
the time it settles the memo is already empty, and the two cards' re-reads
|
|
5921
|
+
then share one read rather than racing two. A lifetime memo here would
|
|
5922
|
+
freeze both lines at their pre-restart value — an efficiency cleanup
|
|
5923
|
+
turned into a wrong answer on screen.
|
|
5924
|
+
|
|
5925
|
+
It RESOLVES with every outcome rather than throwing, so one unreachable
|
|
5926
|
+
project fans out to both lines and neither goes silent: status 0 means it
|
|
5927
|
+
could not be reached at all, any other status is the project's own
|
|
5928
|
+
refusal. The 8s budget and its abort belong to the shared read now — one
|
|
5929
|
+
timer for the pair, not one each. */
|
|
5930
|
+
var manifestPromise = null;
|
|
5931
|
+
var manifestFor = null; /* which project the memo holds — a switch must not reuse it */
|
|
5932
|
+
|
|
5933
|
+
function ensureManifest(domain) {
|
|
5934
|
+
if (manifestPromise && manifestFor === manageId) return manifestPromise;
|
|
5935
|
+
manifestFor = manageId;
|
|
5936
|
+
var ctl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
5937
|
+
var timer = ctl ? setTimeout(function () { ctl.abort(); }, 8000) : null;
|
|
5938
|
+
manifestPromise = fetch('https://' + domain + '/api/gds/instance', { mode: 'cors', signal: ctl ? ctl.signal : undefined })
|
|
5939
|
+
.then(function (res) {
|
|
5940
|
+
/* a project that ANSWERED but refused (its own pre-launch gate, a proxy
|
|
5941
|
+
error page) is not an older core — carry the status, don't guess */
|
|
5942
|
+
if (!res.ok) return { ok: false, status: res.status, data: null };
|
|
5943
|
+
return res.json().then(function (m) { return { ok: true, status: res.status, data: m }; });
|
|
5944
|
+
})
|
|
5945
|
+
.catch(function () { return { ok: false, status: 0, data: null }; })
|
|
5946
|
+
.then(function (ans) { if (timer) clearTimeout(timer); return ans; });
|
|
5947
|
+
return manifestPromise;
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
/* Drop the memo so the next ensureManifest() is a real read — at each manage
|
|
5951
|
+
entry, and for the whole time a settings push is open. */
|
|
5952
|
+
function forgetManifest() { manifestPromise = null; manifestFor = null; }
|
|
5953
|
+
|
|
5903
5954
|
/* what the project RUNS: its own public manifest, read cross-origin (the
|
|
5904
5955
|
same GET /instance the platform's runner reads after a restart) */
|
|
5905
5956
|
function loadVisRuns(inst) {
|
|
5906
5957
|
if (!inst || inst.status !== 'active' || !inst.domain) { visRuns(''); return; }
|
|
5907
5958
|
var asked = manageId;
|
|
5908
5959
|
visRuns('Checking what the project is running…');
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
error page) is not an older core — say what happened, not a guess */
|
|
5915
|
-
if (!res.ok) throw new Error('http:' + res.status);
|
|
5916
|
-
return res.json();
|
|
5917
|
-
})
|
|
5918
|
-
.then(function (m) {
|
|
5919
|
-
if (asked !== manageId) return;
|
|
5920
|
-
var p = m && m.brand && m.brand.project;
|
|
5921
|
-
var v = p && p.platformVisibility;
|
|
5922
|
-
if (!p || typeof p !== 'object') {
|
|
5923
|
-
visRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
|
|
5924
|
-
return;
|
|
5925
|
-
}
|
|
5926
|
-
var o = VIS_OPTIONS[v];
|
|
5927
|
-
if (!o) { visRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
|
|
5928
|
-
var want = mVisSettings && mVisSettings.value;
|
|
5929
|
-
visRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
|
|
5930
|
-
(want && want !== v && VIS_OPTIONS[want] ? ' — the saved choice (' + esc(VIS_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
|
|
5931
|
-
})
|
|
5932
|
-
.catch(function (err) {
|
|
5933
|
-
if (asked !== manageId) return;
|
|
5934
|
-
var http = /^http:(\d+)$/.exec(String(err && err.message));
|
|
5935
|
-
visRuns(http
|
|
5936
|
-
? 'The project answered but didn’t share what it’s running (it replied ' + esc(http[1]) + ').'
|
|
5960
|
+
ensureManifest(inst.domain).then(function (ans) {
|
|
5961
|
+
if (asked !== manageId) return;
|
|
5962
|
+
if (!ans.ok) {
|
|
5963
|
+
visRuns(ans.status
|
|
5964
|
+
? 'The project answered but didn’t share what it’s running (it replied ' + esc(String(ans.status)) + ').'
|
|
5937
5965
|
: 'Couldn’t reach the project to check what it’s running right now.');
|
|
5938
|
-
|
|
5939
|
-
|
|
5966
|
+
return;
|
|
5967
|
+
}
|
|
5968
|
+
var m = ans.data;
|
|
5969
|
+
var p = m && m.brand && m.brand.project;
|
|
5970
|
+
var v = p && p.platformVisibility;
|
|
5971
|
+
if (!p || typeof p !== 'object') {
|
|
5972
|
+
visRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
|
|
5973
|
+
return;
|
|
5974
|
+
}
|
|
5975
|
+
var o = VIS_OPTIONS[v];
|
|
5976
|
+
if (!o) { visRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
|
|
5977
|
+
var want = mVisSettings && mVisSettings.value;
|
|
5978
|
+
visRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
|
|
5979
|
+
(want && want !== v && VIS_OPTIONS[want] ? ' — the saved choice (' + esc(VIS_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
|
|
5980
|
+
});
|
|
5940
5981
|
}
|
|
5941
5982
|
|
|
5942
5983
|
/* ── the settings row, read ONCE per manage entry (task 1003582) ─────────
|
|
@@ -6305,6 +6346,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6305
6346
|
if (!inst || !$('mJoinRuns')) return;
|
|
6306
6347
|
if (visPushOpen(inst)) {
|
|
6307
6348
|
mJoinPushSeen = true;
|
|
6349
|
+
forgetManifest(); /* the same one settle, the same one drop */
|
|
6308
6350
|
joinRuns(inst.open_intent.state === 'running'
|
|
6309
6351
|
? 'Restarting the project to apply a settings change…'
|
|
6310
6352
|
: 'A settings change is queued — the project restarts to pick it up. Until then it keeps its current setting.');
|
|
@@ -6313,40 +6355,33 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6313
6355
|
if (mJoinPushSeen) { mJoinPushSeen = false; loadJoinRuns(inst); }
|
|
6314
6356
|
}
|
|
6315
6357
|
|
|
6316
|
-
/* what the project RUNS:
|
|
6358
|
+
/* what the project RUNS: the SAME shared manifest read the reach card uses
|
|
6359
|
+
(task 1003554) — a different key out of one answer, not a second GET */
|
|
6317
6360
|
function loadJoinRuns(inst) {
|
|
6318
6361
|
if (!inst || inst.status !== 'active' || !inst.domain) { joinRuns(''); return; }
|
|
6319
6362
|
var asked = manageId;
|
|
6320
6363
|
joinRuns('Checking what the project is running…');
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
return res.json();
|
|
6327
|
-
})
|
|
6328
|
-
.then(function (m) {
|
|
6329
|
-
if (asked !== manageId) return;
|
|
6330
|
-
var p = m && m.brand && m.brand.project;
|
|
6331
|
-
var v = p && p.joinability;
|
|
6332
|
-
if (!p || typeof p !== 'object') {
|
|
6333
|
-
joinRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
|
|
6334
|
-
return;
|
|
6335
|
-
}
|
|
6336
|
-
var o = JOIN_OPTIONS[v];
|
|
6337
|
-
if (!o) { joinRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
|
|
6338
|
-
var want = mJoinSettings && mJoinSettings.value;
|
|
6339
|
-
joinRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
|
|
6340
|
-
(want && want !== v && JOIN_OPTIONS[want] ? ' — the saved choice (' + esc(JOIN_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
|
|
6341
|
-
})
|
|
6342
|
-
.catch(function (err) {
|
|
6343
|
-
if (asked !== manageId) return;
|
|
6344
|
-
var http = /^http:(\d+)$/.exec(String(err && err.message));
|
|
6345
|
-
joinRuns(http
|
|
6346
|
-
? 'The project answered but didn’t share what it’s running (it replied ' + esc(http[1]) + ').'
|
|
6364
|
+
ensureManifest(inst.domain).then(function (ans) {
|
|
6365
|
+
if (asked !== manageId) return;
|
|
6366
|
+
if (!ans.ok) {
|
|
6367
|
+
joinRuns(ans.status
|
|
6368
|
+
? 'The project answered but didn’t share what it’s running (it replied ' + esc(String(ans.status)) + ').'
|
|
6347
6369
|
: 'Couldn’t reach the project to check what it’s running right now.');
|
|
6348
|
-
|
|
6349
|
-
|
|
6370
|
+
return;
|
|
6371
|
+
}
|
|
6372
|
+
var m = ans.data;
|
|
6373
|
+
var p = m && m.brand && m.brand.project;
|
|
6374
|
+
var v = p && p.joinability;
|
|
6375
|
+
if (!p || typeof p !== 'object') {
|
|
6376
|
+
joinRuns('This project’s server doesn’t report this setting yet — it runs an older platform version, so the setting can’t take effect until the project is updated.');
|
|
6377
|
+
return;
|
|
6378
|
+
}
|
|
6379
|
+
var o = JOIN_OPTIONS[v];
|
|
6380
|
+
if (!o) { joinRuns('This project’s server reports a setting this page doesn’t recognise (' + esc(String(v)) + ').'); return; }
|
|
6381
|
+
var want = mJoinSettings && mJoinSettings.value;
|
|
6382
|
+
joinRuns('Right now the project runs as <b>' + esc(o.label) + '</b>' +
|
|
6383
|
+
(want && want !== v && JOIN_OPTIONS[want] ? ' — the saved choice (' + esc(JOIN_OPTIONS[want].label) + ') hasn’t taken effect there yet.' : '.'));
|
|
6384
|
+
});
|
|
6350
6385
|
}
|
|
6351
6386
|
|
|
6352
6387
|
function loadJoinability(inst) {
|
|
@@ -6554,7 +6589,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6554
6589
|
var DOOR_OPTIONS = {
|
|
6555
6590
|
public: { label: 'Public', blurb: 'The project is on the map under its name, and anyone who finds it can join.' },
|
|
6556
6591
|
private: { label: 'Private', blurb: 'On the map under its name, but nobody joins on their own — people ask, and you decide.' },
|
|
6557
|
-
stealth: { label: 'Stealth', blurb: 'The map shows only that something is there. Its name, and everything else about it, stay yours — and nobody can ask to join.' }
|
|
6592
|
+
stealth: { label: 'Stealth', blurb: 'The map shows only that something is there. Its name, and everything else about it, stay yours — and nobody can ask to join. Switching to Stealth is itself visible: someone comparing the map before and after can tell which project went quiet.' }
|
|
6558
6593
|
};
|
|
6559
6594
|
var mDoorSettings = null; /* the platform row's last answer: { value, options } */
|
|
6560
6595
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.579",
|
|
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.579",
|
|
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.579",
|
|
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.
|
|
58
|
+
const CORE_VERSION = '1.19.579'; // 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,205 @@
|
|
|
1
|
+
// tests/manage_manifest_shared_read.mjs — one read of the project's OWN
|
|
2
|
+
// manifest per manage entry, EXECUTED (task 1003554).
|
|
3
|
+
//
|
|
4
|
+
// The defect this pins shut: entering /projects?view=manage fetched
|
|
5
|
+
// https://<project>/api/gds/instance twice, identically and cross-origin,
|
|
6
|
+
// because the two cards that render a "what it runs right now" line — how the
|
|
7
|
+
// project is reached (brand.project.platformVisibility) and how people join
|
|
8
|
+
// (brand.project.joinability) — each asked the project for themselves. Two
|
|
9
|
+
// round trips to somebody else's server, each with its own 8s budget, for one
|
|
10
|
+
// answer.
|
|
11
|
+
//
|
|
12
|
+
// It is the sibling of tests/manage_settings_shared_read.mjs, and the reason it
|
|
13
|
+
// is a separate reader rather than the same memo: the settings row is read once
|
|
14
|
+
// per entry and never again, so a memo that lives for the entry is right. This
|
|
15
|
+
// answer goes stale the moment a settings push is queued, and the re-read once
|
|
16
|
+
// that push settles is the ONLY thing that can say the new value took. So the
|
|
17
|
+
// memo is dropped for as long as the push is open. A lifetime memo here would
|
|
18
|
+
// freeze both lines at their pre-restart value — an efficiency cleanup turned
|
|
19
|
+
// into a wrong answer on screen.
|
|
20
|
+
//
|
|
21
|
+
// tests/projects_hub.mjs pins that both cards go through the shared reader;
|
|
22
|
+
// this file lifts the reader out of modules/public-landing/public/projects.html,
|
|
23
|
+
// runs it in a vm under a scripted fetch, and counts the round trips. The
|
|
24
|
+
// vm-harness precedent is tests/project_door_ui.mjs.
|
|
25
|
+
//
|
|
26
|
+
// What it proves, and why each matters:
|
|
27
|
+
// • both cards of one entry cost ONE round trip — the defect itself
|
|
28
|
+
// • forgetManifest() makes the next read real, so entry and tab return each
|
|
29
|
+
// ask the project as it is now, never as it was last visit
|
|
30
|
+
// • the push-open drop is what makes the settle re-read real, and the two
|
|
31
|
+
// cards re-reading after one settle still cost one round trip
|
|
32
|
+
// • switching projects is never served the previous project's manifest
|
|
33
|
+
// • a refusal RESOLVES carrying its status and an unreachable project
|
|
34
|
+
// resolves with status 0 — so ONE failure reaches BOTH lines and neither
|
|
35
|
+
// card goes silent waiting on a rejection nobody handed it
|
|
36
|
+
// • the 8s abort budget is the shared read's, and it is cleared once
|
|
37
|
+
//
|
|
38
|
+
// Run: node --test --test-reporter=tap tests/manage_manifest_shared_read.mjs
|
|
39
|
+
|
|
40
|
+
import assert from 'node:assert/strict';
|
|
41
|
+
import { test } from 'node:test';
|
|
42
|
+
import fs from 'node:fs';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import vm from 'node:vm';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
|
|
47
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
48
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
|
|
49
|
+
const SCRIPT = (HUB.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/) || [, ''])[1];
|
|
50
|
+
|
|
51
|
+
// The reader, sliced from the page verbatim. Both boundaries are asserted so a
|
|
52
|
+
// rename cannot leave this file executing an empty string and passing.
|
|
53
|
+
const START = SCRIPT.indexOf(' var manifestPromise = null;');
|
|
54
|
+
const END = SCRIPT.indexOf(' function loadVisRuns(');
|
|
55
|
+
assert.ok(START > 0, 'the shared reader must start at its memo declaration');
|
|
56
|
+
assert.ok(END > START, 'the shared reader must sit above its first consumer');
|
|
57
|
+
const REGION = SCRIPT.slice(START, END);
|
|
58
|
+
for (const fn of ['function ensureManifest(', 'function forgetManifest(']) {
|
|
59
|
+
assert.ok(REGION.includes(fn), `the region must carry ${fn}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── the harness ──────────────────────────────────────────────────────────────
|
|
63
|
+
// Narrow on purpose: the region touches fetch, manageId and the abort timer,
|
|
64
|
+
// and nothing else — so the stub stays honest about what it can prove.
|
|
65
|
+
|
|
66
|
+
function boot({ ok = true, status = 200, body = { brand: { project: { platformVisibility: 'public', joinability: 'open' } } }, reject = false } = {}) {
|
|
67
|
+
const requests = [];
|
|
68
|
+
const timers = { set: 0, cleared: 0 };
|
|
69
|
+
const sandbox = {
|
|
70
|
+
Promise, JSON, String, Object, Error, AbortController,
|
|
71
|
+
manageId: '18',
|
|
72
|
+
setTimeout: (fn, ms) => { timers.set++; timers.ms = ms; return { fn }; },
|
|
73
|
+
clearTimeout: () => { timers.cleared++; },
|
|
74
|
+
fetch: (url, init = {}) => {
|
|
75
|
+
requests.push({ url, mode: init.mode });
|
|
76
|
+
return reject
|
|
77
|
+
? Promise.reject(new Error('network'))
|
|
78
|
+
: Promise.resolve({ ok, status, json: () => Promise.resolve(body) });
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
sandbox.globalThis = sandbox;
|
|
82
|
+
vm.createContext(sandbox);
|
|
83
|
+
vm.runInContext(REGION, sandbox, { filename: 'projects.html#shared-manifest-read' });
|
|
84
|
+
|
|
85
|
+
return { requests, timers, sandbox };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── the tests ────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
test('the two cards of one manage entry cost ONE round trip, not two', async () => {
|
|
91
|
+
// Arrange — the page's own entry pass: the reach card, then the join card
|
|
92
|
+
const ui = boot();
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const answers = await Promise.all([
|
|
96
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
97
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
assert.equal(ui.requests.length, 1, 'one cross-origin GET served both lines');
|
|
102
|
+
assert.equal(ui.requests[0].url, 'https://demo.example.com/api/gds/instance');
|
|
103
|
+
assert.equal(ui.requests[0].mode, 'cors', 'the project is somebody else’s origin');
|
|
104
|
+
for (const a of answers) {
|
|
105
|
+
assert.equal(a.ok, true);
|
|
106
|
+
assert.equal(a.data.brand.project.joinability, 'open', 'both callers got the same manifest');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('forgetManifest makes the next read real — entry and tab return never reuse the last visit’s answer', async () => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const ui = boot();
|
|
113
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
114
|
+
|
|
115
|
+
// Act — what loadManage does at the top of every entry + tab return pass
|
|
116
|
+
ui.sandbox.forgetManifest();
|
|
117
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
assert.equal(ui.requests.length, 2, 'the second entry asked the project again');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a settled push re-reads for real, and the two cards still share ONE round trip', async () => {
|
|
124
|
+
// Arrange — the reason this memo is not ensureSettings()'s: an answer read
|
|
125
|
+
// before a push cannot say whether the restart took
|
|
126
|
+
const ui = boot();
|
|
127
|
+
await ui.sandbox.ensureManifest('demo.example.com');
|
|
128
|
+
|
|
129
|
+
// Act — the push opens (renderVisState/renderJoinState each drop the memo on
|
|
130
|
+
// every repaint while it is open), then it settles and both cards re-read
|
|
131
|
+
ui.sandbox.forgetManifest();
|
|
132
|
+
ui.sandbox.forgetManifest();
|
|
133
|
+
const after = await Promise.all([
|
|
134
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
135
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
assert.equal(ui.requests.length, 2, 'one re-read after the settle — not one per card, and not zero');
|
|
140
|
+
assert.equal(after[0].ok, true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('switching projects is never served the previous project’s manifest', async () => {
|
|
144
|
+
// Arrange — the memo is warm for project 18
|
|
145
|
+
const ui = boot();
|
|
146
|
+
await ui.sandbox.ensureManifest('one.example.com');
|
|
147
|
+
|
|
148
|
+
// Act — the owner opens a different project; loadManage has not forgotten yet
|
|
149
|
+
ui.sandbox.manageId = '19';
|
|
150
|
+
await ui.sandbox.ensureManifest('two.example.com');
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
assert.equal(ui.requests.length, 2, 'the memo is keyed on the project, not merely present');
|
|
154
|
+
assert.equal(ui.requests[1].url, 'https://two.example.com/api/gds/instance');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('a project that answered-and-refused RESOLVES carrying its status — both lines can say so', async () => {
|
|
158
|
+
// Arrange — a pre-launch gate or a proxy error page, which is NOT an older core
|
|
159
|
+
const ui = boot({ ok: false, status: 403 });
|
|
160
|
+
|
|
161
|
+
// Act
|
|
162
|
+
const [a, b] = await Promise.all([
|
|
163
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
164
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
assert.equal(a.ok, false);
|
|
169
|
+
assert.equal(a.status, 403, 'the status reaches the card, which prints it verbatim');
|
|
170
|
+
assert.equal(a.data, null, 'no body is claimed for a refusal');
|
|
171
|
+
assert.deepEqual(b, a, 'the second card is handed the same refusal, not left waiting');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('an unreachable project RESOLVES with status 0, so one failure fans out to both lines', async () => {
|
|
175
|
+
// Arrange — the failure that used to reject into two separate catches
|
|
176
|
+
const ui = boot({ reject: true });
|
|
177
|
+
|
|
178
|
+
// Act
|
|
179
|
+
const [a, b] = await Promise.all([
|
|
180
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
181
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Assert
|
|
185
|
+
assert.equal(a.ok, false);
|
|
186
|
+
assert.equal(a.status, 0, 'status 0 is "could not reach it at all" — a different sentence from a refusal');
|
|
187
|
+
assert.deepEqual(b, a, 'neither line goes silent on a shared rejection');
|
|
188
|
+
assert.equal(ui.requests.length, 1, 'one failed attempt, not one per card');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the 8s abort budget belongs to the shared read, and is cleared once', async () => {
|
|
192
|
+
// Arrange
|
|
193
|
+
const ui = boot();
|
|
194
|
+
|
|
195
|
+
// Act
|
|
196
|
+
await Promise.all([
|
|
197
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
198
|
+
ui.sandbox.ensureManifest('demo.example.com'),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Assert
|
|
202
|
+
assert.equal(ui.timers.set, 1, 'one timer for the pair, not one each');
|
|
203
|
+
assert.equal(ui.timers.ms, 8000, 'the budget the two cards used to keep separately');
|
|
204
|
+
assert.equal(ui.timers.cleared, 1, 'and it is cleared when the answer lands');
|
|
205
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1418,14 +1418,17 @@ test('the save sends the choice and shows the platform’s receipt verbatim —
|
|
|
1418
1418
|
|
|
1419
1419
|
test('what the project RUNS comes only from its own manifest, and every answer is said', () => {
|
|
1420
1420
|
const runs = fnHub('loadVisRuns');
|
|
1421
|
+
const manifest = fnHub('ensureManifest');
|
|
1421
1422
|
assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
|
|
1422
|
-
assert.match(
|
|
1423
|
+
assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/gds\/instance'/,
|
|
1423
1424
|
'the same public GET /instance the platform’s runner reads after a restart');
|
|
1425
|
+
assert.doesNotMatch(runs, /fetch\(/, 'the card fires no GET of its own — one manifest read serves the pair (task 1003554)');
|
|
1424
1426
|
assert.match(runs, /m\.brand && m\.brand\.project/);
|
|
1425
1427
|
assert.match(runs, /doesn’t report this setting yet/, 'an older core is named, not shown as applied');
|
|
1426
1428
|
assert.match(runs, /hasn’t taken effect there yet/, 'a saved-but-not-applied value is said');
|
|
1427
1429
|
assert.match(runs, /Couldn’t reach the project/, 'an unreachable project is said');
|
|
1428
|
-
assert.match(
|
|
1430
|
+
assert.match(manifest, /if \(!res\.ok\) return \{ ok: false, status: res\.status, data: null \};/,
|
|
1431
|
+
'a refusal is not an older core — its status is carried out to both cards, not thrown away');
|
|
1429
1432
|
assert.match(runs, /answered but didn’t share what it’s running/, 'a project that answered-and-refused is said as such');
|
|
1430
1433
|
assert.doesNotMatch(runs, /mVisSettings\.value\)\s*\+\s*'<\/b>'|runs as <b>' \+ esc\(VIS_OPTIONS\[want\]/,
|
|
1431
1434
|
'the platform’s stored choice is never painted as what the project runs');
|
|
@@ -1540,6 +1543,30 @@ test('the join-door labels are PLAIN — the space metaphor is the orb’s, neve
|
|
|
1540
1543
|
assert.doesNotMatch(options, /unlisted/i, 'dark matter is DERIVED — there is deliberately no way to choose it');
|
|
1541
1544
|
});
|
|
1542
1545
|
|
|
1546
|
+
/* ADR 0259: the one thing Stealth does NOT hide is the switch itself. A project
|
|
1547
|
+
leaving the named list while the anonymous count rises names it to anyone
|
|
1548
|
+
holding yesterday's feed — inherent in publishing a named list, and the owner
|
|
1549
|
+
chose to accept it and say so rather than pay for a delay or decoys. The
|
|
1550
|
+
copy is the whole fix, so the copy is what gets pinned: without this, a
|
|
1551
|
+
future edit tightening the blurb drops the caveat and the page goes back to
|
|
1552
|
+
implying a guarantee the feed does not make. */
|
|
1553
|
+
test('the Stealth copy names its own limit — switching is visible, even though the project is not (ADR 0259)', () => {
|
|
1554
|
+
const options = (SCRIPT.match(/var DOOR_OPTIONS = \{[\s\S]*?\n \};/) || [''])[0];
|
|
1555
|
+
const fold = (HUB.match(/<details class="finePrint" id="mDoorWhat">[\s\S]*?<\/details>/) || [''])[0];
|
|
1556
|
+
assert.ok(options && fold, 'both the option table and the fold must be extractable');
|
|
1557
|
+
for (const [where, text] of [['the choice’s blurb', options], ['the what-changes fold', fold]]) {
|
|
1558
|
+
assert.match(text, /Switching to Stealth is itself visible/,
|
|
1559
|
+
`${where} states the boundary — a privacy control that oversells is worse than one that admits a limit`);
|
|
1560
|
+
assert.match(text, /comparing the map before and after can tell which project went quiet/,
|
|
1561
|
+
`${where} says HOW it is visible, so the owner can judge it rather than take our word`);
|
|
1562
|
+
}
|
|
1563
|
+
/* the plain-register rule this card is held to still binds the new sentence
|
|
1564
|
+
(the space metaphor is the drawn orb's, never the copy's) */
|
|
1565
|
+
for (const word of ['planet', 'star', 'black hole', 'dark matter', 'feed', 'snapshot']) {
|
|
1566
|
+
assert.doesNotMatch(options.toLowerCase(), new RegExp(`\\b${word}\\b`), `"${word}" must not reach a choice’s blurb`);
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1543
1570
|
test('the join-door pick is a real radio group built from the server’s own options list', () => {
|
|
1544
1571
|
const render = fnHub('renderDoor');
|
|
1545
1572
|
assert.match(render, /\(s\.options \|\| \[\]\)\.filter\(function \(v\) \{ return !!DOOR_OPTIONS\[v\]; \}\)/,
|
|
@@ -1598,15 +1625,39 @@ test('the joinability save sends the one key and shows the platform’s receipt
|
|
|
1598
1625
|
test('what the project RUNS for joinability comes only from its own manifest, and every answer is said', () => {
|
|
1599
1626
|
const runs = fnHub('loadJoinRuns');
|
|
1600
1627
|
assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
|
|
1601
|
-
assert.match(runs, /
|
|
1628
|
+
assert.match(runs, /ensureManifest\(inst\.domain\)/, 'the same public GET /instance the runner reads, through the shared reader');
|
|
1629
|
+
assert.doesNotMatch(runs, /fetch\(/, 'the second card of the pair adds no round trip of its own (task 1003554)');
|
|
1602
1630
|
assert.match(runs, /p && p\.joinability/, 'the manifest key is brand.project.joinability');
|
|
1603
1631
|
assert.match(runs, /doesn’t report this setting yet/, 'an older core is named, not shown as applied');
|
|
1604
1632
|
assert.match(runs, /hasn’t taken effect there yet/, 'a saved-but-not-applied value is said');
|
|
1605
1633
|
assert.match(runs, /Couldn’t reach the project/, 'an unreachable project is said');
|
|
1606
|
-
assert.match(runs, /if \(!res\.ok\) throw new Error\('http:' \+ res\.status\)/, 'a refusal is not an older core');
|
|
1607
1634
|
assert.match(runs, /answered but didn’t share what it’s running/, 'a project that answered-and-refused is said as such');
|
|
1608
1635
|
});
|
|
1609
1636
|
|
|
1637
|
+
/* One read of the project's own manifest per manage entry (task 1003554), the
|
|
1638
|
+
sibling of the settings-row share above. Two cards read two keys out of the
|
|
1639
|
+
SAME cross-origin GET and each used to fetch it for itself. The memo differs
|
|
1640
|
+
from ensureSettings() in one load-bearing way — it is dropped for as long as
|
|
1641
|
+
a push is open, because the re-read after it settles is the only thing that
|
|
1642
|
+
can say the new value took. tests/manage_manifest_shared_read.mjs runs the
|
|
1643
|
+
reader; this pins that both cards go through it and that the drop is there. */
|
|
1644
|
+
test('the two “what it runs” lines share ONE read of the project’s manifest', () => {
|
|
1645
|
+
const ensure = fnHub('ensureManifest');
|
|
1646
|
+
assert.match(ensure, /if \(manifestPromise && manifestFor === manageId\) return manifestPromise;/,
|
|
1647
|
+
'memoised AND keyed on the project — a switch must never be served the last one’s manifest');
|
|
1648
|
+
assert.match(ensure, /setTimeout\(function \(\) \{ ctl\.abort\(\); \}, 8000\)/, 'one 8s budget for the pair, not one each');
|
|
1649
|
+
for (const fn of ['loadVisRuns', 'loadJoinRuns']) {
|
|
1650
|
+
assert.match(fnHub(fn), /ensureManifest\(inst\.domain\)/, `${fn} reads the manifest through the shared reader`);
|
|
1651
|
+
assert.doesNotMatch(fnHub(fn), /fetch\(/, `${fn} fires no GET of its own`);
|
|
1652
|
+
}
|
|
1653
|
+
assert.match(SCRIPT, /if \(!keepRepo\) forgetManifest\(\);/,
|
|
1654
|
+
'the memo is dropped on entry + tab return, so neither is ever served the last visit’s answer');
|
|
1655
|
+
for (const fn of ['renderVisState', 'renderJoinState']) {
|
|
1656
|
+
assert.match(fnHub(fn), /forgetManifest\(\);/,
|
|
1657
|
+
`${fn} drops the memo while a push is open — a lifetime memo would freeze the line at its pre-restart value`);
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1610
1661
|
test('the joinability in-flight line shares the visibility section’s push predicate and re-asks the manifest when it settles', () => {
|
|
1611
1662
|
const st = fnHub('renderJoinState');
|
|
1612
1663
|
assert.match(st, /if \(visPushOpen\(inst\)\)/, 'one settings push carries every key — one predicate, shared');
|