@bongos/core 1.19.1045 → 1.19.1046
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 +87 -112
- 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 +2 -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 +6 -0
- 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/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
|
@@ -680,7 +680,7 @@ await ta('listScoutingRollup scalar aggregation is fan-out-free (task 1002911)',
|
|
|
680
680
|
});
|
|
681
681
|
|
|
682
682
|
await ta('listScoutingRollup enforces consent IN the query (ADR 0171 D2/D3)', async () => {
|
|
683
|
-
// /
|
|
683
|
+
// /scouting must honor hide BY CONSTRUCTION: the predicate lives
|
|
684
684
|
// in the scalar-aggregation CTE, not in any caller. Semantics proven against
|
|
685
685
|
// real SQL by tests/profile_rollup_consent.mjs.
|
|
686
686
|
const pool = mockPool([{ rows: [] }]);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1046",
|
|
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.1046",
|
|
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.1046",
|
|
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/release-notes.json
CHANGED
|
@@ -7571,5 +7571,11 @@
|
|
|
7571
7571
|
"id": "1003458",
|
|
7572
7572
|
"text": "The Tasks board and the Goals page now claim and release work through one shared piece of code instead of two copies that had already drifted apart — the same refusal was worded differently on each page, and one had lost its \""
|
|
7573
7573
|
}
|
|
7574
|
+
],
|
|
7575
|
+
"1.19.1046": [
|
|
7576
|
+
{
|
|
7577
|
+
"id": "1003444",
|
|
7578
|
+
"text": "The hall's People page is gone, as you asked. An old People link now opens the Builders page instead, and People is no longer in the side menu. Searching for builders across projects comes back when the hub's community page is"
|
|
7579
|
+
}
|
|
7574
7580
|
]
|
|
7575
7581
|
}
|
package/src/bongos/auth.js
CHANGED
|
@@ -425,17 +425,14 @@ function signInWithReturn(req) {
|
|
|
425
425
|
const PAGE_GATES = Object.freeze({
|
|
426
426
|
requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
|
|
427
427
|
requireGovernmentPage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
|
|
428
|
-
// `alsoAdmits` — a SECOND, non-rank audience
|
|
429
|
-
// the page can follow it (ADR 0210
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
requireProjectCuratePage: Object.freeze({
|
|
437
|
-
permission: 'project.curate', deniedTo: '/builders', alsoAdmits: 'isProjectOwner',
|
|
438
|
-
}),
|
|
428
|
+
// `alsoAdmits` (on requireCorePinPage below) — a SECOND, non-rank audience a
|
|
429
|
+
// page's DATA ROUTE admits, so the page can follow it (ADR 0210). A page gate that
|
|
430
|
+
// could only ever ask about a permission would leave the widened audience staring
|
|
431
|
+
// at a redirect. The value names a function on the `platform-identity.hub` port —
|
|
432
|
+
// the module that owns the ownership fact — and is consulted ONLY after the
|
|
433
|
+
// permission check has already refused, so it can widen and never narrow. Absent
|
|
434
|
+
// port, absent function, or a throw: no admit. Its first user, the hall's /people
|
|
435
|
+
// page, was deleted by task 1003444 (ADR 0337 D5).
|
|
439
436
|
// The /board-room page (ADR 0266) — gated on `board.vote.cast`, the SAME atom
|
|
440
437
|
// the vote route checks, so the surface's reach equals the right to vote.
|
|
441
438
|
//
|
|
@@ -464,7 +461,7 @@ const PAGE_GATES = Object.freeze({
|
|
|
464
461
|
//
|
|
465
462
|
// SINCE ADR 0339 (task 1004174) its routes admit a SECOND audience — a hosted
|
|
466
463
|
// project's own owner, for that project only — so the shell follows through the same
|
|
467
|
-
// `alsoAdmits` door
|
|
464
|
+
// `alsoAdmits` door (ADR 0210). It admits the SHELL for an owner of any
|
|
468
465
|
// live project; which project they may read or move is still the routes' question,
|
|
469
466
|
// asked per request against that project's row.
|
|
470
467
|
requireCorePinPage: Object.freeze({ permission: 'core.pin.move', deniedTo: '/builders', alsoAdmits: 'isProjectOwner' }),
|
|
@@ -619,17 +616,6 @@ async function requireGovernmentPage(req, res, next) {
|
|
|
619
616
|
return runPageGate('requireGovernmentPage', req, res, next);
|
|
620
617
|
}
|
|
621
618
|
|
|
622
|
-
// The /people builder-directory page (task 1002577) — gated on the
|
|
623
|
-
// `project.curate` ATOM, the same key its one data route (GET /scouting,
|
|
624
|
-
// modules/platform-identity) checks, so the page layer can never drift wider
|
|
625
|
-
// than the API it fronts. R14 (goal 1000045, ADR 0210) widened that route to a
|
|
626
|
-
// SECOND audience that is not a rank at all — a project owner — so this gate
|
|
627
|
-
// carries the same OR through `alsoAdmits` above: the two doors of the page are
|
|
628
|
-
// exactly the two doors of the route, and neither layer can drift from it.
|
|
629
|
-
async function requireProjectCuratePage(req, res, next) {
|
|
630
|
-
return runPageGate('requireProjectCuratePage', req, res, next);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
619
|
// The /board-room page (ADR 0266) — gated on the `board.vote.cast` ATOM, the
|
|
634
620
|
// same key the vote route checks, so the page layer can never drift NARROWER
|
|
635
621
|
// than the API it fronts. That is the direction that matters here: the Board
|
|
@@ -940,7 +926,6 @@ module.exports = {
|
|
|
940
926
|
requireBuilder,
|
|
941
927
|
requireBuilderPage,
|
|
942
928
|
requireGovernmentPage,
|
|
943
|
-
requireProjectCuratePage,
|
|
944
929
|
requireBoardVotePage,
|
|
945
930
|
requireCorePinPage,
|
|
946
931
|
requireFleetPage,
|
|
@@ -936,17 +936,13 @@ function mountInternalSurfaces(app) {
|
|
|
936
936
|
// statically, and a comment in that gap drops the regex out of the map, which
|
|
937
937
|
// makes the page test as UNGATED while looking gated here. Prose goes above.
|
|
938
938
|
const BOARD_ROOM_PAGE_RE = /^\/builders\/board-room(?:\.html)?\/?$/;
|
|
939
|
-
// The /people builder directory (task 1002577): gated on `project.curate`, the
|
|
940
|
-
// exact permission its data route (GET /scouting) checks — same posture as the
|
|
941
|
-
// /government tab above (the page can never drift wider than the API it fronts).
|
|
942
|
-
const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
|
|
943
939
|
// /deploy — the owner's deploy door (task 1003159, ADR 0293). Gated on the
|
|
944
940
|
// `core.pin.move` atom, the same key its preview and its move both check, so the
|
|
945
941
|
// shell's reach equals the right to press the button. Archon-floor and system:true,
|
|
946
942
|
// which makes this the narrowest page gate here — correct for the one surface whose
|
|
947
943
|
// button restarts the live service and changes what code the platform runs.
|
|
948
944
|
//
|
|
949
|
-
// DECLARED HERE, WITH THE OTHER GATES, AND NOT BELOW: the
|
|
945
|
+
// DECLARED HERE, WITH THE OTHER GATES, AND NOT BELOW: the RETIRED_DIRECTORY_RE block
|
|
950
946
|
// that follows is a matcher immediately followed by the redirect it feeds, and
|
|
951
947
|
// tests/hall_roster_world.mjs reads them as one span. Wedging a declaration between
|
|
952
948
|
// the two separates a matcher from its handler for a reader as well as for that test.
|
|
@@ -959,23 +955,24 @@ function mountInternalSurfaces(app) {
|
|
|
959
955
|
const DEPLOY_PAGE_RE = /^\/builders\/deploy(?:\.html)?\/?$/;
|
|
960
956
|
// The fleet cost page (task 1001949), on the atom its one read checks.
|
|
961
957
|
const FLEET_PAGE_RE = /^\/builders\/fleet(?:\.html)?\/?$/;
|
|
962
|
-
// /
|
|
963
|
-
// directory
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
// nothing, and /
|
|
970
|
-
|
|
958
|
+
// /people and its legacy twin /scouting were the hall's cross-project builder
|
|
959
|
+
// directory. Task 1003440 deleted the /scouting door; task 1003444 deleted
|
|
960
|
+
// /people itself (owner call, 2026-09-25 — ADR 0337 D5): cross-project
|
|
961
|
+
// discovery belongs to the hub, and /roster is the hall's directory. Both URLs
|
|
962
|
+
// survive as redirects to /roster so a bookmark lands somewhere instead of a
|
|
963
|
+
// 404. 302, not 301: /roster is interim until the hub community page (task
|
|
964
|
+
// 1003363) ships, and a browser caches a 301 past any re-point. No page gate:
|
|
965
|
+
// the redirect discloses nothing, and /roster is deliberately public.
|
|
966
|
+
// GET /scouting's own gate (platform-identity) is untouched by this.
|
|
967
|
+
const RETIRED_DIRECTORY_RE = /^\/builders\/(?:people|scouting)(?:\.html)?\/?$/;
|
|
971
968
|
app.use((req, res, next) => {
|
|
972
969
|
if (isModuleSurfaceHost(req)) return next();
|
|
973
970
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
974
|
-
if (!
|
|
971
|
+
if (!RETIRED_DIRECTORY_RE.test(path.posix.normalize(req.path))) return next();
|
|
975
972
|
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
976
973
|
// On the builders subdomain the /builders prefix is redundant (the canonical
|
|
977
974
|
// redirect above strips it), so emit the clean path and save a second hop.
|
|
978
|
-
return res.redirect(
|
|
975
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/roster${qs}`);
|
|
979
976
|
});
|
|
980
977
|
|
|
981
978
|
// /deploy EXISTS ONLY WHERE THE DOOR DOES (task 1004296). The owner, 2026-09-25: /deploy is
|
|
@@ -1019,9 +1016,6 @@ function mountInternalSurfaces(app) {
|
|
|
1019
1016
|
if (BOARD_ROOM_PAGE_RE.test(norm)) {
|
|
1020
1017
|
return gdsAuth.requireBoardVotePage(req, res, next);
|
|
1021
1018
|
}
|
|
1022
|
-
if (PEOPLE_PAGE_RE.test(norm)) {
|
|
1023
|
-
return gdsAuth.requireProjectCuratePage(req, res, next);
|
|
1024
|
-
}
|
|
1025
1019
|
// ADR 0293: the deploy door, on the same atom its two routes check.
|
|
1026
1020
|
if (DEPLOY_PAGE_RE.test(norm)) {
|
|
1027
1021
|
return gdsAuth.requireCorePinPage(req, res, next);
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1046'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -418,7 +418,7 @@ test('the recruiting surface really is exactly two reads, and each one joins REA
|
|
|
418
418
|
const av = readFileSync(join(ROOT, 'modules/platform-identity/account-visibility.js'), 'utf8');
|
|
419
419
|
// The directory reads TWO tiers since task 1002291 — ranked, and the hidden
|
|
420
420
|
// builders it lists by name — and the opt-out must empty both, or "removes you
|
|
421
|
-
// from the
|
|
421
|
+
// from the recruiting directory" is false for a builder who also hid their record.
|
|
422
422
|
for (const composed of ['scoutingListableSql', 'scoutingNameOnlySql', 'recruiterSliverVisibleSql']) {
|
|
423
423
|
const body = av.slice(av.indexOf(`function ${composed}(`));
|
|
424
424
|
assert.match(body.slice(0, body.indexOf('\n}')), /recruiterReachSql\(alias\)/,
|
|
@@ -93,22 +93,22 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
|
|
|
93
93
|
// rot: a NEW bare `async function require*` added later fails this test.
|
|
94
94
|
// requireBuilder — the API gate. Guarded here (BV1.R109-follow-up).
|
|
95
95
|
// requireBuilderPage \
|
|
96
|
-
// requireGovernmentPage \ all
|
|
97
|
-
//
|
|
98
|
-
// requireBoardVotePage / ADR 0266 /board-room vote gate)
|
|
96
|
+
// requireGovernmentPage \ all delegate to runPageGate, guarded by
|
|
97
|
+
// requireBoardVotePage / BV1.R107 (and the ADR 0266 /board-room vote gate)
|
|
99
98
|
// requireCorePinPage / ADR 0293's /deploy door — the narrowest of them,
|
|
100
99
|
// on the archon-floor `core.pin.move` atom.
|
|
101
100
|
// requireFleetPage — task 1001949's /fleet cost page, same delegation.
|
|
102
101
|
// Task 1003450 removed two more — requireNonXenosPage and the R113
|
|
103
102
|
// requireGovernmentManagePage — which had both ended up dispatched from no
|
|
104
|
-
// route at all; tests/hall_page_gate_map.mjs now fails if that recurs.
|
|
103
|
+
// route at all; tests/hall_page_gate_map.mjs now fails if that recurs. Task
|
|
104
|
+
// 1003444 removed requireProjectCuratePage with the /people page it fronted.
|
|
105
105
|
// Anything else async and router-mounted must be added with its own try/catch.
|
|
106
106
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
|
|
107
107
|
const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
|
|
108
|
-
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireFleetPage', 'requireGovernmentPage'
|
|
108
|
+
assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireFleetPage', 'requireGovernmentPage'],
|
|
109
109
|
'a new async router middleware appeared — give it a try/catch and add it here');
|
|
110
110
|
// The page gates must actually delegate (that is WHY they are safe).
|
|
111
|
-
for (const name of ['requireBuilderPage', 'requireGovernmentPage', '
|
|
111
|
+
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireBoardVotePage', 'requireCorePinPage', 'requireFleetPage']) {
|
|
112
112
|
const fn = src.slice(src.indexOf(`async function ${name}`));
|
|
113
113
|
assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
|
|
114
114
|
}
|
|
@@ -28,7 +28,7 @@ const require = createRequire(import.meta.url);
|
|
|
28
28
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
29
|
|
|
30
30
|
// The session double must be installed BEFORE auth.js loads: auth.js destructures
|
|
31
|
-
// lookupSession at require time (the tests/
|
|
31
|
+
// lookupSession at require time (the tests/page_gate_second_door.mjs precedent).
|
|
32
32
|
const dbKernel = require('../src/bongos/db-kernel.js');
|
|
33
33
|
let nextSession = null;
|
|
34
34
|
dbKernel.lookupSession = async () => nextSession;
|
package/tests/edit_hints.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const hints = require(HOOK);
|
|
|
21
21
|
|
|
22
22
|
test('each generator input maps to its generator; unrelated paths map to nothing', () => {
|
|
23
23
|
const cases = [
|
|
24
|
-
['modules/hall-ui/public/
|
|
24
|
+
['modules/hall-ui/public/roster.js', 'copy-inventory.js'],
|
|
25
25
|
['modules/status-ui/public/index.html', 'copy-inventory.js'],
|
|
26
26
|
['src/bongos/auth.js', 'gen-repo-map.js'],
|
|
27
27
|
['modules/lifecycle/routes/claims.js', 'gen-api-docs.js'],
|
|
@@ -39,12 +39,12 @@ test('each generator input maps to its generator; unrelated paths map to nothing
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
test('rel() reconciles C:\\, C:/ and /c/ shapes between CLAUDE_PROJECT_DIR and file_path', () => {
|
|
42
|
-
const want = 'modules/hall-ui/public/
|
|
43
|
-
assert.equal(hints.rel('C:\\r\\repo\\modules\\hall-ui\\public\\
|
|
44
|
-
assert.equal(hints.rel('C:\\r\\repo\\modules\\hall-ui\\public\\
|
|
45
|
-
assert.equal(hints.rel('/c/r/repo/modules/hall-ui/public/
|
|
46
|
-
assert.equal(hints.rel('c:/r/repo/modules/hall-ui/public/
|
|
47
|
-
assert.equal(hints.rel('/home/u/repo/modules/hall-ui/public/
|
|
42
|
+
const want = 'modules/hall-ui/public/roster.js';
|
|
43
|
+
assert.equal(hints.rel('C:\\r\\repo\\modules\\hall-ui\\public\\roster.js', 'C:\\r\\repo'), want);
|
|
44
|
+
assert.equal(hints.rel('C:\\r\\repo\\modules\\hall-ui\\public\\roster.js', 'C:/r/repo'), want);
|
|
45
|
+
assert.equal(hints.rel('/c/r/repo/modules/hall-ui/public/roster.js', 'C:\\r\\repo'), want);
|
|
46
|
+
assert.equal(hints.rel('c:/r/repo/modules/hall-ui/public/roster.js', 'C:/r/repo/'), want);
|
|
47
|
+
assert.equal(hints.rel('/home/u/repo/modules/hall-ui/public/roster.js', '/home/u/repo'), want);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('run as a hook: prints one hint for a generator input, nothing for an unrelated file, exit 0 both times', () => {
|
|
@@ -53,7 +53,7 @@ test('run as a hook: prints one hint for a generator input, nothing for an unrel
|
|
|
53
53
|
input: JSON.stringify({ tool_input: { file_path: path.join(projectDir, ...relPath.split('/')) } }),
|
|
54
54
|
encoding: 'utf8', timeout: 15000, env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
55
55
|
});
|
|
56
|
-
const a = run('modules/hall-ui/public/
|
|
56
|
+
const a = run('modules/hall-ui/public/roster.js');
|
|
57
57
|
assert.equal(a.status, 0, a.stderr);
|
|
58
58
|
assert.match(a.stdout, /copy-registry\.json is now stale/);
|
|
59
59
|
assert.match(a.stdout, /copy-inventory\.js/);
|
|
@@ -1122,7 +1122,7 @@ await ta('private accounts are absent from the leaderboard/directory rollup', as
|
|
|
1122
1122
|
const rows = await pi.listScoutingRollup({ limit: 50, pool });
|
|
1123
1123
|
const shown = rows.map((r) => r.github_login).sort();
|
|
1124
1124
|
assert.deepEqual(shown, ['octo', 'shifted'],
|
|
1125
|
-
'only active, publicly-visible, unhidden accounts reach /
|
|
1125
|
+
'only active, publicly-visible, unhidden accounts reach /scouting — '
|
|
1126
1126
|
+ `got ${JSON.stringify(shown)}; ghost/scout are private, shy is hidden, newbie is provisional, moved is private, legacy never accepted the terms (ADR 0213)`);
|
|
1127
1127
|
});
|
|
1128
1128
|
|
|
@@ -280,7 +280,7 @@ function parsePageGates(src) {
|
|
|
280
280
|
// Each entry's own object literal is bounded by [^}]* so the scan cannot run past a
|
|
281
281
|
// gate that lacks a `permission` key and silently adopt the NEXT gate's. `permission`
|
|
282
282
|
// is then found ANYWHERE inside that literal rather than assumed to come first —
|
|
283
|
-
//
|
|
283
|
+
// every gate carries a `deniedTo` beside it, and key order in an object
|
|
284
284
|
// literal is not a contract anyone maintains.
|
|
285
285
|
for (const m of block[1].matchAll(/(require[A-Za-z]*Page):\s*Object\.freeze\(\{([^}]*)\}\)/g)) {
|
|
286
286
|
const perm = m[2].match(/permission:\s*'([^']+)'/);
|
|
@@ -223,9 +223,9 @@ test('B: a payload with no id degrades to the place alone', async () => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
test('B: the id wears the same mono/faint voice as the login', () => {
|
|
226
|
-
const css = read('
|
|
226
|
+
const css = read('roster.css');
|
|
227
227
|
const i = css.indexOf('.ledger__place .ledger__id {');
|
|
228
|
-
assert.ok(i > 0, '
|
|
228
|
+
assert.ok(i > 0, 'roster.css styles the roster id');
|
|
229
229
|
const block = css.slice(i, css.indexOf('}', i));
|
|
230
230
|
for (const token of ['var(--font-mono)', 'var(--ink-faint)']) {
|
|
231
231
|
assert.ok(block.includes(token), `the id spends ${token}, the voice .dir-login already uses`);
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -266,10 +266,9 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
266
266
|
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'ideas', 'thinking', 'copy-desk', 'studio'],
|
|
267
267
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
268
268
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// was stricter than the wall). requireProjectCuratePage is the wall server-side.
|
|
269
|
+
// task 1003444: 'people' LEFT the group — the owner deleted the hall's
|
|
270
|
+
// cross-project directory (ADR 0337 D5); its home is the hub, and /people
|
|
271
|
+
// redirects to /roster. Builders is the hall's directory now.
|
|
273
272
|
// task 1003172 (C02 of goal 1000075): 'collab' joins Community, FIRST. The
|
|
274
273
|
// group is picked by whose data it is, and a help request is another builder
|
|
275
274
|
// reaching you — not your own numbers (that is You), and not reference (Docs).
|
|
@@ -280,7 +279,8 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
280
279
|
// changed: it was a hash view on index.html (href /#leaderboard, view:
|
|
281
280
|
// 'leaderboard') and is now a real page at /roster labelled Builders. That is
|
|
282
281
|
// what made this group read as one real page plus a link into the home page.
|
|
283
|
-
assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'roster'
|
|
282
|
+
assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'roster'], 'Community = everyone else, in this project');
|
|
283
|
+
assert.ok(!nav.flatMap((g) => g.items).some((i) => /^\/people\b/.test(i.href)), 'no nav item points at the deleted /people');
|
|
284
284
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
285
285
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
286
286
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
@@ -343,11 +343,11 @@ test('THE CORRECTION: Community shows a xenos two items, Collab and Builders', (
|
|
|
343
343
|
test('neither Community page hides behind a rank the page itself does not enforce', () => {
|
|
344
344
|
// The invariant this task exists to restore: a nav flag may be narrower than
|
|
345
345
|
// the wall for cosmetic reasons (/profile does it), but it must never assert a
|
|
346
|
-
// RANK the page gate does not. /people
|
|
347
|
-
//
|
|
346
|
+
// RANK the page gate does not. /people was the one item with a legitimate rank
|
|
347
|
+
// flag; it left the hall in task 1003444, so the group now carries none.
|
|
348
348
|
const community = parseNav(boot().sidebarHtml).find((g) => g.label === 'Community');
|
|
349
349
|
const ranked = community.items.filter((i) => RANK_GATES.has(i.gate)).map((i) => i.id);
|
|
350
|
-
assert.deepEqual(ranked, [
|
|
350
|
+
assert.deepEqual(ranked, [], 'no Community item carries a rank flag');
|
|
351
351
|
});
|
|
352
352
|
|
|
353
353
|
test('no group head is gated wider than its own items', () => {
|
|
@@ -36,7 +36,6 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
36
36
|
// functions in src/bongos/auth.js):
|
|
37
37
|
// requireBuilderPage page.view.public any signed-in builder (xenos+)
|
|
38
38
|
// requireGovernmentPage page.view.government metic+ (ADR 0157)
|
|
39
|
-
// requireProjectCuratePage project.curate metic+
|
|
40
39
|
// requireBoardVotePage board.vote.cast xenos+ (ADR 0266)
|
|
41
40
|
// That list is now CHECKED, not just written: "every page gate is dispatched"
|
|
42
41
|
// below reads the gate table out of auth.js and fails if a gate here guards no
|
|
@@ -107,12 +106,13 @@ const ORACLE = {
|
|
|
107
106
|
// The fleet cost page (task 1001949), on the atom its one read checks.
|
|
108
107
|
'/builders/fleet': 'requireFleetPage',
|
|
109
108
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// legacy twin /
|
|
113
|
-
// /
|
|
114
|
-
//
|
|
115
|
-
|
|
109
|
+
// Gone (task 1003444, owner call 2026-09-25 — ADR 0337 D5): the cross-project
|
|
110
|
+
// directory left the hall, and /people now redirects to /roster before this
|
|
111
|
+
// dispatch runs, like its legacy twin /scouting (task 1003440).
|
|
112
|
+
// tests/hall_roster_world.mjs owns the proof that the redirect is there. The
|
|
113
|
+
// page gate that fronted it (requireProjectCuratePage) went with it; what
|
|
114
|
+
// still holds is GET /scouting's own gate, in the platform-identity module.
|
|
115
|
+
'/builders/people': null,
|
|
116
116
|
|
|
117
117
|
// Metic+ since R16 (goal 1000069): the Government page is three rooms —
|
|
118
118
|
// Permissions · Board Room · Constitution (reordered, task 1003269) — and only
|
|
@@ -138,8 +138,8 @@ const ORACLE = {
|
|
|
138
138
|
// /roster joins them (task 1003441): it is this project’s own ledger, and its
|
|
139
139
|
// one data source — GET /leaderboard — is deliberately unauthenticated,
|
|
140
140
|
// mirroring the public status surface. The page must not be gated tighter than
|
|
141
|
-
// the API it fronts, which is the same rule that puts /
|
|
142
|
-
//
|
|
141
|
+
// the API it fronts, which is the same rule that puts /deploy at
|
|
142
|
+
// core.pin.move. It was reachable to a stranger as the #leaderboard view on
|
|
143
143
|
// the ungated index, so this preserves the audience rather than widening it.
|
|
144
144
|
'/builders/roster': null,
|
|
145
145
|
'/builders/': null,
|
|
@@ -310,6 +310,9 @@ const GONE_PAGES = {
|
|
|
310
310
|
// Task 1004231 — the ideator's sky replaced both the room and the old sky with
|
|
311
311
|
// one page at /thinking; /sky is a 301 to it, not a page.
|
|
312
312
|
'/builders/sky': 'task 1004231 — the sky lives at /thinking now; /sky redirects there',
|
|
313
|
+
// Task 1003444 — the owner deleted the hall's cross-project directory; the hub
|
|
314
|
+
// community page (task 1003363) is its home, and /people redirects to /roster.
|
|
315
|
+
'/builders/people': 'task 1003444 — the hall has no cross-project directory; /people redirects to /roster',
|
|
313
316
|
};
|
|
314
317
|
|
|
315
318
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -344,8 +347,8 @@ const AUTH_SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/auth.js'), 'utf8');
|
|
|
344
347
|
function readPageGateNames(src) {
|
|
345
348
|
const table = /const PAGE_GATES = Object\.freeze\(\{([\s\S]*?)\n\}\);/.exec(src);
|
|
346
349
|
assert.ok(table, 'PAGE_GATES is still a `const PAGE_GATES = Object.freeze({…});` literal');
|
|
347
|
-
// Entry keys sit at exactly one indent level; `permission:` / `deniedTo:`
|
|
348
|
-
//
|
|
350
|
+
// Entry keys sit at exactly one indent level; `permission:` / `deniedTo:`
|
|
351
|
+
// are nested deeper, so the two-space anchor keeps them out.
|
|
349
352
|
return [...table[1].matchAll(/^ {2}(require\w+):/gm)].map((m) => m[1]);
|
|
350
353
|
}
|
|
351
354
|
|