@bongos/core 1.21.91 → 1.21.93
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 +72 -62
- package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +1 -0
- package/docs/copy-inventory.md +357 -367
- package/docs/copy-registry.json +559 -641
- package/docs/module-api-changelog.md +9 -0
- package/docs/page-inventory.json +15 -18
- package/docs/page-readings.json +1235 -918
- package/modules/autonomy/routes/autonomy.js +14 -5
- package/modules/government/decision-board.js +13 -3
- package/modules/government/decision-rules.js +61 -13
- package/modules/lifecycle/routes/done-when.js +2 -2
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/modules/lifecycle/routes/goals.js +4 -4
- package/modules/lifecycle/routes/project-settings.js +2 -2
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/modules/lifecycle/routes/versions.js +1 -1
- package/modules/npm-release/routes/release.js +1 -0
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/provisioning/routes/core-upgrade.js +11 -0
- package/modules/public-landing/public/projects-hall-live.states.json +6 -35
- package/modules/public-landing/public/projects.html +1019 -983
- package/modules/public-landing/public/projects.probes.json +15 -18
- package/modules/public-landing/public/projects.states.json +22 -25
- package/modules/render-deploy/routes/act.js +1 -1
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness.js +7 -0
- package/src/bongos/act-alone.js +84 -0
- package/src/bongos/routes/builders.js +6 -1
- package/src/module-api.js +7 -1
- package/tests/autonomy_fence_priority.mjs +3 -1
- package/tests/government_act_alone_buildings.mjs +430 -0
- package/tests/module_api.mjs +1 -0
- package/tests/npm_release_release.mjs +2 -1
- package/tests/onboard.mjs +8 -8
- package/tests/project_invite_ui.mjs +6 -5
- package/tests/projects_hub.mjs +77 -98
- package/tests/projects_hub_app_status.mjs +3 -3
- package/tests/projects_hub_app_step.mjs +8 -8
- package/tests/projects_hub_cloud_hop.mjs +29 -53
- package/tests/projects_hub_look.mjs +29 -39
- package/tests/projects_hub_module_picker.mjs +32 -39
- package/tests/projects_hub_pre_uat.mjs +11 -14
- package/tests/ship_mode.mjs +2 -1
- package/tests/wizard_demo.mjs +30 -25
- package/tests/wizard_draft_resume.mjs +24 -32
- package/tests/wizard_front_door.mjs +91 -243
- package/tests/wizard_intent_resume.mjs +28 -48
- package/tests/wizard_one_page.mjs +201 -0
- package/tests/wizard_physics.mjs +46 -41
- package/tests/wizard_physics_more.mjs +71 -106
- package/tests/wizard_ship_mode.mjs +1 -1
- package/tests/wizard_six_screens.mjs +0 -129
|
@@ -155,6 +155,15 @@ module.exports = function buildAutonomyRouter() {
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// WHO DECIDES AT AUTOMATION (task 1004657; ADR 0363 D5). While the project's
|
|
159
|
+
// decision-rules row hands Automation to a sitting, these writes refuse one
|
|
160
|
+
// builder acting alone (409 decided_by_sitting) and point at the matter route;
|
|
161
|
+
// a passed matter acts through matter-acts.js, never through these routes.
|
|
162
|
+
// The BRAKE is exempt: switching unattended building off, or pausing one
|
|
163
|
+
// runner, never waits for a meeting — stopping can only narrow what runs.
|
|
164
|
+
const isTheBrake = (req) => !!req.body && req.body.enabled === false;
|
|
165
|
+
const isAPause = (req) => !!req.body && req.body.paused === true;
|
|
166
|
+
|
|
158
167
|
// ── the Autobongos fence (task 1003908, ADR 0307 §6) ───────────────────────
|
|
159
168
|
//
|
|
160
169
|
// These five routes ARE the fence. Everything the unattended runner is allowed
|
|
@@ -195,7 +204,7 @@ module.exports = function buildAutonomyRouter() {
|
|
|
195
204
|
|
|
196
205
|
// The kill switch. Archon, system:true — see the catalog entry for why it is the
|
|
197
206
|
// one permission that cannot be delegated to a custom rank.
|
|
198
|
-
router.post('/autonomy/fence', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), async (req, res) => {
|
|
207
|
+
router.post('/autonomy/fence', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), auth.requireActAlone('automation', { unless: isTheBrake }), async (req, res) => {
|
|
199
208
|
if (validateOrRespond(req, res, {
|
|
200
209
|
enabled: { required: true, type: 'boolean' },
|
|
201
210
|
reason: { type: 'string', maxLength: 500 },
|
|
@@ -217,7 +226,7 @@ module.exports = function buildAutonomyRouter() {
|
|
|
217
226
|
});
|
|
218
227
|
|
|
219
228
|
// Add a goal to the allowlist.
|
|
220
|
-
router.post('/autonomy/fence/goals', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), async (req, res) => {
|
|
229
|
+
router.post('/autonomy/fence/goals', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), auth.requireActAlone('automation'), async (req, res) => {
|
|
221
230
|
if (validateOrRespond(req, res, {
|
|
222
231
|
goal_id: { required: true }, // number or stringified number; coerced below
|
|
223
232
|
note: { type: 'string', maxLength: 500 },
|
|
@@ -237,7 +246,7 @@ module.exports = function buildAutonomyRouter() {
|
|
|
237
246
|
|
|
238
247
|
// Revoke one. `removed` distinguishes "just revoked" from "already gone" — an
|
|
239
248
|
// owner clicking Remove deserves to know which happened.
|
|
240
|
-
router.delete('/autonomy/fence/goals/:goalId', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), async (req, res) => {
|
|
249
|
+
router.delete('/autonomy/fence/goals/:goalId', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), auth.requireActAlone('automation'), async (req, res) => {
|
|
241
250
|
const goalId = Number(req.params.goalId);
|
|
242
251
|
if (!Number.isSafeInteger(goalId) || goalId <= 0) return res.fail('bad_goal_id', 400);
|
|
243
252
|
try {
|
|
@@ -255,7 +264,7 @@ module.exports = function buildAutonomyRouter() {
|
|
|
255
264
|
// first is part of choosing what it may do. The goal must already be on the
|
|
256
265
|
// allowlist (409 otherwise), so a priority can order the list and never widen
|
|
257
266
|
// it. `goal_id: null` clears it.
|
|
258
|
-
router.post('/autonomy/fence/priority', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), async (req, res) => {
|
|
267
|
+
router.post('/autonomy/fence/priority', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), auth.requireActAlone('automation'), async (req, res) => {
|
|
259
268
|
// Declared with an open rule: the value is a number OR null, which the
|
|
260
269
|
// validator has no type for, so the shape is checked just below.
|
|
261
270
|
if (validateOrRespond(req, res, { goal_id: {} })) return;
|
|
@@ -302,7 +311,7 @@ module.exports = function buildAutonomyRouter() {
|
|
|
302
311
|
// switch. Same atom as the kill switch: stopping a machine is the same act
|
|
303
312
|
// whether it is everyone's or one person's, and the paused builder must not be
|
|
304
313
|
// able to lift it (autonomy.run does not reach this route).
|
|
305
|
-
router.post('/autonomy/builders/:builderId/pause', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), async (req, res) => {
|
|
314
|
+
router.post('/autonomy/builders/:builderId/pause', auth.requireBuilder, auth.requirePermission('autonomy.fence.manage'), auth.requireActAlone('automation', { unless: isAPause }), async (req, res) => {
|
|
306
315
|
if (validateOrRespond(req, res, {
|
|
307
316
|
paused: { required: true, type: 'boolean' },
|
|
308
317
|
reason: { type: 'string', maxLength: 500 },
|
|
@@ -286,13 +286,23 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
// May this builder act ALONE at this building — the check a building's own
|
|
289
|
-
// direct route runs before acting (People's rank assignments
|
|
289
|
+
// direct route runs before acting (People's rank assignments here; every other
|
|
290
|
+
// building's routes through the doorway's requireActAlone, task 1004657 — the
|
|
291
|
+
// list is decision-rules AREAS[].acts). Resolved
|
|
290
292
|
// per request against the live row and the builder's live ranks, uncached.
|
|
291
293
|
// Returns { ok: true } or { ok: false, reason: 'decided_by_sitting', … }.
|
|
294
|
+
//
|
|
295
|
+
// HOT PATH since task 1004657: ~30 write routes across the modules mount this
|
|
296
|
+
// through requireActAlone, so it does ONE query and no file I/O. The board block
|
|
297
|
+
// (loadGovernmentConfig — two synchronous file reads, boot-only by config.js's own
|
|
298
|
+
// rule) only ever shapes the CHARTER row's rule and words, and no direct route
|
|
299
|
+
// asks about the charter; every other building's row reads the same without it.
|
|
292
300
|
async function checkActAlone(area, builderId) {
|
|
293
|
-
|
|
301
|
+
if (!decisionRules.AREA_KEYS.includes(area)) return { ok: false, reason: 'unknown_area' };
|
|
294
302
|
const rows = await db.listDecisionRules();
|
|
295
|
-
const
|
|
303
|
+
const board = area === 'charter' ? loadGovernmentConfig().board : {};
|
|
304
|
+
const stored = (rows || []).find((r) => r.area === area) || decisionRules.defaultRow(area);
|
|
305
|
+
const resolved = decisionRules.resolveRow(stored, { board });
|
|
296
306
|
if (!resolved) return { ok: false, reason: 'unknown_area' };
|
|
297
307
|
if (resolved.as_today) return { ok: true, decided: resolved };
|
|
298
308
|
const seated = resolved.sitting
|
|
@@ -44,30 +44,77 @@ const { RANK_ORDER } = require('./catalog');
|
|
|
44
44
|
// The nine buildings, in street order (held equal to docket-vocab BUILDINGS by
|
|
45
45
|
// test). `gate` is the fixed permission that decides the building's matters
|
|
46
46
|
// today — the atom its own pages require, named so a reader can see what a
|
|
47
|
-
// sitting replaces. `acts`
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
47
|
+
// sitting replaces. `acts` is THE RECORD of every direct route that defers to
|
|
48
|
+
// the building's row: while the row names a sitting, each refuses one builder
|
|
49
|
+
// acting alone (409 decided_by_sitting) and points at the matter route. People's
|
|
50
|
+
// rank assignments ask in this module (task 1004529); every other route mounts
|
|
51
|
+
// the doorway's requireActAlone(area) after its permission gate (task 1004657).
|
|
52
|
+
// tests/government_act_alone_buildings.mjs reads this list against the source
|
|
53
|
+
// both ways, so a route cannot defer without being listed, or be listed without
|
|
54
|
+
// deferring. Which routes are a building's: those gated by its `gate` atom,
|
|
55
|
+
// plus the routes its passed matters act through (decision-board MATTER_ACTIONS)
|
|
56
|
+
// and its docket desks reach. A building with no direct route says so in
|
|
57
|
+
// `no_direct_route` — the charter, whose only act is an amendment.
|
|
58
|
+
// NOT a lone act, so never refused: a BRAKE (switching unattended building off,
|
|
59
|
+
// pausing one runner — stopping never waits for a meeting), a dry run, and a
|
|
60
|
+
// tenant project's own owner moving their own pin (that project's decision).
|
|
51
61
|
const AREAS = Object.freeze([
|
|
52
62
|
Object.freeze({ key: 'charter', name: 'Charter', gate: null,
|
|
53
|
-
today: 'the board, under the constitution in force', acts: Object.freeze([])
|
|
63
|
+
today: 'the board, under the constitution in force', acts: Object.freeze([]),
|
|
64
|
+
no_direct_route: 'the charter is changed only by amending the constitution — POST /government/board/items or /government/board/charter' }),
|
|
54
65
|
Object.freeze({ key: 'rules', name: 'Rules', gate: 'project.settings.manage',
|
|
55
|
-
today: 'whoever may change the project settings', acts: Object.freeze([
|
|
66
|
+
today: 'whoever may change the project settings', acts: Object.freeze([
|
|
67
|
+
'PATCH /project-settings/rot-days',
|
|
68
|
+
'PATCH /project-settings/ship-mode',
|
|
69
|
+
'PATCH /work-categories/:id',
|
|
70
|
+
]) }),
|
|
56
71
|
Object.freeze({ key: 'disputes', name: 'Disputes', gate: 'security.report.adjudicate',
|
|
57
|
-
today: 'whoever may judge reports', acts: Object.freeze([
|
|
72
|
+
today: 'whoever may judge reports', acts: Object.freeze([
|
|
73
|
+
'POST /security/reports/:id/confirm',
|
|
74
|
+
'POST /security/reports/:id/dispute',
|
|
75
|
+
'POST /security/reports/:id/fix',
|
|
76
|
+
]) }),
|
|
58
77
|
Object.freeze({ key: 'people', name: 'People', gate: 'government.manage',
|
|
59
78
|
today: 'whoever may manage ranks',
|
|
60
|
-
acts: Object.freeze([
|
|
79
|
+
acts: Object.freeze([
|
|
80
|
+
'POST /government/assignments',
|
|
81
|
+
'DELETE /government/assignments/:builderId/:rankKey',
|
|
82
|
+
'PATCH /access-requests/:id',
|
|
83
|
+
'POST /access-requests/invite',
|
|
84
|
+
]) }),
|
|
61
85
|
Object.freeze({ key: 'planning', name: 'Planning', gate: 'goal.create',
|
|
62
|
-
today: 'whoever may create goals', acts: Object.freeze([
|
|
86
|
+
today: 'whoever may create goals', acts: Object.freeze([
|
|
87
|
+
'POST /goals',
|
|
88
|
+
'POST /goals/reorder',
|
|
89
|
+
'POST /work-categories',
|
|
90
|
+
'PUT /versions/focus',
|
|
91
|
+
]) }),
|
|
63
92
|
Object.freeze({ key: 'automation', name: 'Automation', gate: 'autonomy.fence.manage',
|
|
64
|
-
today: 'whoever may switch unattended building on and off', acts: Object.freeze([
|
|
93
|
+
today: 'whoever may switch unattended building on and off', acts: Object.freeze([
|
|
94
|
+
'POST /autonomy/fence',
|
|
95
|
+
'POST /autonomy/fence/goals',
|
|
96
|
+
'DELETE /autonomy/fence/goals/:goalId',
|
|
97
|
+
'POST /autonomy/fence/priority',
|
|
98
|
+
'POST /autonomy/builders/:builderId/pause',
|
|
99
|
+
]) }),
|
|
65
100
|
Object.freeze({ key: 'review', name: 'Review', gate: 'criterion.satisfy',
|
|
66
|
-
today: 'whoever may mark work as meeting its checks', acts: Object.freeze([
|
|
101
|
+
today: 'whoever may mark work as meeting its checks', acts: Object.freeze([
|
|
102
|
+
'POST /done-when/:criterionId/satisfy',
|
|
103
|
+
'POST /done-when/:criterionId/unsatisfy',
|
|
104
|
+
]) }),
|
|
67
105
|
Object.freeze({ key: 'release', name: 'Release', gate: 'core.pin.move',
|
|
68
|
-
today: 'whoever may put a release live', acts: Object.freeze([
|
|
106
|
+
today: 'whoever may put a release live', acts: Object.freeze([
|
|
107
|
+
'POST /gate-approvals/:pr/approve',
|
|
108
|
+
'POST /override-requests/:id/decide',
|
|
109
|
+
'POST /provisioning/instances/:id/core-upgrade',
|
|
110
|
+
'POST /provisioning/instances/:id/update-channel',
|
|
111
|
+
'POST /render-deploy/act',
|
|
112
|
+
'POST /npm-release/release',
|
|
113
|
+
]) }),
|
|
69
114
|
Object.freeze({ key: 'treasury', name: 'Treasury', gate: 'builder.budget.set',
|
|
70
|
-
today: 'whoever may set spending limits', acts: Object.freeze([
|
|
115
|
+
today: 'whoever may set spending limits', acts: Object.freeze([
|
|
116
|
+
'PATCH /builders/:id/budget',
|
|
117
|
+
]) }),
|
|
71
118
|
]);
|
|
72
119
|
const AREA_KEYS = Object.freeze(AREAS.map((a) => a.key));
|
|
73
120
|
const areaByKey = (key) => AREAS.find((a) => a.key === key) || null;
|
|
@@ -245,6 +292,7 @@ function resolveRow(stored, { board = {} } = {}) {
|
|
|
245
292
|
gate: def.gate,
|
|
246
293
|
today: def.today,
|
|
247
294
|
acts: def.acts,
|
|
295
|
+
no_direct_route: def.no_direct_route || null,
|
|
248
296
|
set_by_item_id: row.set_by_item_id == null ? null : String(row.set_by_item_id),
|
|
249
297
|
updated_at: row.updated_at || null,
|
|
250
298
|
});
|
|
@@ -248,7 +248,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
248
248
|
// everywhere — never as a UAT. The escape hatch stays (an all-abandoned
|
|
249
249
|
// criterion that was met another way still needs one); it is just no longer
|
|
250
250
|
// silent.
|
|
251
|
-
router.post('/done-when/:criterionId/satisfy', auth.requireBuilder, auth.requirePermission('criterion.satisfy'), asyncHandler('POST /done-when/:criterionId/satisfy', async (req, res) => {
|
|
251
|
+
router.post('/done-when/:criterionId/satisfy', auth.requireBuilder, auth.requirePermission('criterion.satisfy'), auth.requireActAlone('review'), asyncHandler('POST /done-when/:criterionId/satisfy', async (req, res) => {
|
|
252
252
|
if (validateOrRespond(req, res, {
|
|
253
253
|
satisfied_by_task_id: { type: 'integer', min: 1 },
|
|
254
254
|
override_reason: { type: 'string', maxLength: 2000 },
|
|
@@ -476,7 +476,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
476
476
|
// rank: archon — un-flip a criterion; same gate as satisfy.
|
|
477
477
|
// An optional `reason` (the Review desk's one-tap chip, task 1004561) is
|
|
478
478
|
// logged beside who undid it; the flip itself is unchanged.
|
|
479
|
-
router.post('/done-when/:criterionId/unsatisfy', auth.requireBuilder, auth.requirePermission('criterion.unsatisfy'), asyncHandler('POST /done-when/:criterionId/unsatisfy', async (req, res) => {
|
|
479
|
+
router.post('/done-when/:criterionId/unsatisfy', auth.requireBuilder, auth.requirePermission('criterion.unsatisfy'), auth.requireActAlone('review'), asyncHandler('POST /done-when/:criterionId/unsatisfy', async (req, res) => {
|
|
480
480
|
if (validateOrRespond(req, res, { reason: { type: 'string', maxLength: 200 } })) return;
|
|
481
481
|
const id = parseId(req, res, { param: 'criterionId', code: 'bad_criterion_id' });
|
|
482
482
|
if (id === null) return;
|
|
@@ -255,7 +255,7 @@ module.exports = function buildGateApprovalsRouter() {
|
|
|
255
255
|
// POST /gate-approvals/:pr/approve — record owner approval for a stuck gate PR and
|
|
256
256
|
// re-run the gate-review check so it clears. rank: archon (clears the hard floor —
|
|
257
257
|
// pinned in route-rank-check.EXPECTED_RANKS for downgrade protection).
|
|
258
|
-
router.post('/gate-approvals/:pr/approve', auth.requireBuilder, auth.requirePermission('gate_approval.decide'), async (req, res) => {
|
|
258
|
+
router.post('/gate-approvals/:pr/approve', auth.requireBuilder, auth.requirePermission('gate_approval.decide'), auth.requireActAlone('release'), async (req, res) => {
|
|
259
259
|
const number = parseId(req, res, { param: 'pr', code: 'bad_pr', positiveInt: true });
|
|
260
260
|
if (number == null) return;
|
|
261
261
|
try {
|
|
@@ -83,7 +83,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
83
83
|
// POST /goals — create a goal.
|
|
84
84
|
// rank: metic+archon — a Metic or Archon may create a goal; creation of a goal
|
|
85
85
|
// whose scope_modules includes a PROTECTED module is Archon-only (in-handler).
|
|
86
|
-
router.post('/goals', auth.requireBuilder, auth.requirePermission('goal.create'), async (req, res) => {
|
|
86
|
+
router.post('/goals', auth.requireBuilder, auth.requirePermission('goal.create'), auth.requireActAlone('planning'), async (req, res) => {
|
|
87
87
|
if (validateOrRespond(req, res, {
|
|
88
88
|
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
89
89
|
title: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.TITLE },
|
|
@@ -239,7 +239,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
239
239
|
// Mounted BEFORE GET /goals/:id so 'reorder' is never captured as an id.
|
|
240
240
|
// rank: archon — priority is an owner decision, never self-granted by a
|
|
241
241
|
// goal's own member/manager (unlike the goal-authority routes above).
|
|
242
|
-
router.post('/goals/reorder', auth.requireBuilder, auth.requirePermission('goal.reorder'), async (req, res) => {
|
|
242
|
+
router.post('/goals/reorder', auth.requireBuilder, auth.requirePermission('goal.reorder'), auth.requireActAlone('planning'), async (req, res) => {
|
|
243
243
|
if (validateOrRespond(req, res, {
|
|
244
244
|
version_id: { required: true, type: 'string', minLength: 1, maxLength: 64 },
|
|
245
245
|
ordered_ids: { required: true, type: 'array', maxItems: 500, itemsType: 'integer' },
|
|
@@ -425,7 +425,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
425
425
|
// prompt writes into. Same permission as authoring a goal: extending the project's
|
|
426
426
|
// vocabulary is a planning act. A duplicate slug returns the existing row (200),
|
|
427
427
|
// so two people prompted at once converge instead of one being stranded.
|
|
428
|
-
router.post('/work-categories', auth.requireBuilder, auth.requirePermission('goal.create'),
|
|
428
|
+
router.post('/work-categories', auth.requireBuilder, auth.requirePermission('goal.create'), auth.requireActAlone('planning'),
|
|
429
429
|
goalCategory.categoryCreateHandler({ validateOrRespond, LIMITS, log }));
|
|
430
430
|
// PATCH /work-categories/:id — body { label?, sort_order?, active? }: rename a
|
|
431
431
|
// category, move it in the order, or stop offering it. The set has been list+add
|
|
@@ -440,7 +440,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
440
440
|
// /goal-create prompt refer to. Deactivating never deletes — goals already carrying
|
|
441
441
|
// a retired category keep a resolvable label.
|
|
442
442
|
router.patch('/work-categories/:id', auth.requireBuilder,
|
|
443
|
-
auth.requirePermission('project.settings.manage'),
|
|
443
|
+
auth.requirePermission('project.settings.manage'), auth.requireActAlone('rules'),
|
|
444
444
|
goalCategory.categoryUpdateHandler({ validateOrRespond, LIMITS, parseId, log }));
|
|
445
445
|
|
|
446
446
|
// PATCH /goals/:id/category — body { category_id } (null clears it). Separate from
|
|
@@ -313,7 +313,7 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
313
313
|
// first and fitness.js caught it; that is why every gated route in the tree spells
|
|
314
314
|
// its atom inline. (Wrapping the middleware list across lines is fine — the sibling
|
|
315
315
|
// PATCH /work-categories/:id does exactly that and still classifies metic+archon.)
|
|
316
|
-
router.patch('/project-settings/rot-days', api.requireBuilder, api.requirePermission('project.settings.manage'), async (req, res) => {
|
|
316
|
+
router.patch('/project-settings/rot-days', api.requireBuilder, api.requirePermission('project.settings.manage'), api.requireActAlone('rules'), async (req, res) => {
|
|
317
317
|
// STRICT schema first (ADR 0118): it declares the only field this handler reads,
|
|
318
318
|
// so an unknown key is refused through the ADR 0116 envelope instead of being
|
|
319
319
|
// silently dropped. It settles the TYPE; parseDays below settles the RANGE, using
|
|
@@ -364,7 +364,7 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
364
364
|
// rank: project.settings.manage (metic+, government_010) — the same atom as the
|
|
365
365
|
// other writes on this page. Validated with the resolver's own parser, so the
|
|
366
366
|
// page, the CLI and the reader can never disagree about what a mode is.
|
|
367
|
-
router.patch('/project-settings/ship-mode', api.requireBuilder, api.requirePermission('project.settings.manage'), async (req, res) => {
|
|
367
|
+
router.patch('/project-settings/ship-mode', api.requireBuilder, api.requirePermission('project.settings.manage'), api.requireActAlone('rules'), async (req, res) => {
|
|
368
368
|
if (api.validateOrRespond(req, res, { mode: { required: true, type: 'string' } })) return;
|
|
369
369
|
const parsed = api.projectSettings.parseShipMode(req.body.mode);
|
|
370
370
|
if (!parsed.mode) {
|
|
@@ -1437,7 +1437,7 @@ module.exports = function buildTasksRouter() {
|
|
|
1437
1437
|
// caller; this route only renders its refusal (403 override_owner_only).
|
|
1438
1438
|
// Approving one confirms nothing and pays nothing — it stops the review holding
|
|
1439
1439
|
// the DEPLOY and leaves it open on the board.
|
|
1440
|
-
router.post('/override-requests/:id/decide', auth.requireBuilder, auth.requirePermission('override_request.decide'), async (req, res) => {
|
|
1440
|
+
router.post('/override-requests/:id/decide', auth.requireBuilder, auth.requirePermission('override_request.decide'), auth.requireActAlone('release'), async (req, res) => {
|
|
1441
1441
|
const id = parseId(req, res, { code: 'bad_request_id' });
|
|
1442
1442
|
if (id === null) return;
|
|
1443
1443
|
if (validateOrRespond(req, res, {
|
|
@@ -56,7 +56,7 @@ module.exports = function buildVersionsRouter() {
|
|
|
56
56
|
|
|
57
57
|
// rank: archon (atom version.focus.set, system) — body { version_id } sets the focus; { version_id: null } clears it.
|
|
58
58
|
// An unknown version id is refused rather than stored.
|
|
59
|
-
router.put('/versions/focus', auth.requireBuilder, auth.requirePermission('version.focus.set'), asyncHandler('PUT /versions/focus', async (req, res) => {
|
|
59
|
+
router.put('/versions/focus', auth.requireBuilder, auth.requirePermission('version.focus.set'), auth.requireActAlone('planning'), asyncHandler('PUT /versions/focus', async (req, res) => {
|
|
60
60
|
if (validateOrRespond(req, res, { version_id: { type: 'string', maxLength: 64 } })) return;
|
|
61
61
|
const raw = (req.body || {}).version_id;
|
|
62
62
|
if (raw === undefined || (raw !== null && !raw.trim())) return res.fail('bad_version_id', { status: 400, message: 'Pick a version to star, or send null to clear the focus.' });
|
|
@@ -36,6 +36,7 @@ module.exports = function buildNpmReleaseReleaseRouter({
|
|
|
36
36
|
'/npm-release/release',
|
|
37
37
|
api.requireBuilder,
|
|
38
38
|
api.requirePermission('core.pin.move'),
|
|
39
|
+
api.requireActAlone('release'),
|
|
39
40
|
async (req, res) => {
|
|
40
41
|
if (api.validateOrRespond(req, res, releaseSchema, { strict: true })) return undefined;
|
|
41
42
|
if (!VERSION_RE.test(req.body.version)) {
|
|
@@ -58,7 +58,7 @@ const express = require('express');
|
|
|
58
58
|
// core file a module may require, ADR 0083); approval-broadcast travels WITH this
|
|
59
59
|
// admission route (its sole consumer) and is an intra-module require.
|
|
60
60
|
const api = require('../../../src/module-api');
|
|
61
|
-
const auth = { requireBuilder: api.requireBuilder, requirePermission: api.requirePermission };
|
|
61
|
+
const auth = { requireBuilder: api.requireBuilder, requirePermission: api.requirePermission, requireActAlone: api.requireActAlone };
|
|
62
62
|
const { pool } = api;
|
|
63
63
|
const { validateOrRespond, parseId, accountExistenceReadRateLimit } = api;
|
|
64
64
|
// A recruit invite's door closes 30 days after it is sent, computed at read time
|
|
@@ -486,7 +486,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
486
486
|
// the SET value. Postgres infers text from the assignment anyway, but no real
|
|
487
487
|
// engine ever executes this statement — there is no integration test for this
|
|
488
488
|
// route and CI has no Postgres — so the type is stated rather than relied on.
|
|
489
|
-
router.patch('/access-requests/:id', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
489
|
+
router.patch('/access-requests/:id', auth.requireBuilder, auth.requirePermission('access_request.review'), auth.requireActAlone('people'), async (req, res) => {
|
|
490
490
|
const id = parseId(req, res, { code: 'bad_request_id' });
|
|
491
491
|
if (id === null) return;
|
|
492
492
|
if (validateOrRespond(req, res, {
|
|
@@ -567,7 +567,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
567
567
|
// pending request was resolved; 200 already_invited when an invite already
|
|
568
568
|
// stands; 409 already_member when they have an account.
|
|
569
569
|
// rank: archon — the same gate as resolving a request.
|
|
570
|
-
router.post('/access-requests/invite', auth.requireBuilder, auth.requirePermission('access_request.review'), async (req, res) => {
|
|
570
|
+
router.post('/access-requests/invite', auth.requireBuilder, auth.requirePermission('access_request.review'), auth.requireActAlone('people'), async (req, res) => {
|
|
571
571
|
if (validateOrRespond(req, res, {
|
|
572
572
|
github_login: { required: true, type: 'string', minLength: 1, maxLength: 60 },
|
|
573
573
|
note: { type: 'string', maxLength: 500 },
|
|
@@ -62,6 +62,15 @@ function contributeCoreDesk() {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// WHO DECIDES AT RELEASE (task 1004657; ADR 0363 D5). The pin move and the
|
|
66
|
+
// update rule are Release's own acts when the PIN door admitted the caller: while
|
|
67
|
+
// the project's decision-rules row hands Release to a sitting, one builder acting
|
|
68
|
+
// alone is refused (409 decided_by_sitting). Not a lone Release act, so never
|
|
69
|
+
// asked: a tenant project's own owner moving THEIR project (ADR 0339 — that
|
|
70
|
+
// project's decision, not this one's), and a dry run, which only reads.
|
|
71
|
+
const notThisProjectsRelease = (req) => req.coreUpgradeAuthority === 'owner';
|
|
72
|
+
const notADecision = (req) => notThisProjectsRelease(req) || (!!req.body && req.body.dry_run === true);
|
|
73
|
+
|
|
65
74
|
module.exports = function buildCoreUpgradeRouter() {
|
|
66
75
|
contributeCoreDesk();
|
|
67
76
|
const router = express.Router();
|
|
@@ -95,6 +104,7 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
95
104
|
'/provisioning/instances/:id/core-upgrade',
|
|
96
105
|
api.requireBuilder,
|
|
97
106
|
requireOwnProjectOrPin({ pinGate: api.requirePermission('core.pin.move'), provisioning, pool: api.pool, log }),
|
|
107
|
+
api.requireActAlone('release', { unless: notADecision }),
|
|
98
108
|
makeCoreUpgradeHandler({
|
|
99
109
|
provisioning,
|
|
100
110
|
pool: api.pool,
|
|
@@ -130,6 +140,7 @@ module.exports = function buildCoreUpgradeRouter() {
|
|
|
130
140
|
'/provisioning/instances/:id/update-channel',
|
|
131
141
|
api.requireBuilder,
|
|
132
142
|
requireOwnProjectOrPin({ pinGate: api.requirePermission('core.pin.move'), provisioning, pool: api.pool, log }),
|
|
143
|
+
api.requireActAlone('release', { unless: notThisProjectsRelease }),
|
|
133
144
|
makeUpdateChannelHandler({
|
|
134
145
|
provisioning,
|
|
135
146
|
pool: api.pool,
|
|
@@ -32,22 +32,6 @@
|
|
|
32
32
|
"wait",
|
|
33
33
|
600
|
|
34
34
|
],
|
|
35
|
-
[
|
|
36
|
-
"click",
|
|
37
|
-
"#termsAck"
|
|
38
|
-
],
|
|
39
|
-
[
|
|
40
|
-
"wait",
|
|
41
|
-
100
|
|
42
|
-
],
|
|
43
|
-
[
|
|
44
|
-
"click",
|
|
45
|
-
"#wizStart"
|
|
46
|
-
],
|
|
47
|
-
[
|
|
48
|
-
"wait",
|
|
49
|
-
400
|
|
50
|
-
],
|
|
51
35
|
[
|
|
52
36
|
"fill",
|
|
53
37
|
"#newName",
|
|
@@ -62,14 +46,6 @@
|
|
|
62
46
|
"#f-desc",
|
|
63
47
|
"A co-op farming game where the seasons never repeat."
|
|
64
48
|
],
|
|
65
|
-
[
|
|
66
|
-
"click",
|
|
67
|
-
"#panel-what button[type=submit]"
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
"wait",
|
|
71
|
-
400
|
|
72
|
-
],
|
|
73
49
|
[
|
|
74
50
|
"select",
|
|
75
51
|
"#physCountry",
|
|
@@ -91,13 +67,9 @@
|
|
|
91
67
|
"click",
|
|
92
68
|
"[data-phys=\"entity\"][data-val=\"intent-solo\"]"
|
|
93
69
|
],
|
|
94
|
-
[
|
|
95
|
-
"click",
|
|
96
|
-
"#physNext"
|
|
97
|
-
],
|
|
98
70
|
[
|
|
99
71
|
"wait",
|
|
100
|
-
|
|
72
|
+
300
|
|
101
73
|
],
|
|
102
74
|
[
|
|
103
75
|
"click",
|
|
@@ -135,11 +107,11 @@
|
|
|
135
107
|
],
|
|
136
108
|
[
|
|
137
109
|
"click",
|
|
138
|
-
"#
|
|
110
|
+
"#termsAck"
|
|
139
111
|
],
|
|
140
112
|
[
|
|
141
113
|
"wait",
|
|
142
|
-
|
|
114
|
+
100
|
|
143
115
|
],
|
|
144
116
|
[
|
|
145
117
|
"click",
|
|
@@ -159,12 +131,11 @@
|
|
|
159
131
|
"#panel-done",
|
|
160
132
|
"#wizInvite",
|
|
161
133
|
"#wizInviteWho",
|
|
162
|
-
"#humanSteps
|
|
163
|
-
"#stepPager",
|
|
164
|
-
"#stepNext"
|
|
134
|
+
"#humanSteps"
|
|
165
135
|
],
|
|
166
136
|
"hidden": [
|
|
167
|
-
"#hop"
|
|
137
|
+
"#hop",
|
|
138
|
+
"#wizParts"
|
|
168
139
|
],
|
|
169
140
|
"text": [
|
|
170
141
|
[
|