@bongos/core 1.20.55 → 1.20.56
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 +76 -36
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +54 -3
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +45 -41
- package/docs/copy-registry.json +83 -47
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +13 -5
- package/docs/page-readings.json +102 -103
- package/modules/hall-ui/public/founding-mark.js +67 -0
- package/modules/hall-ui/public/hall-widgets.js +31 -0
- package/modules/hall-ui/public/index-founding.states.json +13 -0
- package/modules/hall-ui/public/index.html +2 -1
- package/modules/hall-ui/public/profile.html +2 -1
- package/modules/hall-ui/public/profile.js +18 -0
- package/modules/hall-ui/public/ranks-founding.states.json +12 -0
- package/modules/hall-ui/public/ranks.html +11 -1
- package/modules/hall-ui/public/ranks.js +25 -0
- package/modules/hall-ui/public/roster.html +1 -0
- package/modules/hall-ui/public/roster.js +18 -2
- package/modules/hall-ui/public/style.css +35 -0
- package/modules/hall-ui/records/founding-mark.md +17 -0
- package/modules/onboarding/founding-builders.js +127 -0
- package/modules/onboarding/founding-mode.js +27 -3
- package/modules/onboarding/routes/onboarding.js +11 -0
- package/modules/ui-design/kit/fixtures/founding-builders-sample.json +9 -0
- package/modules/ui-design/kit/fixtures/founding-builders.json +4 -0
- package/modules/ui-design/kit/serve.js +4 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/founding_builders.mjs +263 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// founding-mark.js — THE FOUNDING MARK in the hall (task 1004424 / BV2.PS10).
|
|
2
|
+
//
|
|
3
|
+
// Everyone who joined while the project was being founded carries a seed from its
|
|
4
|
+
// genesis, numbered in join order; the project owner is "founder". The server works
|
|
5
|
+
// that out (modules/onboarding/founding-builders.js, GET /api/bongos/founding/builders);
|
|
6
|
+
// this file only shows it, in the hall's own badge grammar: a pill the height and type
|
|
7
|
+
// of .rank-badge, with a seed dot in the accent. Four surfaces read it — the roster,
|
|
8
|
+
// the profile, the ranks page and the founding wall on Home — and each one renders
|
|
9
|
+
// exactly as before when the list is empty, which it is on every project that was
|
|
10
|
+
// never founded (the blend rule: founding is a mode the hall wears, never a re-skin).
|
|
11
|
+
//
|
|
12
|
+
// It is a mark, not a rank: nothing here gates anything.
|
|
13
|
+
(function () {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
var STAGE_NAME = {
|
|
17
|
+
decide: 'decide', scope: 'scope', strategize: 'strategize',
|
|
18
|
+
charter: 'charter', legalize: 'legalize', legislation: 'legislation',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function esc(s) {
|
|
22
|
+
if (window.OTB && typeof window.OTB.escapeHtml === 'function') return window.OTB.escapeHtml(s);
|
|
23
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
24
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// One request per page, shared by every surface on it. Any failure (signed out, the
|
|
29
|
+
// onboarding module off, a network blip) is an empty list: no marks, today's page.
|
|
30
|
+
var pending = null;
|
|
31
|
+
function load() {
|
|
32
|
+
if (pending) return pending;
|
|
33
|
+
pending = fetch('/api/bongos/founding/builders', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
34
|
+
.then(function (r) { return r.ok ? r.json() : { builders: [] }; })
|
|
35
|
+
.then(function (d) { return Array.isArray(d && d.builders) ? d.builders : []; })
|
|
36
|
+
.catch(function () { return []; });
|
|
37
|
+
return pending;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// byId(list) — { builderId: mark } for the per-row surfaces.
|
|
41
|
+
function byId(list) {
|
|
42
|
+
var out = {};
|
|
43
|
+
(list || []).forEach(function (m) { if (m && m.builder_id != null) out[String(m.builder_id)] = m; });
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// "founder" | "founding · #2" — the words on the pill.
|
|
48
|
+
function label(m) {
|
|
49
|
+
return m.founder ? 'founder' : 'founding · #' + m.number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// "Founded the project" | "Joined at scope, day 3 of genesis" — the plain sentence.
|
|
53
|
+
function joinedLine(m) {
|
|
54
|
+
if (m.founder) return 'Founded the project';
|
|
55
|
+
var at = 'Joined at ' + (STAGE_NAME[m.stage] || m.stage || 'genesis');
|
|
56
|
+
return m.day ? at + ', day ' + m.day + ' of genesis' : at + ' during genesis';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function chipHtml(m) {
|
|
60
|
+
if (!m) return '';
|
|
61
|
+
var title = (m.founder ? 'Founder' : 'Founding builder #' + m.number) + ' — ' + joinedLine(m) + '. A mark for good; it grants nothing.';
|
|
62
|
+
return '<span class="founding-mark" title="' + esc(title) + '">'
|
|
63
|
+
+ '<span class="founding-mark__seed" aria-hidden="true"></span>' + esc(label(m)) + '</span>';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
window.OTBFounding = { load: load, byId: byId, label: label, joinedLine: joinedLine, chipHtml: chipHtml };
|
|
67
|
+
}());
|
|
@@ -110,6 +110,37 @@
|
|
|
110
110
|
'<p class="card-foot"><a href="#standing">All your work →</a></p>',
|
|
111
111
|
{ view: 'home', zone: 'home-main', labelledby: 'home-recent-h', hiddenByDefault: true });
|
|
112
112
|
|
|
113
|
+
// 0f. The founding wall (task 1004424) — everyone who joined while the project was
|
|
114
|
+
// being founded, founder first, each with their mark. Hidden until the founding
|
|
115
|
+
// list comes back non-empty, so a project that was never founded keeps today's Home.
|
|
116
|
+
// It fills itself rather than going through builders.js: the list is one read the
|
|
117
|
+
// other founding surfaces share (founding-mark.js), and nothing else on Home needs it.
|
|
118
|
+
reg({
|
|
119
|
+
id: 'home-founding', label: 'Founding builders', order: 30, core: true,
|
|
120
|
+
view: 'home', zone: 'home-main', labelledby: 'home-founding-h', hiddenByDefault: true,
|
|
121
|
+
mount(host) {
|
|
122
|
+
host.innerHTML =
|
|
123
|
+
'<h2 id="home-founding-h" class="scroll__h">The founding wall</h2>' +
|
|
124
|
+
'<p class="scroll__lede">Everyone who joined while this project was being founded, in the order they arrived. The mark is for good.</p>' +
|
|
125
|
+
'<ul class="founding-wall" id="founding-wall"></ul>';
|
|
126
|
+
if (!window.OTBFounding) return;
|
|
127
|
+
window.OTBFounding.load().then(function (list) {
|
|
128
|
+
if (!list.length) return;
|
|
129
|
+
var esc = window.OTB.escapeHtml;
|
|
130
|
+
var wall = host.querySelector('#founding-wall');
|
|
131
|
+
wall.innerHTML = list.map(function (m) {
|
|
132
|
+
var name = esc(m.display_name || m.github_login || 'builder');
|
|
133
|
+
var sub = m.founder ? 'founder' : '#' + m.number + ' · ' + esc(m.stage || 'genesis');
|
|
134
|
+
return '<li class="founding-wall__item">' +
|
|
135
|
+
'<span class="founding-wall__seed" aria-hidden="true"></span>' +
|
|
136
|
+
'<a class="founding-wall__name" href="/profile?id=' + encodeURIComponent(m.builder_id) + '">' + name + '</a>' +
|
|
137
|
+
'<span class="founding-wall__sub">' + sub + '</span></li>';
|
|
138
|
+
}).join('');
|
|
139
|
+
host.hidden = false;
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
113
144
|
/* ---------------- Standing ---------------- */
|
|
114
145
|
|
|
115
146
|
// 1. Profile — the identity band at the top of Standing (#726). No `group`:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The founding wall on Home (task 1004424): the home-founding widget a founded project shows, from the kit fixture founding-builders-sample.json. The page itself is index.states.json's; only the founding read differs. The never-founded case is index.states.json, where the wall stays hidden. Run it with --states (Home has no last path segment).",
|
|
3
|
+
"page": "/builders/",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/", "fixtures": { "founding-builders": "founding-builders-sample" } },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox", "/api/(gds|bongos)/live", "/api/(gds|bongos)/analytics/", "/api/(gds|bongos)/achievements", "/api/(gds|bongos)/versions", "/api/(gds|bongos)/leaderboard", "/api/(gds|bongos)/removal-proposals", "/api/(gds|bongos)/blockers"],
|
|
8
|
+
"_ignored": "removal-proposals and blockers are fixtures the kit stub does not carry; index.states.json's home render reports the same two 404s, so they are not this variant's.",
|
|
9
|
+
"ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
|
|
10
|
+
"states": {
|
|
11
|
+
"founding": { "auth": true, "actions": [["wait", 600], ["focus", ".founding-wall__name"]], "expect": { "visible": ["#home-founding"] } }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -119,7 +119,8 @@
|
|
|
119
119
|
script (served only when the module is enabled) is injected by the server
|
|
120
120
|
immediately before the builders.js tag below — that anchor is load-bearing
|
|
121
121
|
(serve-internal.js injectModuleHallWidgets). -->
|
|
122
|
-
<script src="/builders/
|
|
122
|
+
<script src="/builders/founding-mark.js?v=2026-09-30-founding" defer></script>
|
|
123
|
+
<script src="/builders/hall-widgets.js?v=2026-09-30-founding" defer></script>
|
|
123
124
|
<!-- BV1.R78 (task 1694): the Goals widget registers into the same registry. -->
|
|
124
125
|
<script src="/builders/goals.js?v=2026-07-01-goals" defer></script>
|
|
125
126
|
<!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
|
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
exists when renderProfile runs; profile.js guards on it, so a page that
|
|
211
211
|
omits this script simply has no charts section content. -->
|
|
212
212
|
<script src="/builders/profile-charts.js?v=2026-09-04-calendar"></script>
|
|
213
|
-
<script src="/builders/
|
|
213
|
+
<script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
|
|
214
|
+
<script src="/builders/profile.js?v=2026-09-30-founding"></script>
|
|
214
215
|
</body>
|
|
215
216
|
</html>
|
|
@@ -114,6 +114,23 @@
|
|
|
114
114
|
</div>`;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// ---- render: the founding mark (task 1004424) ----------------------------
|
|
118
|
+
// A builder who joined while the project was founding wears the seed badge beside
|
|
119
|
+
// their rank, and the head says when they joined. Added to the head renderHead
|
|
120
|
+
// already drew; a builder without the mark (or a project never founded) leaves the
|
|
121
|
+
// head exactly as it was.
|
|
122
|
+
function renderFoundingMark(builder) {
|
|
123
|
+
if (!window.OTBFounding || builder.id == null) return;
|
|
124
|
+
window.OTBFounding.load().then((list) => {
|
|
125
|
+
const mark = window.OTBFounding.byId(list)[String(builder.id)];
|
|
126
|
+
const row = document.querySelector('#profile-head .profile__name-row');
|
|
127
|
+
if (!mark || !row || row.querySelector('.founding-mark')) return;
|
|
128
|
+
row.insertAdjacentHTML('beforeend', window.OTBFounding.chipHtml(mark));
|
|
129
|
+
const stats = document.querySelector('#profile-head .profile__stats');
|
|
130
|
+
if (stats) stats.insertAdjacentHTML('afterend', `<p class="profile__founding">${escapeHtml(window.OTBFounding.joinedLine(mark))}</p>`);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
// ---- render: by the numbers ---------------------------------------------
|
|
118
135
|
|
|
119
136
|
function tile(label, num, sub) {
|
|
@@ -764,6 +781,7 @@
|
|
|
764
781
|
const shipped = Number(data.shipped_count) || 0;
|
|
765
782
|
showMessage(`${name} — ${rankLabel(normalizeRank(builder.rank))} · ${fmtNum(shipped)} work${shipped === 1 ? '' : 's'} shipped`);
|
|
766
783
|
renderHead(builder);
|
|
784
|
+
renderFoundingMark(builder);
|
|
767
785
|
renderNumbers(data.shipped_count, data.achievements);
|
|
768
786
|
// The charts (task 1003489) read the activity route and the credits ledger
|
|
769
787
|
// (task 1003491), so they need the same session the ledgers do; on the
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The founding mark (task 1004424) on the Ranks page: the Founding builders ledger (builder, rank, mark) a founded project shows, from the kit fixture founding-builders-sample.json. The page itself is ranks.states.json's; only the founding read differs. The never-founded case is ranks.states.json, where the section stays hidden.",
|
|
3
|
+
"page": "/builders/ranks",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/", "fixtures": { "founding-builders": "founding-builders-sample" } },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/me/"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized", "404 \\(Not Found\\)", "goals load failed"],
|
|
9
|
+
"states": {
|
|
10
|
+
"founding": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#founding-scroll"] } }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
</section>
|
|
41
41
|
|
|
42
|
+
<!-- The founding builders (task 1004424) — everyone who joined while the
|
|
43
|
+
project was founding, with their mark beside their rank. Hidden, and
|
|
44
|
+
never fetched into view, on a project that was never founded. -->
|
|
45
|
+
<section class="scroll" id="founding-scroll" aria-labelledby="founding-h" hidden>
|
|
46
|
+
<h2 id="founding-h" class="scroll__h">Founding builders</h2>
|
|
47
|
+
<p class="scroll__lede">Rank is earned by shipping work. The founding mark is for good: it is numbered in the order people joined while the project was being founded, and it grants nothing.</p>
|
|
48
|
+
<div id="founding-body"></div>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
42
51
|
<!-- The ladder — one card per rank. Current + next are shown in full;
|
|
43
52
|
ranks beyond the next keep their unlock details hidden until the
|
|
44
53
|
builder is one rank below them. -->
|
|
@@ -73,6 +82,7 @@
|
|
|
73
82
|
<script src="/builders/palette.js" defer></script>
|
|
74
83
|
<!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
|
|
75
84
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
76
|
-
<script src="/builders/
|
|
85
|
+
<script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
|
|
86
|
+
<script src="/builders/ranks.js?v=2026-09-30-founding"></script>
|
|
77
87
|
</body>
|
|
78
88
|
</html>
|
|
@@ -200,6 +200,31 @@
|
|
|
200
200
|
// /me may return the builder under `.builder` or at the top level.
|
|
201
201
|
const builder = (payload && payload.builder) ? payload.builder : payload;
|
|
202
202
|
render(builder || {});
|
|
203
|
+
renderFounding();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- the founding builders (task 1004424) ----
|
|
207
|
+
// A ledger of everyone who joined while the project was founding: builder, rank,
|
|
208
|
+
// and the mark. The section stays hidden when nobody carries one, so a project
|
|
209
|
+
// that was never founded shows today's page.
|
|
210
|
+
function renderFounding() {
|
|
211
|
+
const section = $('#founding-scroll');
|
|
212
|
+
const body = $('#founding-body');
|
|
213
|
+
if (!section || !body || !window.OTBFounding) return;
|
|
214
|
+
window.OTBFounding.load().then((list) => {
|
|
215
|
+
if (!list.length) return;
|
|
216
|
+
const rows = list.map((m) => {
|
|
217
|
+
const name = escapeHtml(m.display_name || m.github_login || 'builder');
|
|
218
|
+
return `<tr>
|
|
219
|
+
<td><a class="founding-name" href="/profile?id=${encodeURIComponent(m.builder_id)}">${name}</a>
|
|
220
|
+
<span class="founding-line">${escapeHtml(window.OTBFounding.joinedLine(m))}</span></td>
|
|
221
|
+
<td>${badge(m.rank)}</td>
|
|
222
|
+
<td>${window.OTBFounding.chipHtml(m)}</td>
|
|
223
|
+
</tr>`;
|
|
224
|
+
}).join('');
|
|
225
|
+
body.innerHTML = `<table class="ledger"><thead><tr><th>Builder</th><th>Rank</th><th>Mark</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
226
|
+
section.hidden = false;
|
|
227
|
+
});
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
load();
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
59
59
|
<script src="/builders/brand-apply.js"></script>
|
|
60
60
|
<script src="/builders/hall-kit.js"></script>
|
|
61
|
+
<script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
|
|
61
62
|
<script src="/builders/roster.js"></script>
|
|
62
63
|
</body>
|
|
63
64
|
</html>
|
|
@@ -157,10 +157,20 @@
|
|
|
157
157
|
return '<span class="ledger__id">#' + esc(String(b.id)) + '</span>';
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// task 1004424: the founding mark — a seed badge under the builder for everyone who
|
|
161
|
+
// joined while the project was founding. Empty on a project that was never founded,
|
|
162
|
+
// so the row is exactly today's.
|
|
163
|
+
var marks = {};
|
|
164
|
+
function foundingHtml(b) {
|
|
165
|
+
var m = b.id != null ? marks[String(b.id)] : null;
|
|
166
|
+
if (!m || !window.OTBFounding) return '';
|
|
167
|
+
return '<span class="lb-founding">' + window.OTBFounding.chipHtml(m) + '</span>';
|
|
168
|
+
}
|
|
169
|
+
|
|
160
170
|
function rowHtml(b) {
|
|
161
171
|
return '<tr>'
|
|
162
172
|
+ '<td class="ledger__num ledger__place">' + esc(String(b.__place || '—')) + idHtml(b) + '</td>'
|
|
163
|
-
+ '<td>' + whoHtml(b) + achRowHtml(b.achievements) + '</td>'
|
|
173
|
+
+ '<td>' + whoHtml(b) + foundingHtml(b) + achRowHtml(b.achievements) + '</td>'
|
|
164
174
|
+ '<td class="ledger__rank">' + rankBadgeHtml(b.rank) + '</td>'
|
|
165
175
|
+ '<td class="ledger__num">' + esc(fmtInt(b.total_credits)) + '</td>'
|
|
166
176
|
+ '<td class="ledger__num">' + esc(fmtInt(b.karma)) + '</td>'
|
|
@@ -226,11 +236,17 @@
|
|
|
226
236
|
rerender();
|
|
227
237
|
}
|
|
228
238
|
|
|
239
|
+
var foundingLoad = window.OTBFounding ? window.OTBFounding.load() : Promise.resolve([]);
|
|
229
240
|
fetch('/api/bongos/leaderboard', { headers: { Accept: 'application/json' } })
|
|
230
241
|
.then(function (r) {
|
|
231
242
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
232
243
|
return r.json();
|
|
233
244
|
})
|
|
234
|
-
.then(function (data) {
|
|
245
|
+
.then(function (data) {
|
|
246
|
+
return foundingLoad.then(function (list) {
|
|
247
|
+
marks = window.OTBFounding ? window.OTBFounding.byId(list) : {};
|
|
248
|
+
mountRoster(data && data.leaderboard);
|
|
249
|
+
});
|
|
250
|
+
})
|
|
235
251
|
.catch(function () { notice('Couldn’t load the roster right now.'); });
|
|
236
252
|
}());
|
|
@@ -698,6 +698,41 @@ main:focus { outline: none; }
|
|
|
698
698
|
.founding-band__desc { display: none; }
|
|
699
699
|
.founding-band__stage { margin-left: 0; white-space: normal; }
|
|
700
700
|
}
|
|
701
|
+
/* task 1004424 (BV2.PS10): THE FOUNDING MARK. A pill in .rank-badge's grammar (same
|
|
702
|
+
height, type and radius) so it sits beside a rank without competing with it; the
|
|
703
|
+
seed is the accent, the founding band's live-state colour, because the mark is
|
|
704
|
+
that state made permanent. It confers nothing, so it is never a link. */
|
|
705
|
+
.founding-mark {
|
|
706
|
+
display: inline-flex;
|
|
707
|
+
align-items: center;
|
|
708
|
+
gap: 6px;
|
|
709
|
+
height: 24px;
|
|
710
|
+
padding: 0 9px 0 7px;
|
|
711
|
+
border-radius: 999px;
|
|
712
|
+
font-size: 11.5px;
|
|
713
|
+
font-weight: 600;
|
|
714
|
+
letter-spacing: 0.02em;
|
|
715
|
+
white-space: nowrap;
|
|
716
|
+
color: var(--ink-soft);
|
|
717
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
718
|
+
border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
|
|
719
|
+
}
|
|
720
|
+
.founding-mark__seed,
|
|
721
|
+
.founding-wall__seed {
|
|
722
|
+
width: 8px; height: 8px; border-radius: 50%; flex: none;
|
|
723
|
+
background: var(--accent);
|
|
724
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
|
|
725
|
+
}
|
|
726
|
+
.lb-founding { display: block; margin-top: 4px; }
|
|
727
|
+
.profile__founding { margin: 2px 0 0; font-size: 13px; color: var(--ink-soft); }
|
|
728
|
+
/* The prose-link 24px floor: the target grows by padding, handed back by margin. */
|
|
729
|
+
.founding-name { display: inline-block; padding: 3px 6px; margin: -3px -6px; }
|
|
730
|
+
.founding-line { display: block; font-size: 12px; color: var(--ink-faint); }
|
|
731
|
+
.founding-wall { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 14px; }
|
|
732
|
+
.founding-wall__item { display: grid; justify-items: center; gap: 4px; text-align: center; padding: 12px 8px; }
|
|
733
|
+
.founding-wall__seed { width: 14px; height: 14px; margin-bottom: 4px; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent); }
|
|
734
|
+
.founding-wall__name { padding: 4px 6px; margin: -4px -6px; font-weight: 500; color: var(--ink); }
|
|
735
|
+
.founding-wall__sub { font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
|
|
701
736
|
.nav-item--asleep { opacity: 0.55; }
|
|
702
737
|
.nav-item__wakes { display: none; margin-left: auto; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); }
|
|
703
738
|
.app-sidebar:hover .nav-item__wakes,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The founding mark — a seed badge on the roster, the profile and the ranks page, and the founding wall on Home (task 1004424)
|
|
2
|
+
|
|
3
|
+
**BV2.PS10 (goal 1000121; spec `docs/specs/<redacted>.md` D1).** The canvas frame is `docs/design/mocks/project-startup/FoundingBuilders.dc.html`. Everyone who joined while the project was being founded is a founding builder, numbered in join order; the project owner is "founder". **It is a mark, not a rank: it grants nothing**, and nothing in the hall or on the server reads it to decide anything.
|
|
4
|
+
|
|
5
|
+
Files: `founding-mark.js` (`window.OTBFounding`: one shared read of `GET /api/bongos/founding/builders`, the pill, the plain "Joined at scope, day 3 of genesis" sentence), plus a few lines in `roster.js`, `profile.js`, `ranks.js` (+ a hidden `#founding-scroll` section in `ranks.html`) and the `home-founding` widget in `hall-widgets.js`. The CSS sits beside the founding band in `style.css`. The data is `modules/onboarding/founding-builders.js`.
|
|
6
|
+
|
|
7
|
+
## Blended, not re-skinned
|
|
8
|
+
|
|
9
|
+
The canvas draws its own hall chrome: a ranks table, seed images, an emblem wall. The live hall wins (the blend rule), so the mark is built from the hall's own parts:
|
|
10
|
+
|
|
11
|
+
- **The pill is `.rank-badge`'s grammar**: the same 24px height, 11.5px/600 type and full radius, so it sits beside a rank without competing with it. The seed is a dot in `--accent`, the founding band's live-state colour, because the mark is that state made permanent. It is not a link, because it confers nothing.
|
|
12
|
+
- **The ranks page has no builders table** (it shows the viewer's own standing), so the canvas's "column on the ranks page" is a `Founding builders` ledger section on that page: builder | rank | mark. It is not a new page.
|
|
13
|
+
- **The wall is a core Home widget in `home-main`**, which brings that zone to its budget of three. It is hidden by default and fills itself from the shared read.
|
|
14
|
+
|
|
15
|
+
## Off is today's hall
|
|
16
|
+
|
|
17
|
+
A project that was never founded has no founding window, so the read returns an empty list without querying, and every surface renders exactly as before. The roster row of an unmarked builder is byte-identical, the ranks section stays `hidden`, and the wall never unhides. `tests/founding_builders.mjs` runs the real `roster.js` and `hall-widgets.js` to hold this. A signed-out viewer (the profile is public) gets a 401 on the read, which is the same empty list.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/onboarding/founding-builders.js — THE FOUNDING MARK: who joined this project
|
|
4
|
+
// while it was being founded, numbered in the order they arrived, and the genesis
|
|
5
|
+
// stage each joined at (task 1004424 / BV2.PS10, goal 1000121; design of record
|
|
6
|
+
// docs/design/mocks/project-startup/FoundingBuilders.dc.html).
|
|
7
|
+
//
|
|
8
|
+
// IT IS A MARK, NOT A RANK (spec D1). It confers nothing: no rank, no council seat, no
|
|
9
|
+
// permission reads it. It exists to be shown — a badge on the roster and the profile,
|
|
10
|
+
// a column on the ranks page, and the founding wall on the project's home.
|
|
11
|
+
//
|
|
12
|
+
// DERIVED, NOT STORED. The founding record founding-mode.js keeps is all the mark
|
|
13
|
+
// needs: the window (founding.started_at .. founding.ended_at) and the time each stage
|
|
14
|
+
// first became current (founding.stage_at.<stage>). A builder whose row was created in
|
|
15
|
+
// the window joined during genesis, so their number is their place in join order and
|
|
16
|
+
// their stage is the last stage stamped at or before their join. Every stamp is
|
|
17
|
+
// write-once, so the mark is for good: nothing a later write does can renumber it.
|
|
18
|
+
// That also means no sign-in path had to change to record a joiner.
|
|
19
|
+
//
|
|
20
|
+
// THE FOUNDER is the project owner (the doorway's getFoundingBuilder: the first human
|
|
21
|
+
// builder) and is marked "founder" rather than numbered. Joiners count from #2, so the
|
|
22
|
+
// founder is always the implied #1 and a founder who joins late cannot shift anyone.
|
|
23
|
+
//
|
|
24
|
+
// A project that was never founded has no window, and every read answers no marks at
|
|
25
|
+
// all, so a hall that is not founding renders exactly as before.
|
|
26
|
+
|
|
27
|
+
const { STAGES, KEYS, stageAtKey, dayOf } = require('./founding-mode');
|
|
28
|
+
|
|
29
|
+
// A founding cohort is the people present at a project's birth — tens, not thousands.
|
|
30
|
+
// The cap only bounds the query; past it the latest joiners simply go unshown.
|
|
31
|
+
const MAX_MARKS = 500;
|
|
32
|
+
|
|
33
|
+
const ms = (v) => {
|
|
34
|
+
if (!v) return NaN;
|
|
35
|
+
const t = new Date(v).getTime();
|
|
36
|
+
return Number.isFinite(t) ? t : NaN;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// stageAt(joinedAt, stageAt) — PURE. The stage that was current when a builder joined:
|
|
40
|
+
// the last stage, in genesis order, first stamped at or before that moment. Before any
|
|
41
|
+
// stamp (or with none recorded) it is 'decide', the pre-stage founding opens on.
|
|
42
|
+
function stageAt(joinedAt, stamps) {
|
|
43
|
+
const t = ms(joinedAt);
|
|
44
|
+
let current = 'decide';
|
|
45
|
+
if (!Number.isFinite(t) || !stamps) return current;
|
|
46
|
+
for (const stage of STAGES) {
|
|
47
|
+
const s = ms(stamps[stage]);
|
|
48
|
+
if (Number.isFinite(s) && s <= t) current = stage;
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// shapeFoundingBuilders({ startedAt, endedAt, stageAt, founderId, builders }) — PURE.
|
|
54
|
+
// builders: rows of { id, created_at, ... } in any order. Returns the marks in join
|
|
55
|
+
// order (founder first), or [] when the project was never founded.
|
|
56
|
+
function shapeFoundingBuilders({ startedAt, endedAt, stageAt: stamps, founderId, builders } = {}) {
|
|
57
|
+
const start = ms(startedAt);
|
|
58
|
+
if (!Number.isFinite(start) || !Array.isArray(builders)) return [];
|
|
59
|
+
const end = ms(endedAt);
|
|
60
|
+
const inWindow = (b) => {
|
|
61
|
+
const t = ms(b.created_at);
|
|
62
|
+
return Number.isFinite(t) && t >= start && (!Number.isFinite(end) || t < end);
|
|
63
|
+
};
|
|
64
|
+
const ordered = builders
|
|
65
|
+
.filter((b) => b && b.id != null)
|
|
66
|
+
.slice()
|
|
67
|
+
.sort((a, b) => (ms(a.created_at) - ms(b.created_at)) || (Number(a.id) - Number(b.id)));
|
|
68
|
+
const pick = (b) => ({
|
|
69
|
+
builder_id: Number(b.id),
|
|
70
|
+
display_name: b.display_name || null,
|
|
71
|
+
github_login: b.github_login || null,
|
|
72
|
+
avatar_url: b.avatar_url || null,
|
|
73
|
+
rank: b.rank || null,
|
|
74
|
+
});
|
|
75
|
+
const marks = [];
|
|
76
|
+
const founder = founderId != null ? ordered.find((b) => String(b.id) === String(founderId)) : null;
|
|
77
|
+
if (founder) {
|
|
78
|
+
marks.push({ ...pick(founder), founder: true, number: 1, stage: 'decide', day: 1 });
|
|
79
|
+
}
|
|
80
|
+
let next = 2;
|
|
81
|
+
for (const b of ordered) {
|
|
82
|
+
if (founder && b === founder) continue;
|
|
83
|
+
if (!inWindow(b)) continue;
|
|
84
|
+
marks.push({
|
|
85
|
+
...pick(b),
|
|
86
|
+
founder: false,
|
|
87
|
+
number: next++,
|
|
88
|
+
stage: stageAt(b.created_at, stamps),
|
|
89
|
+
day: dayOf(startedAt, new Date(b.created_at)),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return marks;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// foundingBuildersFor(deps) — the read behind GET /founding/builders. Never throws:
|
|
96
|
+
// any failure is "no marks", which is how a hall that is not founding looks anyway.
|
|
97
|
+
async function foundingBuildersFor(deps = {}) {
|
|
98
|
+
const api = deps.api || require('../../src/module-api');
|
|
99
|
+
const get = deps.getSetting || api.projectSettings.get;
|
|
100
|
+
try {
|
|
101
|
+
const value = async (key) => { const row = await get(key); return row && row.value ? row.value : null; };
|
|
102
|
+
const startedAt = await value(KEYS.STARTED_AT);
|
|
103
|
+
if (!startedAt) return [];
|
|
104
|
+
const [endedAt, ...stageValues] = await Promise.all([
|
|
105
|
+
value(KEYS.ENDED_AT), ...STAGES.map((s) => value(stageAtKey(s))),
|
|
106
|
+
]);
|
|
107
|
+
const stamps = {};
|
|
108
|
+
STAGES.forEach((s, i) => { if (stageValues[i]) stamps[s] = stageValues[i]; });
|
|
109
|
+
const founder = await (deps.getFoundingBuilder || api.getFoundingBuilder)();
|
|
110
|
+
const founderId = founder ? founder.id : null;
|
|
111
|
+
const pool = deps.pool || api.pool;
|
|
112
|
+
const { rows } = await pool.query(
|
|
113
|
+
`SELECT id, github_login, display_name, avatar_url, rank, created_at
|
|
114
|
+
FROM builders
|
|
115
|
+
WHERE system_role IS NULL
|
|
116
|
+
AND (id = $1 OR (created_at >= $2 AND ($3::timestamptz IS NULL OR created_at < $3)))
|
|
117
|
+
ORDER BY created_at, id
|
|
118
|
+
LIMIT ${MAX_MARKS}`,
|
|
119
|
+
[founderId, startedAt, endedAt],
|
|
120
|
+
);
|
|
121
|
+
return shapeFoundingBuilders({ startedAt, endedAt, stageAt: stamps, founderId, builders: rows });
|
|
122
|
+
} catch (_) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { stageAt, shapeFoundingBuilders, foundingBuildersFor, MAX_MARKS };
|
|
@@ -47,8 +47,15 @@ const KEYS = Object.freeze({
|
|
|
47
47
|
ACTIVE: 'founding.active',
|
|
48
48
|
STAGE: 'founding.stage',
|
|
49
49
|
STARTED_AT: 'founding.started_at',
|
|
50
|
+
// When the planet came alive (founding switched off). With STARTED_AT it bounds the
|
|
51
|
+
// window a builder must have joined in to carry the founding mark (task 1004424).
|
|
52
|
+
ENDED_AT: 'founding.ended_at',
|
|
50
53
|
});
|
|
51
54
|
|
|
55
|
+
// The key that records when a stage first became current, so a builder's join time
|
|
56
|
+
// reads back as the stage they joined at (the founding mark, task 1004424).
|
|
57
|
+
function stageAtKey(stage) { return `founding.stage_at.${stage}`; }
|
|
58
|
+
|
|
52
59
|
// The only 'on' spelling accepted, mirroring mingle's consent-gate parse: an explicit,
|
|
53
60
|
// recognised yes. Anything else is off.
|
|
54
61
|
function parseActive(raw) {
|
|
@@ -160,26 +167,43 @@ function waitingOnFromGovernment(gov) {
|
|
|
160
167
|
// (task 1004423) drives: start founding on a new project, move the stage, and switch
|
|
161
168
|
// founding off when the planet comes alive. Refuses an unknown stage rather than storing
|
|
162
169
|
// a value the read would then treat as OFF. Starting stamps started_at once.
|
|
170
|
+
//
|
|
171
|
+
// It also keeps the founding record the mark reads (task 1004424): the first time a
|
|
172
|
+
// stage becomes current it is stamped (starting with no stage is 'decide'), and the
|
|
173
|
+
// first switch-off after a start stamps ended_at. Every stamp is write-once, so the
|
|
174
|
+
// record of who joined when is for good.
|
|
163
175
|
async function setFoundingMode({ active, stage } = {}, builderId = null, deps = {}) {
|
|
164
176
|
const settings = require('../../src/module-api').projectSettings;
|
|
165
177
|
const get = deps.getSetting || settings.get;
|
|
166
178
|
const set = deps.setSetting || settings.set;
|
|
179
|
+
const nowIso = (deps.now || new Date()).toISOString();
|
|
180
|
+
const stampOnce = async (key) => {
|
|
181
|
+
const cur = await get(key);
|
|
182
|
+
if (!cur || !cur.value) await set(key, nowIso, builderId);
|
|
183
|
+
};
|
|
167
184
|
if (stage != null && !STAGES.includes(stage)) {
|
|
168
185
|
const err = new Error(`unknown founding stage '${stage}'`);
|
|
169
186
|
err.code = 'bad_founding_stage';
|
|
170
187
|
throw err;
|
|
171
188
|
}
|
|
172
189
|
if (active === true) {
|
|
190
|
+
await stampOnce(KEYS.STARTED_AT);
|
|
191
|
+
if (stage == null) await stampOnce(stageAtKey('decide'));
|
|
192
|
+
}
|
|
193
|
+
if (active === false) {
|
|
173
194
|
const started = await get(KEYS.STARTED_AT);
|
|
174
|
-
if (
|
|
195
|
+
if (started && started.value) await stampOnce(KEYS.ENDED_AT);
|
|
175
196
|
}
|
|
176
197
|
if (typeof active === 'boolean') await set(KEYS.ACTIVE, active ? 'true' : 'false', builderId);
|
|
177
|
-
if (stage != null)
|
|
198
|
+
if (stage != null) {
|
|
199
|
+
await stampOnce(stageAtKey(stage));
|
|
200
|
+
await set(KEYS.STAGE, stage, builderId);
|
|
201
|
+
}
|
|
178
202
|
return foundingModeFor({ getSetting: get, now: deps.now });
|
|
179
203
|
}
|
|
180
204
|
|
|
181
205
|
module.exports = {
|
|
182
|
-
STAGES, WAKES, WAKES_WHEN_ALIVE, KEYS,
|
|
206
|
+
STAGES, WAKES, WAKES_WHEN_ALIVE, KEYS, stageAtKey,
|
|
183
207
|
parseActive, asleepRooms, dayOf, shapeFoundingMode, waitingOnFromGovernment,
|
|
184
208
|
foundingModeFor, setFoundingMode,
|
|
185
209
|
};
|
|
@@ -34,6 +34,7 @@ const express = require('express');
|
|
|
34
34
|
const api = require('../../../src/module-api');
|
|
35
35
|
const onboardingPort = require('../port');
|
|
36
36
|
const buildAccessRequestsRouter = require('./access-requests');
|
|
37
|
+
const foundingBuilders = require('../founding-builders');
|
|
37
38
|
|
|
38
39
|
// Register the `onboarding` kernel PORT. See the header + port.js for the full
|
|
39
40
|
// consumer list and the graceful-degradation contract.
|
|
@@ -50,5 +51,15 @@ module.exports = function buildOnboardingRouter() {
|
|
|
50
51
|
// Archon-only resolve routes). Mounted on this same /api/bongos router by the
|
|
51
52
|
// loader, before the public catch-all — byte-identical paths.
|
|
52
53
|
router.use(buildAccessRequestsRouter());
|
|
54
|
+
// The founding mark (task 1004424): who joined while the project was founding, in
|
|
55
|
+
// join order, with the stage each joined at. Read by the roster, the profile, the
|
|
56
|
+
// ranks page and the home's founding wall; an empty list on a project that was
|
|
57
|
+
// never founded, so those surfaces render exactly as before. Signed-in, like the
|
|
58
|
+
// hall pages that read it; it carries nothing the roster does not already show.
|
|
59
|
+
router.get('/founding/builders', api.requireBuilder, async (req, res) => {
|
|
60
|
+
const builders = await foundingBuilders.foundingBuildersFor();
|
|
61
|
+
res.set('Cache-Control', 'private, max-age=60');
|
|
62
|
+
res.json({ builders });
|
|
63
|
+
});
|
|
53
64
|
return router;
|
|
54
65
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The founding mark (task 1004424): GET /founding/builders on a project that was founded. Builder 1 is the kit's own signed-in builder (me.json), so the ranks and Home renders show the viewer as founder. Only the founding variant states files point at this; the default name has no file, so every other render is a never-founded project.",
|
|
3
|
+
"builders": [
|
|
4
|
+
{ "builder_id": 1, "display_name": "A Builder", "github_login": "builder", "avatar_url": null, "rank": "archon", "founder": true, "number": 1, "stage": "decide", "day": 1 },
|
|
5
|
+
{ "builder_id": 2, "display_name": "Avery Okonkwo-Lindqvist", "github_login": "avery", "avatar_url": null, "rank": "metic", "founder": false, "number": 2, "stage": "scope", "day": 3 },
|
|
6
|
+
{ "builder_id": 3, "display_name": "Jordan", "github_login": "jordan", "avatar_url": null, "rank": "thetes", "founder": false, "number": 3, "stage": "strategize", "day": 8 },
|
|
7
|
+
{ "builder_id": 4, "display_name": "Sam", "github_login": "sam", "avatar_url": null, "rank": "xenos", "founder": false, "number": 4, "stage": "legislation", "day": 19 }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -105,7 +105,10 @@ const FEEDS = {
|
|
|
105
105
|
'/public/leaderboard': 'leaderboard',
|
|
106
106
|
'/projects/featured': 'featured',
|
|
107
107
|
};
|
|
108
|
-
|
|
108
|
+
// /founding/builders (task 1004424) answers a never-founded project's empty list by default,
|
|
109
|
+
// so every existing render stays today's page; a founding variant states file points it at
|
|
110
|
+
// founding-builders-sample via stub.fixtures.
|
|
111
|
+
const AUTHED = { '/my-projects': 'my-projects', '/my-projects/invites': 'my-projects__invites', '/founding/builders': 'founding-builders' };
|
|
109
112
|
const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' } };
|
|
110
113
|
|
|
111
114
|
// GET /provisioning/starter-bundles as the create wizard's module picker reads it
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.56",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.56",
|
|
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.20.
|
|
3
|
+
"version": "1.20.56",
|
|
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",
|