@bongos/core 1.19.1045 → 1.19.1047
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 +90 -115
- package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +14 -2
- package/docs/adr/README.md +1 -1
- package/docs/copy-inventory.md +17 -24
- package/docs/copy-registry.json +24 -97
- package/docs/file-map.md +2 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/core-release-pipeline.md +19 -0
- package/docs/recipes/windows-builders.md +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.css +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/{people.css → roster.css} +7 -11
- package/modules/hall-ui/public/roster.html +5 -5
- package/modules/hall-ui/public/roster.js +6 -7
- package/modules/hall-ui/public/roster.states.json +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +3 -10
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +2 -9
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/builders-roster.md +1 -1
- package/modules/hall-ui/records/roster-and-ledger.md +2 -2
- package/modules/platform-identity/account-visibility.js +3 -3
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/profile.js +1 -1
- package/modules/platform-identity/routes/sso.js +4 -3
- package/modules/platform-identity/scouting-authz.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/go-live.js +101 -5
- package/src/bongos/auth.js +9 -24
- package/src/bongos/serve-internal.js +13 -19
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +1 -1
- package/tests/auth_resolve_failure.mjs +6 -6
- package/tests/core_upgrade_owner_door.mjs +1 -1
- package/tests/edit_hints.mjs +8 -8
- package/tests/effective_visibility_predicate.mjs +1 -1
- package/tests/go_live.mjs +143 -4
- package/tests/government_parity.mjs +1 -1
- package/tests/hall_builders_page_pass.mjs +2 -2
- package/tests/hall_nav.mjs +8 -8
- package/tests/hall_page_gate_map.mjs +14 -11
- package/tests/hall_roster_world.mjs +47 -78
- package/tests/module_contributions.mjs +0 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/{scouting_page_gate.mjs → page_gate_second_door.mjs} +34 -29
- package/tests/profile_rollup_consent.mjs +4 -4
- package/tests/profile_route.mjs +3 -26
- package/tests/scouting_owner_access_and_reach.mjs +4 -8
- package/modules/hall-ui/public/people.html +0 -79
- package/modules/hall-ui/public/people.js +0 -321
- package/modules/hall-ui/public/people.states.json +0 -89
- package/tests/people_directory_clip.mjs +0 -269
- package/tests/people_name_only.mjs +0 -263
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/page_gate_second_door.mjs
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
3
|
+
// The page gates' `alsoAdmits` SECOND DOOR (ADR 0210), proven on the one gate that
|
|
4
|
+
// declares it: requireCorePinPage, the /deploy shell, whose routes admit a hosted
|
|
5
|
+
// project's own owner beside `core.pin.move` (ADR 0339, task 1004174). The door's
|
|
6
|
+
// first user was the hall's /people page; task 1003444 deleted that page (ADR 0337
|
|
7
|
+
// D5) and retargeted this file (formerly tests/scouting_page_gate.mjs) here.
|
|
5
8
|
//
|
|
6
|
-
// WHY THIS IS ITS OWN FILE
|
|
9
|
+
// WHY THIS IS ITS OWN FILE. `runPageGate` grew an optional
|
|
7
10
|
// `alsoAdmits` door — new authorization code that runs on EVERY page-gate
|
|
8
11
|
// denial, for every gate. The route proof
|
|
9
|
-
// (tests/
|
|
12
|
+
// (tests/core_upgrade_owner_door.mjs) proves the widening itself; this file pins
|
|
13
|
+
// the door's failure modes, which a route proof cannot reach: page gates are
|
|
10
14
|
// driven by a session, and that file's ESM imports pull `auth.js` in through the
|
|
11
15
|
// module graph before any session double could be installed. This file installs
|
|
12
16
|
// the double FIRST, exactly as tests/government_page_gates.mjs does, and so gets
|
|
@@ -21,12 +25,12 @@
|
|
|
21
25
|
// 3. every way the second door can fail — module absent, function absent, a
|
|
22
26
|
// throw, a non-boolean, and a name that resolves only through the prototype
|
|
23
27
|
// — fails CLOSED. A fail-open default in an authz path is the bug class;
|
|
24
|
-
// 4. the other
|
|
28
|
+
// 4. the other gates are untouched, and their seam is never even
|
|
25
29
|
// consulted, because they declare no second door.
|
|
26
30
|
//
|
|
27
31
|
// DB-free: a session double and a port double. No Postgres, no HTTP.
|
|
28
32
|
//
|
|
29
|
-
// Run: node tests/
|
|
33
|
+
// Run: node tests/page_gate_second_door.mjs
|
|
30
34
|
|
|
31
35
|
import { strict as assert } from 'node:assert';
|
|
32
36
|
import { createRequire } from 'node:module';
|
|
@@ -53,12 +57,13 @@ async function t(name, fn) {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// The cast. `rank` is the caller's rank ON THE HUB; `github_id` is the account
|
|
56
|
-
// whose project memberships the ownership door asks about. The
|
|
57
|
-
//
|
|
58
|
-
//
|
|
60
|
+
// whose project memberships the ownership door asks about. The first row is the
|
|
61
|
+
// point: a project's registrant is an Archon on their OWN instance and, on the
|
|
62
|
+
// hub, very often a xenos holding nothing at all.
|
|
59
63
|
const OWNER = { builder_id: 1, github_id: '9001', rank: 'xenos' };
|
|
60
64
|
const STRANGER = { builder_id: 2, github_id: '9002', rank: 'xenos' };
|
|
61
|
-
|
|
65
|
+
// core.pin.move is archon-floor (ADR 0293), so the permission-door control is an Archon.
|
|
66
|
+
const OPERATOR = { builder_id: 5, github_id: '9005', rank: 'archon' };
|
|
62
67
|
|
|
63
68
|
// The ownership answer, as the platform-identity.hub port supplies it.
|
|
64
69
|
const OWNS = new Set(['9001']);
|
|
@@ -94,33 +99,33 @@ async function runGate(gate, session, hub) {
|
|
|
94
99
|
return { res, nexted };
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const
|
|
102
|
+
const DENIED_PIN = { code: 302, loc: '/builders?denied=core.pin.move' };
|
|
98
103
|
|
|
99
|
-
console.log('the /
|
|
104
|
+
console.log('the /deploy shell follows its data routes:');
|
|
100
105
|
|
|
101
106
|
await t('a PROJECT OWNER reaches the shell, holding no permission at all', async () => {
|
|
102
|
-
const { nexted } = await runGate('
|
|
107
|
+
const { nexted } = await runGate('requireCorePinPage', OWNER, workingHub);
|
|
103
108
|
assert.ok(nexted,
|
|
104
109
|
'the page gate must admit the audience its data route admits — otherwise the widening is real in the API and unreachable in the product');
|
|
105
110
|
});
|
|
106
111
|
|
|
107
112
|
await t('CONTROL: the permission door still opens the shell on its own', async () => {
|
|
108
|
-
const { nexted } = await runGate('
|
|
109
|
-
assert.ok(nexted, '
|
|
113
|
+
const { nexted } = await runGate('requireCorePinPage', OPERATOR, workingHub);
|
|
114
|
+
assert.ok(nexted, 'core.pin.move admits an Archon (ADR 0293); ADR 0339 did not move it');
|
|
110
115
|
// ...and it does not need the second door at all.
|
|
111
|
-
const { nexted: noHub } = await runGate('
|
|
116
|
+
const { nexted: noHub } = await runGate('requireCorePinPage', OPERATOR, null);
|
|
112
117
|
assert.ok(noHub, 'an operator gets in on a vanilla instance with no hub module');
|
|
113
118
|
});
|
|
114
119
|
|
|
115
120
|
await t('a builder who is neither gets the byte-identical redirect the gate always sent', async () => {
|
|
116
|
-
const { res, nexted } = await runGate('
|
|
117
|
-
assert.equal(nexted, false, '
|
|
118
|
-
assert.deepEqual(res._redirect,
|
|
121
|
+
const { res, nexted } = await runGate('requireCorePinPage', STRANGER, workingHub);
|
|
122
|
+
assert.equal(nexted, false, 'a xenos who owns no project stays out');
|
|
123
|
+
assert.deepEqual(res._redirect, DENIED_PIN,
|
|
119
124
|
'the denial keeps naming the permission (task 1002613), so the hall shell can still render WHY');
|
|
120
125
|
});
|
|
121
126
|
|
|
122
127
|
await t('an unauthenticated visitor is still bounced into sign-in, never to the second door', async () => {
|
|
123
|
-
const { res, nexted } = await runGate('
|
|
128
|
+
const { res, nexted } = await runGate('requireCorePinPage', null, workingHub);
|
|
124
129
|
assert.equal(nexted, false);
|
|
125
130
|
assert.match(res._redirect.loc, /auth\/web\/start/, 'no session means sign-in, not a denial page');
|
|
126
131
|
});
|
|
@@ -139,12 +144,12 @@ await t('every way the second door can fail, it fails CLOSED', async () => {
|
|
|
139
144
|
['the function resolves only through the prototype', Object.create({ isProjectOwner: () => true })],
|
|
140
145
|
];
|
|
141
146
|
for (const [why, hub] of cases) {
|
|
142
|
-
const { res, nexted } = await runGate('
|
|
147
|
+
const { res, nexted } = await runGate('requireCorePinPage', OWNER, hub);
|
|
143
148
|
assert.equal(nexted, false, `${why}: must not admit`);
|
|
144
|
-
assert.deepEqual(res._redirect,
|
|
149
|
+
assert.deepEqual(res._redirect, DENIED_PIN,
|
|
145
150
|
`${why}: must still redirect rather than hang, throw, or answer JSON`);
|
|
146
151
|
}
|
|
147
|
-
const { nexted } = await runGate('
|
|
152
|
+
const { nexted } = await runGate('requireCorePinPage', OWNER, workingHub);
|
|
148
153
|
assert.ok(nexted, 'CONTROL: the same caller IS admitted when the door works — the six refusals are the door, not the caller');
|
|
149
154
|
});
|
|
150
155
|
|
|
@@ -152,7 +157,7 @@ await t('the second door is consulted only where the AUTHORITY answer was no', a
|
|
|
152
157
|
// A permission holder must not pay for it, and — more important — the seam
|
|
153
158
|
// must never be able to NARROW a caller the permission already admitted.
|
|
154
159
|
const spy = { isProjectOwner: () => { throw new Error('the seam must not be consulted for a caller the permission already admitted'); } };
|
|
155
|
-
const { nexted } = await runGate('
|
|
160
|
+
const { nexted } = await runGate('requireCorePinPage', OPERATOR, spy);
|
|
156
161
|
assert.ok(nexted, 'the atom holder is admitted before the second door is reached');
|
|
157
162
|
});
|
|
158
163
|
|
|
@@ -176,14 +181,14 @@ await t('the OTHER page gates are untouched — their seam is never consulted at
|
|
|
176
181
|
|
|
177
182
|
await t('only the gates WITH A PROOF declare a second door (a widening must be opted into, one gate at a time)', () => {
|
|
178
183
|
const src = require('node:fs').readFileSync(new URL('../src/bongos/auth.js', import.meta.url), 'utf8');
|
|
179
|
-
// Keyed by GATE, not just the door's name, so a
|
|
184
|
+
// Keyed by GATE, not just the door's name, so a second gate borrowing 'isProjectOwner'
|
|
180
185
|
// fails here instead of hiding behind an unchanged list of names. Each entry names the
|
|
181
186
|
// file that proves its widening:
|
|
182
|
-
//
|
|
183
|
-
//
|
|
187
|
+
// requireCorePinPage — tests/core_upgrade_owner_door.mjs (ADR 0339, task 1004174).
|
|
188
|
+
// (requireProjectCuratePage, the /people gate, was deleted with its page — task 1003444.)
|
|
184
189
|
const declarations = [...src.matchAll(/(require\w+Page):\s*Object\.freeze\(\{[^}]*alsoAdmits:\s*'([^']+)'/g)]
|
|
185
190
|
.map((m) => `${m[1]}:${m[2]}`);
|
|
186
|
-
assert.deepEqual(declarations, ['
|
|
191
|
+
assert.deepEqual(declarations, ['requireCorePinPage:isProjectOwner'],
|
|
187
192
|
'a second alsoAdmits appearing without its own proof is how a page gate quietly widens');
|
|
188
193
|
const plain = [...src.matchAll(/alsoAdmits:\s*'([^']+)'/g)].length;
|
|
189
194
|
assert.equal(plain, declarations.length, 'every alsoAdmits sits on a named page gate this test can see');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// unnest(), so the rollup queries need a live Postgres (the task-1002911
|
|
5
5
|
// precedent). Replays the module's actual migrations, then asserts:
|
|
6
6
|
// 1. listScoutingRollup lists ONLY consented + not-hidden builders (D2/D3) —
|
|
7
|
-
// provisional, hidden and never-accepted accounts drop out of
|
|
7
|
+
// provisional, hidden and never-accepted accounts drop out of
|
|
8
8
|
// /scouting by construction. Consent is TWO columns since ADR 0213
|
|
9
9
|
// (profile_state='active' AND terms_accepted_at IS NOT NULL), and the
|
|
10
10
|
// LEGACY fixture is the pre-terms row that proves the second one.
|
|
@@ -151,7 +151,7 @@ try {
|
|
|
151
151
|
[...ALL_CLIENTS, ACTIVE, HIDDEN, PROV, LEGACY],
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
-
// 1 — the scouting
|
|
154
|
+
// 1 — the scouting predicate (D2/D3).
|
|
155
155
|
const listed = (await pi.listScoutingRollup({ limit: 500, pool })).map((r) => Number(r.github_id));
|
|
156
156
|
ok(listed.includes(ACTIVE), 'active + accepted + visible builder is listed');
|
|
157
157
|
ok(!listed.includes(HIDDEN), 'hide_stats builder drops out of the scouting rollup');
|
|
@@ -209,11 +209,11 @@ try {
|
|
|
209
209
|
|
|
210
210
|
// 4b — the live hide toggle (R03, task 1002279): flipping hide_stats takes
|
|
211
211
|
// effect IMMEDIATELY on every public read — the profile port and the
|
|
212
|
-
// /
|
|
212
|
+
// /scouting rollup — while the self view keeps serving.
|
|
213
213
|
await pi.setAccountHideStats({ githubId: ACTIVE, githubLogin: 'active-b', hide: true }, { pool });
|
|
214
214
|
ok(await pi.getPublicProfileExtras(ACTIVE, { pool }) === null, 'hide ON → public read drops to null immediately');
|
|
215
215
|
const hiddenListed = (await pi.listScoutingRollup({ limit: 500, pool })).map((r) => Number(r.github_id));
|
|
216
|
-
ok(!hiddenListed.includes(ACTIVE), 'hide ON → the builder drops out of /
|
|
216
|
+
ok(!hiddenListed.includes(ACTIVE), 'hide ON → the builder drops out of /scouting immediately');
|
|
217
217
|
const stillOwn = await pi.getOwnProfileExtras(ACTIVE, { pool });
|
|
218
218
|
ok(stillOwn && stillOwn.totals.credits === 175, 'hide ON → the self view still serves their own numbers');
|
|
219
219
|
let hiddenAcct = await pi.getAccount(ACTIVE, { pool });
|
package/tests/profile_route.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// 2. flipping hide_stats removes it on the IMMEDIATE next load, and the
|
|
19
19
|
// response is `Cache-Control: no-store` — the old `public, max-age=60`
|
|
20
20
|
// would keep serving the pre-hide body for a minute (ADR 0171 D5);
|
|
21
|
-
// 3. `/scouting` (the
|
|
21
|
+
// 3. `/scouting` (the recruiting directory's endpoint) drops the
|
|
22
22
|
// hidden builder too — enforcement is on every read path, not the profile;
|
|
23
23
|
// 4. the leak guard BITES: planting a private field turns the key-list and
|
|
24
24
|
// sentinel assertions red (a leak test that cannot fail is not a test);
|
|
@@ -35,14 +35,9 @@
|
|
|
35
35
|
import { strict as assert } from 'node:assert';
|
|
36
36
|
import { createRequire } from 'node:module';
|
|
37
37
|
import http from 'node:http';
|
|
38
|
-
import { readFileSync } from 'node:fs';
|
|
39
|
-
import { fileURLToPath } from 'node:url';
|
|
40
|
-
import { dirname, join } from 'node:path';
|
|
41
38
|
|
|
42
39
|
const require = createRequire(import.meta.url);
|
|
43
40
|
const express = require('express');
|
|
44
|
-
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
-
const sourceOf = (rel) => readFileSync(join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
|
|
46
41
|
|
|
47
42
|
let passed = 0;
|
|
48
43
|
let failed = 0;
|
|
@@ -410,8 +405,8 @@ const engine = {
|
|
|
410
405
|
const sql = String(text);
|
|
411
406
|
engine.calls.push({ sql, params });
|
|
412
407
|
|
|
413
|
-
// listScoutingRollup — the ONE read behind GET /scouting
|
|
414
|
-
//
|
|
408
|
+
// listScoutingRollup — the ONE read behind GET /scouting. Honors exactly
|
|
409
|
+
// the predicates the query declares.
|
|
415
410
|
if (/AS project_count/.test(sql)) {
|
|
416
411
|
let rows = SCOUTING_TABLE;
|
|
417
412
|
if (/pa\.profile_state = 'active'/.test(sql)) rows = rows.filter((r) => r.profile_state === 'active');
|
|
@@ -666,24 +661,6 @@ await t('R06.3 GET /scouting ranks the visible builder, lists the HIDDEN one by
|
|
|
666
661
|
}
|
|
667
662
|
});
|
|
668
663
|
|
|
669
|
-
await t('R06.3 the /people page reads /scouting and nothing else', async () => {
|
|
670
|
-
// /people is the hall's render of the same endpoint, so whatever /scouting
|
|
671
|
-
// drops, /people drops. Pin that it has no second data source.
|
|
672
|
-
//
|
|
673
|
-
// MATCHED AS AN EXPRESSION, NOT A STRING LITERAL. This used to capture the
|
|
674
|
-
// whole URL out of `fetch('…')` and compare it to one hardcoded string, which
|
|
675
|
-
// pinned the row limit as a side effect of pinning the ENDPOINT — so task
|
|
676
|
-
// 1003110 naming that limit (`DIRECTORY_LIMIT`, the one constant that feeds
|
|
677
|
-
// both the request and the "showing the first N" copy) failed a test about
|
|
678
|
-
// data sources. What is load-bearing here is the count and the destination:
|
|
679
|
-
// ONE read, and it goes to the scouting rollup. How the query string is
|
|
680
|
-
// assembled is the page's business.
|
|
681
|
-
const args = [...sourceOf('modules/hall-ui/public/people.js').matchAll(/\bfetch\(\s*([^,)]+)/g)].map((m) => m[1].trim());
|
|
682
|
-
assert.equal(args.length, 1, '/people has exactly one data source');
|
|
683
|
-
assert.match(args[0], /^'\/api\/bongos\/scouting\?limit='/,
|
|
684
|
-
'and it is the scouting rollup — nothing may re-enter through a second endpoint');
|
|
685
|
-
});
|
|
686
|
-
|
|
687
664
|
scout.server.close();
|
|
688
665
|
server.close();
|
|
689
666
|
|
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
// WHO APPEARS IN IT. The recruiting opt-out (`recruiter_discoverable`, privacy
|
|
13
13
|
// spec D1) gated nothing until this task: a public account that had opted out
|
|
14
14
|
// still appeared. listScoutingRollup now joins scoutingListableSql, so an
|
|
15
|
-
// opted-out builder is absent from /scouting
|
|
16
|
-
//
|
|
17
|
-
// twice — and flipping the toggle back restores them.
|
|
15
|
+
// opted-out builder is absent from /scouting — its one reader's predicate is
|
|
16
|
+
// proved from source below — and flipping the toggle back restores them.
|
|
18
17
|
//
|
|
19
18
|
// AND NEITHER AXIS TOUCHES THE OTHER (ADR 0016). Being scoutable is not
|
|
20
19
|
// authority and opting out is not a punishment: an opted-out builder who owns
|
|
@@ -641,13 +640,10 @@ await t('CALIBRATION: the interpreter refuses what it cannot model, rather than
|
|
|
641
640
|
));
|
|
642
641
|
});
|
|
643
642
|
|
|
644
|
-
await t('
|
|
645
|
-
const page = fs.readFileSync(path.join(repo, 'modules/hall-ui/public/people.js'), 'utf8');
|
|
646
|
-
assert.match(page, /fetch\('\/api\/bongos\/scouting/,
|
|
647
|
-
'the hall directory reads GET /scouting; if it ever grows its own query, this proof stops covering it');
|
|
643
|
+
await t('listScoutingRollup, the one reader behind GET /scouting, joins the composed predicate', async () => {
|
|
648
644
|
const source = fs.readFileSync(path.join(repo, 'modules/platform-identity/platform-identity.js'), 'utf8');
|
|
649
645
|
assert.match(source, /scoutingListableSql\('pa'\)/,
|
|
650
|
-
'
|
|
646
|
+
'the reader joins scoutingListableSql, so "absent from /scouting" holds by construction');
|
|
651
647
|
});
|
|
652
648
|
|
|
653
649
|
// ===========================================================================
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>People — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The builder directory: everyone building across {{worldName}} projects, searchable and ranked by track record.">
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
-
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<!-- task 1003470: the identity cell (avatar, name, login) is the one thing the
|
|
13
|
-
directory adds to the shell's ledger. -->
|
|
14
|
-
<link rel="stylesheet" href="/builders/people.css">
|
|
15
|
-
</head>
|
|
16
|
-
<body data-page="people" data-page-title="People">
|
|
17
|
-
|
|
18
|
-
<div class="app">
|
|
19
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
20
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
21
|
-
<div class="app-body">
|
|
22
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
23
|
-
<div class="app-content">
|
|
24
|
-
<!-- The board page head (task 1003307): the label voice, because on a
|
|
25
|
-
working surface the page's own name is the least interesting thing on
|
|
26
|
-
it. The sub is rewritten by people.js once the directory has loaded. -->
|
|
27
|
-
<div class="page-head page-head--board">
|
|
28
|
-
<h1 class="page-head__h">People</h1>
|
|
29
|
-
<p class="page-head__sub" id="people-sub">Everyone building across hosted projects, ranked by track record. Descriptive only — cross-project numbers never carry authority here.</p>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<main id="hall-main">
|
|
33
|
-
<!-- task 1003470: ONE directory. Its legacy door /scouting rendered this
|
|
34
|
-
same page from the same script, and task 1003440 deleted it — the same
|
|
35
|
-
/api/bongos/scouting rollup used to be drawn three different ways. -->
|
|
36
|
-
<section class="scroll" id="people-scroll" aria-labelledby="people-h">
|
|
37
|
-
<h2 id="people-h" class="scroll__h">Builder directory<span class="count-pill" id="people-count" hidden></span></h2>
|
|
38
|
-
<div id="people-filters"></div>
|
|
39
|
-
<div id="people-body">
|
|
40
|
-
<div class="profile__placeholder">Loading the directory…</div>
|
|
41
|
-
</div>
|
|
42
|
-
<!-- task 1003110: the clipped-directory footnote. Hidden unless the
|
|
43
|
-
response came back FULL, which is the only truncation signal the
|
|
44
|
-
rollup offers; people.js writes it. -->
|
|
45
|
-
<p class="ledger__note" id="people-foot" hidden></p>
|
|
46
|
-
</section>
|
|
47
|
-
|
|
48
|
-
<!-- task 1002291 (ADR 0334): builders who hid their cross-project record
|
|
49
|
-
but are still open to recruiting, listed by name alone (privacy spec
|
|
50
|
-
D2). Its own panel because they are never ranked; hidden unless the
|
|
51
|
-
response carries a row, and people.js writes it. -->
|
|
52
|
-
<section class="scroll" id="people-named-scroll" aria-labelledby="people-named-h" hidden>
|
|
53
|
-
<h2 id="people-named-h" class="scroll__h">Listed by name only<span class="count-pill" id="people-named-count" hidden></span></h2>
|
|
54
|
-
<p class="scroll__lede" id="people-named-lede">These builders are open to recruiting but keep their cross-project record hidden, so only who they are is shown. They are never ranked, and a role filter never finds them.</p>
|
|
55
|
-
<div id="people-named-body"></div>
|
|
56
|
-
<p class="ledger__note" id="people-named-foot" hidden></p>
|
|
57
|
-
</section>
|
|
58
|
-
</main>
|
|
59
|
-
|
|
60
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
66
|
-
|
|
67
|
-
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
68
|
-
chrome exists before the page script runs. -->
|
|
69
|
-
<script src="/builders/shell.js?v=2026-09-26-swupdate"></script>
|
|
70
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
71
|
-
<script src="/builders/palette.js" defer></script>
|
|
72
|
-
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
73
|
-
branding pack's currency label, loaded BEFORE people.js. -->
|
|
74
|
-
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
75
|
-
<script src="/builders/brand-apply.js"></script>
|
|
76
|
-
<script src="/builders/hall-kit.js"></script>
|
|
77
|
-
<script src="/builders/people.js"></script>
|
|
78
|
-
</body>
|
|
79
|
-
</html>
|