@bongos/core 1.19.578 → 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 +22 -17
- 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 +2 -0
- package/modules/public-landing/public/projects.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +24 -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.579",
|
|
6
|
+
"core_contract": "1.19.579",
|
|
7
|
+
"source_commit": "8b206e9f473df316fc1981854a479d1ba509d76e",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-07T17:
|
|
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
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",
|
|
@@ -11647,7 +11652,7 @@
|
|
|
11647
11652
|
{
|
|
11648
11653
|
"path": "tests/projects_hub.mjs",
|
|
11649
11654
|
"mode": "0000644",
|
|
11650
|
-
"sha256": "
|
|
11655
|
+
"sha256": "d32fd31e368acdaaf1d4f31b775f62ef8e20ea4f5810a37fddb8ef1dba1487b6"
|
|
11651
11656
|
},
|
|
11652
11657
|
{
|
|
11653
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",
|
|
@@ -1605,5 +1605,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1605
1605
|
landed since 1.19.576 with no explicit bump. run 34142767912. (task 1002620)
|
|
1606
1606
|
1.19.578 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1607
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)
|
|
1608
1610
|
---------------------------------------------------------------------------
|
|
1609
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
|
|
|
@@ -6589,7 +6589,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6589
6589
|
var DOOR_OPTIONS = {
|
|
6590
6590
|
public: { label: 'Public', blurb: 'The project is on the map under its name, and anyone who finds it can join.' },
|
|
6591
6591
|
private: { label: 'Private', blurb: 'On the map under its name, but nobody joins on their own — people ask, and you decide.' },
|
|
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.' }
|
|
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.' }
|
|
6593
6593
|
};
|
|
6594
6594
|
var mDoorSettings = null; /* the platform row's last answer: { value, options } */
|
|
6595
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');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1543,6 +1543,30 @@ test('the join-door labels are PLAIN — the space metaphor is the orb’s, neve
|
|
|
1543
1543
|
assert.doesNotMatch(options, /unlisted/i, 'dark matter is DERIVED — there is deliberately no way to choose it');
|
|
1544
1544
|
});
|
|
1545
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
|
+
|
|
1546
1570
|
test('the join-door pick is a real radio group built from the server’s own options list', () => {
|
|
1547
1571
|
const render = fnHub('renderDoor');
|
|
1548
1572
|
assert.match(render, /\(s\.options \|\| \[\]\)\.filter\(function \(v\) \{ return !!DOOR_OPTIONS\[v\]; \}\)/,
|