@bongos/core 1.21.33 → 1.21.35
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 +51 -41
- package/docs/copy-inventory.md +19 -17
- package/docs/copy-registry.json +39 -21
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -1
- package/docs/page-readings.json +322 -194
- package/modules/autonomy/governor-docket.js +68 -1
- package/modules/government/docket-floors.js +20 -0
- package/modules/government/docket-vocab.js +14 -0
- package/modules/government/docket.js +103 -2
- package/modules/government/routes/government.js +13 -0
- package/modules/hall-ui/public/street-card.js +97 -1
- package/modules/hall-ui/public/street-desk.js +386 -0
- package/modules/hall-ui/public/street-inside.js +14 -10
- package/modules/hall-ui/public/street.css +60 -0
- package/modules/hall-ui/public/street.html +9 -3
- package/modules/hall-ui/public/street.js +238 -17
- package/modules/hall-ui/public/street.states.json +314 -6
- package/modules/lifecycle/governor-docket.js +24 -2
- package/modules/lifecycle/routes/project-settings.js +24 -0
- package/modules/onboarding/routes/access-requests.js +7 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +36 -1
- package/modules/security/routes/reports.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +40 -0
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/hall_matter_card.mjs +12 -3
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_desk.mjs +557 -0
- package/tests/hall_street_inside.mjs +1 -1
|
@@ -58,10 +58,29 @@ function removalMatter(r) {
|
|
|
58
58
|
tally: null,
|
|
59
59
|
href: `/task/${encodeURIComponent(r.proposal_task_id)}`,
|
|
60
60
|
ref: { type: 'task', id: r.proposal_task_id },
|
|
61
|
+
// POST /removal-proposals/:id/decide, the docket source's own atom (task 1004559)
|
|
62
|
+
acts: [{ key: 'remove' }, { key: 'keep' }],
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
// The one-tap reasons a force sign-off may give (POST /done-when/:id/satisfy
|
|
67
|
+
// requires one); the desk sends the label as the override reason.
|
|
68
|
+
const FORCE_REASONS = Object.freeze([
|
|
69
|
+
{ key: 'tested_by_hand', label: 'tested by hand' },
|
|
70
|
+
{ key: 'checked_review', label: 'seen in review' },
|
|
71
|
+
{ key: 'no_longer', label: 'no longer applies' },
|
|
72
|
+
]);
|
|
73
|
+
// What the viewer may do on a criterion waiting for a tester (task 1004559). A
|
|
74
|
+
// backend-only criterion signs off in one tap (with the live attestation as its
|
|
75
|
+
// chip); a user-facing one needs a recording, so its act goes to the goal's page.
|
|
76
|
+
// Force sign-off only for a holder of criterion.satisfy, that route's own atom.
|
|
77
|
+
function uatActs(c, { force = false } = {}) {
|
|
78
|
+
const acts = [c.backend_only === true ? { key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] } : { key: 'record' }];
|
|
79
|
+
if (force) acts.push({ key: 'force', options: FORCE_REASONS });
|
|
80
|
+
return acts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function uatMatter(c, opts) {
|
|
65
84
|
return {
|
|
66
85
|
id: c.id,
|
|
67
86
|
rule: 'signoff',
|
|
@@ -75,6 +94,7 @@ function uatMatter(c) {
|
|
|
75
94
|
tally: { for: c.shipped_total, against: 0, open: 0, needs: 'one sign-off by someone who did not build it' },
|
|
76
95
|
href: c.goal_id ? `/goals#/goal/${encodeURIComponent(c.goal_id)}` : '/goals',
|
|
77
96
|
ref: { type: 'criterion', id: c.id },
|
|
97
|
+
acts: uatActs(c, opts),
|
|
78
98
|
};
|
|
79
99
|
}
|
|
80
100
|
|
|
@@ -326,7 +346,8 @@ function contributeLifecycleToDocket() {
|
|
|
326
346
|
source: 'criteria_uat',
|
|
327
347
|
building: 'review',
|
|
328
348
|
gate: { permission: 'criterion.review' },
|
|
329
|
-
read: async () => (await doneWhen.listPendingReviewCriteria({})).filter((c) => c.awaiting_uat === true)
|
|
349
|
+
read: async (ctx) => (await doneWhen.listPendingReviewCriteria({})).filter((c) => c.awaiting_uat === true)
|
|
350
|
+
.map((c) => uatMatter(c, { force: ctx.has('criterion.satisfy') })),
|
|
330
351
|
});
|
|
331
352
|
// Own-scoped by its read, like GET /goals/inbox: a goal's lead sees the requests
|
|
332
353
|
// to join their goals, and everyone sees their own pending ones. An Archon is
|
|
@@ -371,6 +392,7 @@ module.exports = {
|
|
|
371
392
|
overrideMatter,
|
|
372
393
|
removalMatter,
|
|
373
394
|
uatMatter,
|
|
395
|
+
uatActs,
|
|
374
396
|
joinRequestMatter,
|
|
375
397
|
flowReading,
|
|
376
398
|
promisesReading,
|
|
@@ -134,7 +134,29 @@ function upmarketRow() {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// The Rules desk's settings rows (task 1004559), under this page's own atom: the
|
|
138
|
+
// rot timer as one-tap day counts (PATCH /project-settings/rot-days), and the
|
|
139
|
+
// work categories as a link to this page, since a category is named in words.
|
|
140
|
+
// Pure; exported for tests.
|
|
141
|
+
const ROT_CHOICES = Object.freeze([7, 14, 30, 60, 90]);
|
|
142
|
+
function settingsDeskRows({ days, categories }) {
|
|
143
|
+
const live = (categories || []).filter((c) => c && c.active !== false).length;
|
|
144
|
+
return [
|
|
145
|
+
{ id: 'rot', name: 'Rot timer', fact: `stale after ${days} ${days === 1 ? 'day' : 'days'}`, facts: ['raising it only asks a person'], tool: true, ref: { type: 'project_setting', id: 'rot_days' },
|
|
146
|
+
acts: [{ key: 'rot', options: ROT_CHOICES.filter((d) => d !== Number(days)).slice(0, 4).map((d) => ({ key: String(d), label: `${d} days` })) }] },
|
|
147
|
+
{ id: 'categories', name: 'Work categories', fact: `${live} in use`, tool: true, href: '/project-settings', ref: { type: 'project_setting', id: 'work_categories' }, acts: [{ key: 'open' }] },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
function contributeSettingsDesk() {
|
|
151
|
+
api.contribute('governor.docket', {
|
|
152
|
+
desk: 'rules.settings',
|
|
153
|
+
gate: { permission: 'project.settings.manage' },
|
|
154
|
+
read: async () => settingsDeskRows({ days: (await api.resolveRotDays()).days, categories: await goalCategory.listCategories({ includeInactive: true }) }),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
module.exports = function buildProjectSettingsRouter() {
|
|
159
|
+
contributeSettingsDesk();
|
|
138
160
|
const router = express.Router();
|
|
139
161
|
|
|
140
162
|
// GET /project-settings — all five rows in ONE read.
|
|
@@ -261,3 +283,5 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
261
283
|
|
|
262
284
|
return router;
|
|
263
285
|
};
|
|
286
|
+
|
|
287
|
+
module.exports.settingsDeskRows = settingsDeskRows;
|
|
@@ -242,7 +242,12 @@ function accessRequestDocketMatter(r) {
|
|
|
242
242
|
// The governor's docket: a pending request to join stands at People, shown only
|
|
243
243
|
// to a viewer holding `access_request.review` (this queue's own atom, because it
|
|
244
244
|
// exposes would-be builders' usernames) and needing that same viewer's act.
|
|
245
|
+
// The People desk's invite row (task 1004559), under the invite route's own atom:
|
|
246
|
+
// one GitHub name typed in place, sent to POST /access-requests/invite.
|
|
247
|
+
const INVITE_DESK_ROW = Object.freeze({ id: 'invite', name: 'Invite someone', fact: 'by their GitHub name', tool: true, ref: { type: 'invite', id: 'new' }, acts: [{ key: 'invite', field: 'github_login' }] });
|
|
248
|
+
|
|
245
249
|
function contributeAccessRequestsToDocket() {
|
|
250
|
+
api.contribute('governor.docket', { desk: 'people.invite', gate: { permission: 'access_request.review' }, read: async () => [INVITE_DESK_ROW] });
|
|
246
251
|
api.contribute('governor.docket', {
|
|
247
252
|
source: 'access_requests',
|
|
248
253
|
building: 'people',
|
|
@@ -651,3 +656,5 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
651
656
|
};
|
|
652
657
|
|
|
653
658
|
module.exports.accessRequestDocketMatter = accessRequestDocketMatter;
|
|
659
|
+
|
|
660
|
+
module.exports.INVITE_DESK_ROW = INVITE_DESK_ROW;
|
|
@@ -545,5 +545,5 @@ function makeUpdateChannelHandler({ provisioning, pool, parseId, validateOrRespo
|
|
|
545
545
|
};
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
module.exports = { UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, selfUpdateChannel, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
548
|
+
module.exports = { loadPreview, UPDATE_CHANNELS, DEFAULT_UPDATE_CHANNEL, updateChannelOf, selfUpdateChannel, makeUpdateChannelHandler, CORE_UPGRADE_FROM, lastCoreMove, lastMoveSuperseded, parseRepoNote, preflightForOffer, UPGRADE_SHAPES, VERSION_RE, isCoreVersion, coreUpgradeFault, enqueueCoreUpgrade, makeCoreUpgradeHandler, previewFromSnapshot, makeCoreUpgradePreviewHandler, makeControlPlaneHandler };
|
|
549
549
|
|
|
@@ -25,12 +25,45 @@
|
|
|
25
25
|
const express = require('express');
|
|
26
26
|
const api = require('../../../src/module-api');
|
|
27
27
|
const provisioning = require('../provisioning');
|
|
28
|
-
const { makeCoreUpgradeHandler, makeCoreUpgradePreviewHandler, makeControlPlaneHandler, makeUpdateChannelHandler } = require('../core-upgrade');
|
|
28
|
+
const { makeCoreUpgradeHandler, makeCoreUpgradePreviewHandler, makeControlPlaneHandler, makeUpdateChannelHandler, loadPreview } = require('../core-upgrade');
|
|
29
29
|
const { requireOwnProjectOrPin } = require('../core-upgrade-authz');
|
|
30
30
|
|
|
31
31
|
const log = api.logger('provisioning');
|
|
32
32
|
|
|
33
|
+
// The Release desk's core row (task 1004559): the platform's own move, in two
|
|
34
|
+
// steps on one row. Until a dry run has PASSED for the very version on offer the
|
|
35
|
+
// row offers only 'check' (POST …/core-upgrade with dry_run); 'take' (the real
|
|
36
|
+
// move) is handed only after that pass, so no desk can move the pin without the
|
|
37
|
+
// check. The chip carries the version, which the route requires as its own
|
|
38
|
+
// confirmation. A run in progress offers nothing (the row then steps aside for
|
|
39
|
+
// the plain core-update matter). Pure; exported for tests.
|
|
40
|
+
function coreDeskRows(preview) {
|
|
41
|
+
const p = preview || {};
|
|
42
|
+
const to = p.recommended_version;
|
|
43
|
+
if (!p.behind || !to || p.instance_id == null) return [];
|
|
44
|
+
if (p.open_intent && (p.open_intent.state === 'pending' || p.open_intent.state === 'running')) return [];
|
|
45
|
+
const pf = p.preflight && p.preflight.target === to ? p.preflight : null;
|
|
46
|
+
const version = [{ key: String(to), label: String(to) }];
|
|
47
|
+
const base = { id: p.instance_id, name: `Core ${to}`, over: 'core_update', opened_at: p.observed_at || null, ref: { type: 'core_pin', id: p.instance_id } };
|
|
48
|
+
if (pf && pf.ok) return [{ ...base, fact: 'checked · nothing blocks', facts: [`running ${p.served_version || 'unknown'}`], acts: [{ key: 'take', options: version }, { key: 'check', options: version }] }];
|
|
49
|
+
if (pf) return [{ ...base, fact: 'the check refused it', facts: [String(pf.error || 'no reason given'), `running ${p.served_version || 'unknown'}`], acts: [{ key: 'check', options: version }] }];
|
|
50
|
+
return [{ ...base, fact: 'not checked yet', facts: [`running ${p.served_version || 'unknown'}`], acts: [{ key: 'check', options: version }] }];
|
|
51
|
+
}
|
|
52
|
+
function contributeCoreDesk() {
|
|
53
|
+
api.contribute('governor.docket', {
|
|
54
|
+
desk: 'release.core',
|
|
55
|
+
// the control-plane read's own atom (GET /provisioning/control-plane)
|
|
56
|
+
gate: { permission: 'core.pin.move' },
|
|
57
|
+
read: async () => {
|
|
58
|
+
const { rows } = await api.pool.query("SELECT * FROM provisioning_instances WHERE hosting_shape = 'control-plane' LIMIT 1");
|
|
59
|
+
if (!rows.length) return [];
|
|
60
|
+
return coreDeskRows(await loadPreview(api.pool, provisioning, rows[0]));
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
module.exports = function buildCoreUpgradeRouter() {
|
|
66
|
+
contributeCoreDesk();
|
|
34
67
|
const router = express.Router();
|
|
35
68
|
|
|
36
69
|
// POST /provisioning/instances/:id/core-upgrade — move the core pin.
|
|
@@ -119,3 +152,5 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
119
152
|
|
|
120
153
|
return router;
|
|
121
154
|
};
|
|
155
|
+
|
|
156
|
+
module.exports.coreDeskRows = coreDeskRows;
|
|
@@ -56,6 +56,23 @@ function redteamDocketMatter(r) {
|
|
|
56
56
|
tally: null,
|
|
57
57
|
href: '/watch',
|
|
58
58
|
ref: { type: 'vulnerability_report', id: r.id },
|
|
59
|
+
// The floor desk's acts (task 1004559): the docket only lists this for a
|
|
60
|
+
// holder of the adjudicate atom, which both routes check.
|
|
61
|
+
acts: [{ key: 'confirm' }, { key: 'dispute' }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A confirmed report still waiting for its fix, as a Disputes desk row (task
|
|
66
|
+
// 1004559): one act, POST /security/reports/:id/fix. Pure; exported for tests.
|
|
67
|
+
function confirmedDeskRow(r) {
|
|
68
|
+
return {
|
|
69
|
+
id: r.id,
|
|
70
|
+
name: r.title,
|
|
71
|
+
fact: r.severity ? `${r.severity} · confirmed` : 'confirmed',
|
|
72
|
+
facts: ['confirmed, not fixed yet'],
|
|
73
|
+
opened_at: r.confirmed_at || r.reported_at || null,
|
|
74
|
+
ref: { type: 'vulnerability_report', id: r.id },
|
|
75
|
+
acts: [{ key: 'fix' }],
|
|
59
76
|
};
|
|
60
77
|
}
|
|
61
78
|
|
|
@@ -69,6 +86,11 @@ function contributeReportsToDocket() {
|
|
|
69
86
|
return lists.flat().map(redteamDocketMatter);
|
|
70
87
|
},
|
|
71
88
|
});
|
|
89
|
+
api.contribute('governor.docket', {
|
|
90
|
+
desk: 'disputes.confirmed',
|
|
91
|
+
gate: { permission: 'security.report.adjudicate' },
|
|
92
|
+
read: async () => (await security.listVulnerabilityReports({ scope: 'all', status: 'confirmed', limit: 40 })).map(confirmedDeskRow),
|
|
93
|
+
});
|
|
72
94
|
// The Disputes record (task 1004527): every report that was ruled on, dated by
|
|
73
95
|
// its ruling (confirmed or fixed). Same atom as the list route it reads.
|
|
74
96
|
api.contribute('governor.docket', {
|
|
@@ -318,3 +340,4 @@ module.exports = function buildSecurityReportsRouter() {
|
|
|
318
340
|
|
|
319
341
|
module.exports.redteamDocketMatter = redteamDocketMatter;
|
|
320
342
|
module.exports.rulingHistory = rulingHistory;
|
|
343
|
+
module.exports.confirmedDeskRow = confirmedDeskRow;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.35",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.35",
|
|
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.21.
|
|
3
|
+
"version": "1.21.35",
|
|
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
|
@@ -8958,5 +8958,17 @@
|
|
|
8958
8958
|
"id": "1004494",
|
|
8959
8959
|
"text": "The look for each craft mode is decided. Artistry, Ideating and Governing each get their own colour (moonlight, star gold, city blue) on the mode switch, the tab you are on and the top bar, the mode's name beside the page titl"
|
|
8960
8960
|
}
|
|
8961
|
+
],
|
|
8962
|
+
"1.21.34": [
|
|
8963
|
+
{
|
|
8964
|
+
"id": "1004558",
|
|
8965
|
+
"text": "Inside every building in the governor city, the floor you pick is now a desk you can act from, built from the new floor desks design. It shows the floor's name and how many things wait on you, up to three short numbers, and on"
|
|
8966
|
+
}
|
|
8967
|
+
],
|
|
8968
|
+
"1.21.35": [
|
|
8969
|
+
{
|
|
8970
|
+
"id": "1004559",
|
|
8971
|
+
"text": "Governing now happens inside the buildings. On a floor's desk you can set a builder's tier, deactivate them, invite someone by GitHub name, give or take back a custom rank; confirm, dispute or mark fixed a security report and"
|
|
8972
|
+
}
|
|
8961
8973
|
]
|
|
8962
8974
|
}
|
|
@@ -24,6 +24,7 @@ const auth = require('../auth');
|
|
|
24
24
|
const { branding } = require('../../branding');
|
|
25
25
|
const { resolveEnv } = require('../../instance-config');
|
|
26
26
|
const { asyncHandler } = require('./_helpers');
|
|
27
|
+
const seams = require('../../module-seams');
|
|
27
28
|
// task 1003208: structured logging — was console.*. A core file logs through
|
|
28
29
|
// the kernel logger directly; only MODULES go via api.logger (the doorway).
|
|
29
30
|
const log = require('../logger').logger.child({ src: 'backup' });
|
|
@@ -76,6 +77,20 @@ function isStaleDump(stamp, nowMs = Date.now(), staleAfterHours = STALE_AFTER_HO
|
|
|
76
77
|
return age > staleAfterHours;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// The newest dump's age for the Treasury desk (task 1004559), read WITHOUT
|
|
81
|
+
// blocking: the docket is read on every street visit, so this is one async
|
|
82
|
+
// directory listing and the age from the filename (no stat), never latestDump()'s
|
|
83
|
+
// synchronous reads. Same filename rule and staleness rule as latestDump().
|
|
84
|
+
async function newestDumpAge(nowMs = Date.now()) {
|
|
85
|
+
let files;
|
|
86
|
+
try { files = await fs.promises.readdir(BACKUP_DIR); } catch { return null; }
|
|
87
|
+
const dumps = files.filter((f) => f.startsWith(`${DB_NAME}-`) && f.endsWith('.sql.gz')).sort();
|
|
88
|
+
if (!dumps.length) return null;
|
|
89
|
+
const ts = dumps[dumps.length - 1].replace(`${DB_NAME}-`, '').replace('.sql.gz', '');
|
|
90
|
+
const age = dumpAgeHours(ts, nowMs);
|
|
91
|
+
return { age_hours: age === null ? null : Math.round(age * 10) / 10, stale: isStaleDump(ts, nowMs) };
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function latestDump(nowMs = Date.now()) {
|
|
80
95
|
let files;
|
|
81
96
|
try {
|
|
@@ -107,7 +122,16 @@ function latestDump(nowMs = Date.now()) {
|
|
|
107
122
|
};
|
|
108
123
|
}
|
|
109
124
|
|
|
125
|
+
// The Treasury desk's backup row (task 1004559), under the backup routes' own
|
|
126
|
+
// atom: one tap runs POST /backup/trigger. Pure; exported for tests.
|
|
127
|
+
function backupDeskRows(dump, running) {
|
|
128
|
+
const age = dump && Number.isFinite(Number(dump.age_hours)) ? Number(dump.age_hours) : null;
|
|
129
|
+
const fact = running ? 'backing up now' : age == null ? 'no backup yet' : age < 1 ? 'last one under an hour' : age < 48 ? `last ${Math.round(age)} hours ago` : `last ${Math.round(age / 24)} days ago`;
|
|
130
|
+
return [{ id: 'now', name: 'Back up now', fact, facts: dump && dump.stale ? ['the last one is stale'] : [], tool: true, ref: { type: 'backup', id: 'now' }, acts: running ? [] : [{ key: 'backup' }] }];
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
module.exports = function buildBackupRouter() {
|
|
134
|
+
seams.contribute('governor.docket', { desk: 'treasury.backup', gate: { permission: 'backup.manage' }, read: async () => backupDeskRows(await newestDumpAge(), triggerRunning) });
|
|
111
135
|
const router = express.Router();
|
|
112
136
|
|
|
113
137
|
// rank: metic+archon — any working builder may check or trigger a backup. Gate:
|
|
@@ -168,3 +192,5 @@ module.exports.parseDumpTimestamp = parseDumpTimestamp;
|
|
|
168
192
|
module.exports.dumpAgeHours = dumpAgeHours;
|
|
169
193
|
module.exports.isStaleDump = isStaleDump;
|
|
170
194
|
module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
|
|
195
|
+
|
|
196
|
+
module.exports.backupDeskRows = backupDeskRows;
|
|
@@ -168,7 +168,45 @@ function fireDiscordBroadcast(text) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// The People desk's builder rows (task 1004559): one row per active builder the
|
|
172
|
+
// viewer may act on, with the acts the viewer's own atoms allow: set tier
|
|
173
|
+
// (builder.rank.set, PATCH /builders/:id/rank) and deactivate (builder.status.set,
|
|
174
|
+
// PATCH /builders/:id/status). Never the viewer's own row: both routes refuse it.
|
|
175
|
+
// Pure; exported for tests.
|
|
176
|
+
const STOP_REASONS = Object.freeze([
|
|
177
|
+
{ key: 'left', label: 'left the project' },
|
|
178
|
+
{ key: 'quiet', label: 'gone quiet' },
|
|
179
|
+
{ key: 'asked', label: 'asked to stop' },
|
|
180
|
+
]);
|
|
181
|
+
function builderDeskRows(builders, { viewerId, mayRank, mayStop, ranks }) {
|
|
182
|
+
if (!mayRank && !mayStop) return [];
|
|
183
|
+
return (builders || []).filter((b) => b && String(b.id) !== String(viewerId)).map((b) => {
|
|
184
|
+
const acts = [];
|
|
185
|
+
// one rung up at most (the route refuses a multi-rung promotion), any rung down
|
|
186
|
+
const at = (ranks || []).indexOf(b.rank);
|
|
187
|
+
// a rank off the live ladder offers no tier chip: there is no rung to move by one
|
|
188
|
+
if (mayRank && at >= 0) acts.push({ key: 'tier', options: (ranks || []).filter((r, i) => r !== b.rank && i <= at + 1).map((r) => ({ key: r, label: r })) });
|
|
189
|
+
if (mayStop) acts.push({ key: 'deactivate', options: STOP_REASONS });
|
|
190
|
+
return { id: b.id, name: b.display_name || b.github_login || `builder ${b.id}`, fact: b.rank || 'builder', facts: b.github_login ? [`@${b.github_login}`] : [], ref: { type: 'builder', id: b.id }, acts };
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Contributed from the router factory: any builder passes the gate, and the read
|
|
194
|
+
// scopes itself to the viewer's atoms (an empty desk for a viewer holding neither).
|
|
195
|
+
function contributeBuilderDesk() {
|
|
196
|
+
seams.contribute('governor.docket', {
|
|
197
|
+
desk: 'people.builders',
|
|
198
|
+
gate: { anyBuilder: true },
|
|
199
|
+
read: async (ctx) => {
|
|
200
|
+
const mayRank = ctx.has('builder.rank.set');
|
|
201
|
+
const mayStop = ctx.has('builder.status.set');
|
|
202
|
+
if (!mayRank && !mayStop) return [];
|
|
203
|
+
return builderDeskRows(await db.listActiveBuilders(), { viewerId: ctx.builderId, mayRank, mayStop, ranks: db.LIVE_RANK_LADDER });
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
module.exports = function buildBuildersRouter() {
|
|
209
|
+
contributeBuilderDesk();
|
|
172
210
|
const router = express.Router();
|
|
173
211
|
|
|
174
212
|
// rank: archon — sole chokepoint that mutates builders.rank; ADR 0016 §enforcement.
|
|
@@ -707,3 +745,5 @@ module.exports = function buildBuildersRouter() {
|
|
|
707
745
|
|
|
708
746
|
return router;
|
|
709
747
|
};
|
|
748
|
+
|
|
749
|
+
module.exports.builderDeskRows = builderDeskRows;
|
|
@@ -180,8 +180,24 @@ function contributeModuleRechecksToDocket() {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// The Rules desk's module rows (task 1004559), under the toggle's own atom
|
|
184
|
+
// (POST /modules/:key/enable, module.enable): each module on disk, turned on in
|
|
185
|
+
// one tap, or off with a one-tap 'yes' so a stray tap cannot pull one. Pure;
|
|
186
|
+
// exported for tests.
|
|
187
|
+
function moduleDeskRows(mods) {
|
|
188
|
+
return (mods || []).filter((m) => m && m.key).map((m) => ({
|
|
189
|
+
id: m.key, name: m.title || m.key, fact: m.enabled ? 'on' : 'off', facts: m.description ? [m.description] : [], tool: true,
|
|
190
|
+
ref: { type: 'module', id: m.key },
|
|
191
|
+
acts: [m.enabled ? { key: 'module_off', options: [{ key: 'yes', label: 'yes, turn off' }] } : { key: 'module_on' }],
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
function contributeModuleDesk() {
|
|
195
|
+
seams.contribute('governor.docket', { desk: 'rules.modules', gate: { permission: 'module.enable' }, read: async () => moduleDeskRows(listModules()) });
|
|
196
|
+
}
|
|
197
|
+
|
|
183
198
|
module.exports = function buildModulesRouter() {
|
|
184
199
|
contributeModuleRechecksToDocket();
|
|
200
|
+
contributeModuleDesk();
|
|
185
201
|
const router = express.Router();
|
|
186
202
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
187
203
|
// router starts empty.
|
|
@@ -702,3 +718,5 @@ module.exports = function buildModulesRouter() {
|
|
|
702
718
|
// own disk scan, no DB), so it's safe to require directly outside an Express
|
|
703
719
|
// request cycle, the same way tests/atlas.mjs requires gen-atlas.js's builders.
|
|
704
720
|
module.exports.listModules = listModules;
|
|
721
|
+
|
|
722
|
+
module.exports.moduleDeskRows = moduleDeskRows;
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.35'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -285,7 +285,7 @@ test('the response shape: counts, line, street-ordered buildings, sorted matters
|
|
|
285
285
|
c.readings.set('flow', { reading: 'flow', gate: { anyBuilder: true }, read: async () => ({ value: 3, figure: '3 stuck', line: 'longest wait 4 days, waiting for review', hot: true }) });
|
|
286
286
|
const d = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast', 'security.report.adjudicate'], contributions: c, now: NOW });
|
|
287
287
|
|
|
288
|
-
assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
|
|
288
|
+
assert.deepEqual(Object.keys(d).sort(), ['buildings', 'counts', 'desks', 'floors', 'generated_at', 'history', 'history_sources', 'line', 'matters', 'readings', 'sources', 'viewer', 'vocabulary'].sort());
|
|
289
289
|
assert.deepEqual(d.viewer, { id: '7' });
|
|
290
290
|
// board 11 (needs) + board 12 (settled, closed this week) + blocker + report; board 13 is too old
|
|
291
291
|
assert.deepEqual(d.counts, { needs: 2, moving: 1, settled: 1 });
|
|
@@ -734,3 +734,30 @@ test('GET /government/docket: 403 without board.vote.cast, the documented shape
|
|
|
734
734
|
assert.equal(body.sources.find((s) => s.source === 'access_requests').status, 'not_yours');
|
|
735
735
|
assert.ok(!JSON.stringify(body).includes('Admit Kai'), 'a hidden source leaks nothing');
|
|
736
736
|
});
|
|
737
|
+
|
|
738
|
+
// ── the floor desks' acts and rows (task 1004559) ────────────────────────────
|
|
739
|
+
|
|
740
|
+
test('acts: a matter carries only well-formed acts, at most two, with at most five one-tap options', () => {
|
|
741
|
+
const m = docket.normalizeMatter({ id: 1, title: 't', acts: [{ key: 'confirm', options: [1, 2, 3, 4, 5, 6].map((n) => ({ key: n, label: `option ${n}` })) }, { key: 'Bad Key' }, { key: 'fix' }, { key: 'third' }] }, { source: 's', building: 'review' });
|
|
742
|
+
assert.deepEqual(m.acts.map((a) => a.key), ['confirm', 'fix']);
|
|
743
|
+
assert.equal(m.acts[0].options.length, 5);
|
|
744
|
+
assert.equal('acts' in docket.normalizeMatter({ id: 1, title: 't' }, { source: 's', building: 'review' }), false, 'no acts, no field');
|
|
745
|
+
assert.deepEqual(docket.normalizeActs([{ key: 'allow', pick: 'open_goals', except: [1, 2] }, { key: 'pause', pick: 'everything' }]), [{ key: 'allow', pick: 'open_goals', except: ['1', '2'] }, { key: 'pause' }]);
|
|
746
|
+
assert.equal(docket.normalizeDeskRow({ id: 1, name: 'n', acts: [] }, { desk: 'people.builders', building: 'people' }), null, 'a desk row with nothing to do is dropped');
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('desks: gated by the atom their write route checks; a gated-out desk is not_yours with no rows; a broken one is unavailable', async () => {
|
|
750
|
+
const c = none();
|
|
751
|
+
c.desks = new Map([
|
|
752
|
+
['people.builders', { desk: 'people.builders', gate: { permission: 'builder.rank.set' }, read: async () => [{ id: 7, name: 'Ana', fact: 'thetes', acts: [{ key: 'tier', options: [{ key: 'metic', label: 'metic' }] }] }, { id: 8, name: 'no acts', acts: [] }] }],
|
|
753
|
+
['automation.goals', { desk: 'automation.goals', gate: { anyBuilder: true }, read: async () => { throw new Error('boom'); } }],
|
|
754
|
+
]);
|
|
755
|
+
const without = await docket.buildDocket({ viewer: VIEWER, permissions: [], contributions: c, now: NOW });
|
|
756
|
+
const by = (d, k) => d.desks.find((x) => x.key === k);
|
|
757
|
+
assert.deepEqual(by(without, 'people.builders'), { key: 'people.builders', building: 'people', status: 'not_yours', rows: [] });
|
|
758
|
+
assert.equal(by(without, 'automation.goals').status, 'unavailable');
|
|
759
|
+
assert.equal(by(without, 'automation.runners').status, 'off');
|
|
760
|
+
const withAtom = await docket.buildDocket({ viewer: VIEWER, permissions: ['builder.rank.set'], contributions: c, now: NOW });
|
|
761
|
+
assert.deepEqual(by(withAtom, 'people.builders').rows.map((r) => [r.id, r.name, r.acts[0].key]), [['people.builders:7', 'Ana', 'tier']]);
|
|
762
|
+
assert.equal(withAtom.matters.length, 0, 'desk rows never become matters (no pins, no counts)');
|
|
763
|
+
});
|
|
@@ -177,9 +177,18 @@ test('every act maps to the route that does it, with the body that route accepts
|
|
|
177
177
|
assert.deepEqual(C.request(gate, 'release'), { method: 'POST', url: '/api/bongos/gate-approvals/812/approve', body: {} });
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
// Task 1004559 moved the core update onto the Release desk, check first: the ONLY
|
|
181
|
+
// way a card reaches the pin is the core_pin row, whose 'take' the server hands
|
|
182
|
+
// only after a passed dry run (tests/hall_street_desk.mjs pins that), and which
|
|
183
|
+
// needs the version chip as its confirmation. Nothing else may name the route.
|
|
184
|
+
test('only the core_pin row reaches the core pin, and never without its version', () => {
|
|
181
185
|
const src = read('street-card.js').replace(/^\s*\/\/.*$/gm, '');
|
|
182
|
-
|
|
186
|
+
const block = src.split('ACT_ROUTES.core_pin = {')[1].split('};')[0];
|
|
187
|
+
assert.equal(src.replace(block, '').match(/core-upgrade|provisioning|\/core-update/g), null, 'no other act names a provisioning route');
|
|
188
|
+
const cv = C.actsCard({ key: 'release.core:41', kind: 'core_pin', id: '41' });
|
|
189
|
+
assert.equal(C.request(cv, 'take', {}), null);
|
|
190
|
+
assert.equal(C.request(cv, 'check', {}), null);
|
|
191
|
+
assert.deepEqual(JSON.parse(JSON.stringify(C.request(cv, 'check', { option: '1.21.20' }))).body, { to: '1.21.20', dry_run: true });
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
test('an objection names its reason and its part before anything is sent; the parts are the server\'s own keys', () => {
|
|
@@ -258,7 +267,7 @@ async function bootPage({ feeds, posts = {} }) {
|
|
|
258
267
|
};
|
|
259
268
|
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite });
|
|
260
269
|
ctx.globalThis = ctx;
|
|
261
|
-
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
270
|
+
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
262
271
|
await window.GovStreetPage.ready;
|
|
263
272
|
return { window, byId, calls, focused };
|
|
264
273
|
}
|
package/tests/hall_street.mjs
CHANGED
|
@@ -376,7 +376,7 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
376
376
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
377
377
|
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
378
378
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
379
|
-
assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
|
|
379
|
+
assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526), the floor desk (task 1004558) and the drafted amendment (task 1004530)');
|
|
380
380
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
381
381
|
});
|
|
382
382
|
|
|
@@ -253,7 +253,7 @@ async function bootPage({ hash = '#building=review', cons = constitution(), answ
|
|
|
253
253
|
};
|
|
254
254
|
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map, setTimeout, clearTimeout });
|
|
255
255
|
ctx.globalThis = ctx;
|
|
256
|
-
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-amend.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
256
|
+
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street-amend.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
257
257
|
await window.GovStreetPage.ready;
|
|
258
258
|
await tick();
|
|
259
259
|
return { window, byId, focused, gets, posts, history };
|