@bongos/core 1.21.35 → 1.21.36
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 +54 -39
- package/clients/bongos-client/index.d.ts +3 -3
- package/docs/api/openapi.json +25 -35
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +350 -304
- package/modules/autonomy/matter-acts.js +41 -0
- package/modules/autonomy/routes/autonomy.js +2 -0
- package/modules/government/board.js +4 -0
- package/modules/government/db.js +14 -0
- package/modules/government/decision-board.js +90 -11
- package/modules/government/docket-vocab.js +5 -0
- package/modules/government/docket.js +11 -0
- package/modules/government/routes/government.js +2 -1
- package/modules/hall-ui/public/street-card.js +62 -2
- package/modules/hall-ui/public/street-desk.js +101 -8
- package/modules/hall-ui/public/street-inside.js +6 -3
- package/modules/hall-ui/public/street.css +15 -0
- package/modules/hall-ui/public/street.js +73 -4
- package/modules/hall-ui/public/street.states.json +124 -0
- package/modules/ideas/governor-docket.js +91 -0
- package/modules/lifecycle/governor-docket.js +69 -0
- package/modules/lifecycle/matter-acts.js +63 -0
- package/modules/lifecycle/routes/done-when.js +26 -12
- package/modules/lifecycle/routes/gate-approvals.js +74 -59
- package/modules/lifecycle/routes/lifecycle.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/src/bongos/route-rank-check.js +2 -0
- package/src/bongos/routes/builders.js +56 -1
- package/src/module-api.js +1 -1
- package/tests/governor_matter_acts.mjs +241 -0
- package/tests/hall_street_desk.mjs +244 -4
- package/tests/hall_street_inside.mjs +3 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/autonomy/matter-acts.js — what a PASSED Automation matter carries out
|
|
4
|
+
// (task 1004561, goal 1000125). When a project's decision table puts Automation
|
|
5
|
+
// to a sitting, the sitting's passed matter acts here, after the close commits:
|
|
6
|
+
// the switch on or off, or one goal allowed. Each act goes through the SAME db
|
|
7
|
+
// function its direct route calls (POST /autonomy/fence, POST
|
|
8
|
+
// /autonomy/fence/goals), so a matter can do nothing the route could not.
|
|
9
|
+
// The government module only checks the act's shape and calls in through the
|
|
10
|
+
// 'governor.matterAct' contribution point (a module never requires a sibling).
|
|
11
|
+
|
|
12
|
+
const api = require('../../src/module-api');
|
|
13
|
+
const db = require('./db');
|
|
14
|
+
|
|
15
|
+
const log = api.logger('autonomy');
|
|
16
|
+
const POINT = 'governor.matterAct';
|
|
17
|
+
|
|
18
|
+
const why = (ctx) => `a passed Automation matter (matter ${ctx && ctx.matterId})`;
|
|
19
|
+
|
|
20
|
+
async function switchFence(enabled, ctx) {
|
|
21
|
+
const row = await db.setFenceEnabled({ enabled, reason: why(ctx), builderId: ctx.decidedBy });
|
|
22
|
+
if (!row) return { done: false, reason: 'fence_uninitialised' };
|
|
23
|
+
log.info(`[gds] autonomy fence ${enabled ? 'ENABLED' : 'DISABLED'} by ${why(ctx)}, proposed by builder ${ctx.decidedBy}`);
|
|
24
|
+
return { done: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ACTS = Object.freeze({
|
|
28
|
+
switch_on: (_action, ctx) => switchFence(true, ctx),
|
|
29
|
+
switch_off: (_action, ctx) => switchFence(false, ctx),
|
|
30
|
+
allow_goal: async (action, ctx) => {
|
|
31
|
+
await db.allowGoal({ goalId: action.goal_id, note: why(ctx), builderId: ctx.decidedBy });
|
|
32
|
+
log.info(`[gds] autonomy fence: goal ${action.goal_id} ALLOWED by ${why(ctx)}`);
|
|
33
|
+
return { done: true };
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function contributeAutomationMatterActs() {
|
|
38
|
+
for (const [type, act] of Object.entries(ACTS)) api.contribute(POINT, { area: 'automation', type, act });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { ACTS, contributeAutomationMatterActs };
|
|
@@ -108,6 +108,8 @@ const governorDocket = require('../governor-docket');
|
|
|
108
108
|
|
|
109
109
|
module.exports = function buildAutonomyRouter() {
|
|
110
110
|
governorDocket.contributeAutonomyToDocket();
|
|
111
|
+
// what a passed Automation matter carries out (task 1004561)
|
|
112
|
+
require('../matter-acts').contributeAutomationMatterActs();
|
|
111
113
|
const router = express.Router();
|
|
112
114
|
|
|
113
115
|
// The one Discord line for a runner idle on a setting (task 1004539). The port
|
|
@@ -1370,6 +1370,9 @@ async function boardPassForIdea(ideaId) {
|
|
|
1370
1370
|
});
|
|
1371
1371
|
}
|
|
1372
1372
|
|
|
1373
|
+
// boardPassForIdea for a list, in one read (task 1004560).
|
|
1374
|
+
const boardPassedIdeaIds = async (ids) => new Set(await db.listPassedSubjectIds('full_idea', ids));
|
|
1375
|
+
|
|
1373
1376
|
// ── the summons: what is waiting on THIS member's vote (task 1003271) ───────
|
|
1374
1377
|
//
|
|
1375
1378
|
// THE PROBLEM THIS EXISTS TO FIX. Nine items sat open for two days with zero
|
|
@@ -1485,6 +1488,7 @@ module.exports = {
|
|
|
1485
1488
|
maybeCloseAfterVote,
|
|
1486
1489
|
latestRejectionForIdea,
|
|
1487
1490
|
boardPassForIdea,
|
|
1491
|
+
boardPassedIdeaIds,
|
|
1488
1492
|
validateProposedConstitution,
|
|
1489
1493
|
proposeAmendment,
|
|
1490
1494
|
boardRoomView,
|
package/modules/government/db.js
CHANGED
|
@@ -712,6 +712,19 @@ async function getLatestPassedItem(subjectType, subjectId) {
|
|
|
712
712
|
return rows[0] || null;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
// Which of these subjects the board passed, in ONE read (task 1004560: the
|
|
716
|
+
// Ideas-to-ratify desk asks for every open Full Idea at once).
|
|
717
|
+
async function listPassedSubjectIds(subjectType, subjectIds) {
|
|
718
|
+
const ids = (subjectIds || []).map(Number).filter((n) => Number.isInteger(n) && n > 0);
|
|
719
|
+
if (!ids.length) return [];
|
|
720
|
+
const { rows } = await pool.query(
|
|
721
|
+
`SELECT DISTINCT subject_id FROM government_board_items
|
|
722
|
+
WHERE subject_type = $1 AND subject_id = ANY($2::bigint[]) AND outcome = 'passed'`,
|
|
723
|
+
[subjectType, ids],
|
|
724
|
+
);
|
|
725
|
+
return rows.map((r) => String(r.subject_id));
|
|
726
|
+
}
|
|
727
|
+
|
|
715
728
|
// The expiry sweep's read (R11): every OPEN item whose deadline has passed, on
|
|
716
729
|
// the database clock, oldest deadline first. Served by the partial index
|
|
717
730
|
// government_board_items_open_by_deadline (government_003 built it for exactly
|
|
@@ -872,6 +885,7 @@ module.exports = {
|
|
|
872
885
|
getExpiredOpenBoardItems,
|
|
873
886
|
getLatestReturnedItemWithObjection,
|
|
874
887
|
getLatestPassedItem,
|
|
888
|
+
listPassedSubjectIds,
|
|
875
889
|
createAmendment,
|
|
876
890
|
getAmendment,
|
|
877
891
|
listDecisionRules,
|
|
@@ -50,7 +50,9 @@ async function applyPassedDecisionRules(client, closedRow, amendment) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// A passed matter: act where the building can act by itself, and build the
|
|
53
|
-
// announcement. Never throws — the decision already stands.
|
|
53
|
+
// announcement. Never throws — the decision already stands. Runs after the
|
|
54
|
+
// close commits (People since task 1004529; Automation, Release, Review and
|
|
55
|
+
// Treasury since task 1004561, through their owning modules' acts).
|
|
54
56
|
async function actOnPassedMatter(closed) {
|
|
55
57
|
const area = decisionRules.areaForSubjectType(closed.subject_type);
|
|
56
58
|
let matter = null;
|
|
@@ -71,11 +73,30 @@ async function actOnPassedMatter(closed) {
|
|
|
71
73
|
await db.unassignRank({ builderId: action.builder_id, rankKey: action.rank_key });
|
|
72
74
|
result = { ...result, action: action.type, done: true, builder_id: String(action.builder_id), rank_key: action.rank_key };
|
|
73
75
|
}
|
|
76
|
+
} else if (area !== 'people' && action && MATTER_ACTIONS[area]) {
|
|
77
|
+
// The owning module carries it out, through its direct route's own
|
|
78
|
+
// function (task 1004561). Re-validated first: the stored row was checked
|
|
79
|
+
// when opened and is immutable since, so a refusal means it was altered.
|
|
80
|
+
const v = validateMatterAction(area, action);
|
|
81
|
+
const doer = v.ok ? matterActFor(area, v.action.type) : null;
|
|
82
|
+
if (!v.ok) {
|
|
83
|
+
result = { ...result, action: action.type || null, done: false, reason: 'bad_action' };
|
|
84
|
+
} else if (!doer) {
|
|
85
|
+
result = { ...result, action: v.action.type, done: false, reason: 'not_wired' };
|
|
86
|
+
} else {
|
|
87
|
+
const out = (await doer.act(v.action, { decidedBy: matter.proposed_by, matterId: String(matter.id) })) || {};
|
|
88
|
+
result = { ...result, action: v.action.type, done: out.done === true, ...(out.reason ? { reason: String(out.reason) } : {}) };
|
|
89
|
+
}
|
|
74
90
|
}
|
|
75
91
|
} catch (err) {
|
|
76
92
|
log.error(`[government] matter ${closed.subject_id} passed at ${area} but its act did NOT run (re-drivable from the passed item): ${err && err.message}`);
|
|
77
93
|
result = { ...result, done: false, reason: 'act_failed' };
|
|
78
94
|
}
|
|
95
|
+
// A refusal (already decided elsewhere, the module off) leaves the decision
|
|
96
|
+
// standing; it is said loudly here and carried as `acted: false`.
|
|
97
|
+
if (result.action && !result.done && result.reason !== 'act_failed') {
|
|
98
|
+
log.warn(`[government] matter ${closed.subject_id} passed at ${area} but its act was refused: ${result.reason || 'not done'}`);
|
|
99
|
+
}
|
|
79
100
|
return {
|
|
80
101
|
result,
|
|
81
102
|
payload: {
|
|
@@ -153,22 +174,74 @@ async function decisionRulesView() {
|
|
|
153
174
|
// the whole point — People decided by a council's consent sits as exactly that.
|
|
154
175
|
const MATTER_TITLE_MAX = 200;
|
|
155
176
|
const MATTER_DETAIL_MAX = 5000;
|
|
156
|
-
|
|
177
|
+
// What a passed matter may carry out, per building (task 1004561). People acts
|
|
178
|
+
// here, in this module; every other act is carried out by the module that owns
|
|
179
|
+
// the building's direct route, through the same function that route calls,
|
|
180
|
+
// contributed to 'governor.matterAct' as { area, type, act }. This table is only
|
|
181
|
+
// the SHAPE of each act (field → kind), checked when the matter is opened and
|
|
182
|
+
// again before it acts. 'id' is a positive integer, 'rank' a custom rank key,
|
|
183
|
+
// 'usd' a non-negative amount or null (no limit).
|
|
184
|
+
const MATTER_ACTIONS = Object.freeze({
|
|
185
|
+
people: Object.freeze({
|
|
186
|
+
assign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
|
|
187
|
+
unassign_rank: Object.freeze({ builder_id: 'id', rank_key: 'rank' }),
|
|
188
|
+
}),
|
|
189
|
+
automation: Object.freeze({
|
|
190
|
+
switch_on: Object.freeze({}),
|
|
191
|
+
switch_off: Object.freeze({}),
|
|
192
|
+
allow_goal: Object.freeze({ goal_id: 'id' }),
|
|
193
|
+
}),
|
|
194
|
+
release: Object.freeze({
|
|
195
|
+
approve_override: Object.freeze({ request_id: 'id' }),
|
|
196
|
+
approve_gate: Object.freeze({ pr: 'id' }),
|
|
197
|
+
}),
|
|
198
|
+
review: Object.freeze({
|
|
199
|
+
satisfy_criterion: Object.freeze({ criterion_id: 'id' }),
|
|
200
|
+
}),
|
|
201
|
+
treasury: Object.freeze({
|
|
202
|
+
set_budget: Object.freeze({ builder_id: 'id', monthly_usd: 'usd' }),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
const MATTER_ACT_POINT = 'governor.matterAct';
|
|
206
|
+
const MAX_BUDGET_USD = 100000;
|
|
207
|
+
|
|
208
|
+
function fieldValue(kind, raw) {
|
|
209
|
+
if (kind === 'id') {
|
|
210
|
+
const n = Number(raw);
|
|
211
|
+
return Number.isSafeInteger(n) && n > 0 && raw !== null && raw !== '' && typeof raw !== 'boolean' ? { ok: true, v: n } : { ok: false };
|
|
212
|
+
}
|
|
213
|
+
if (kind === 'rank') return typeof raw === 'string' && /^[a-z][a-z0-9-]*$/.test(raw) && raw.length <= 64 ? { ok: true, v: raw } : { ok: false };
|
|
214
|
+
if (kind === 'usd') {
|
|
215
|
+
if (raw === null) return { ok: true, v: null };
|
|
216
|
+
return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 && raw <= MAX_BUDGET_USD ? { ok: true, v: raw } : { ok: false };
|
|
217
|
+
}
|
|
218
|
+
return { ok: false };
|
|
219
|
+
}
|
|
157
220
|
|
|
158
221
|
function validateMatterAction(area, action) {
|
|
159
222
|
if (action == null) return { ok: true, action: null };
|
|
160
|
-
|
|
223
|
+
const acts = MATTER_ACTIONS[area];
|
|
224
|
+
if (!acts) return { ok: false, reason: 'action_not_supported_here', expected: `a matter acts by itself only at ${Object.keys(MATTER_ACTIONS).join(', ')}` };
|
|
161
225
|
if (typeof action !== 'object' || Array.isArray(action)) return { ok: false, reason: 'bad_action' };
|
|
226
|
+
const shape = acts[action.type];
|
|
227
|
+
if (!shape) return { ok: false, reason: 'bad_action', expected: Object.keys(acts) };
|
|
162
228
|
for (const k of Object.keys(action)) {
|
|
163
|
-
if (
|
|
229
|
+
if (k !== 'type' && !Object.prototype.hasOwnProperty.call(shape, k)) return { ok: false, reason: 'unknown_field', field: `action.${k}` };
|
|
164
230
|
}
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return { ok: false, reason: 'bad_action', field:
|
|
231
|
+
const out = { type: action.type };
|
|
232
|
+
for (const [k, kind] of Object.entries(shape)) {
|
|
233
|
+
if (!Object.prototype.hasOwnProperty.call(action, k)) return { ok: false, reason: 'bad_action', field: `action.${k}` };
|
|
234
|
+
const f = fieldValue(kind, action[k]);
|
|
235
|
+
if (!f.ok) return { ok: false, reason: 'bad_action', field: `action.${k}` };
|
|
236
|
+
out[k] = f.v;
|
|
170
237
|
}
|
|
171
|
-
return { ok: true, action:
|
|
238
|
+
return { ok: true, action: out };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The owning module's act for (area, type), or null when that module is off.
|
|
242
|
+
function matterActFor(area, type) {
|
|
243
|
+
return api.contributions(MATTER_ACT_POINT)
|
|
244
|
+
.find((c) => c && c.area === area && c.type === type && typeof c.act === 'function') || null;
|
|
172
245
|
}
|
|
173
246
|
|
|
174
247
|
async function openMatter({ area, title, detailMd = null, action = null, proposedBy } = {}) {
|
|
@@ -190,7 +263,10 @@ async function openMatter({ area, title, detailMd = null, action = null, propose
|
|
|
190
263
|
};
|
|
191
264
|
}
|
|
192
265
|
if (!resolved.sitting) return { ok: false, reason: 'no_one_holds_this', area, what_this_means: `No one holds ${resolved.name} yet, so nothing there can be decided.` };
|
|
193
|
-
if (a.action) {
|
|
266
|
+
if (a.action && area !== 'people' && !matterActFor(area, a.action.type)) {
|
|
267
|
+
return { ok: false, reason: 'action_not_wired', area, what_this_means: `Nothing at ${resolved.name} can carry that out right now, so it would pass and do nothing. Turn on the module that owns ${resolved.name}, or file the matter without an act.` };
|
|
268
|
+
}
|
|
269
|
+
if (a.action && area === 'people') {
|
|
194
270
|
const rank = await db.getRankByKey(a.action.rank_key);
|
|
195
271
|
if (!rank) return { ok: false, reason: 'unknown_rank', rank_key: a.action.rank_key };
|
|
196
272
|
if (rank.is_system) return { ok: false, reason: 'seeded_rank', rank_key: a.action.rank_key, expected: 'a custom rank — a seeded rank follows the builder\'s rank' };
|
|
@@ -295,6 +371,9 @@ module.exports = {
|
|
|
295
371
|
proposeDecisionRules,
|
|
296
372
|
decisionRulesView,
|
|
297
373
|
validateMatterAction,
|
|
374
|
+
matterActFor,
|
|
375
|
+
MATTER_ACTIONS,
|
|
376
|
+
MATTER_ACT_POINT,
|
|
298
377
|
openMatter,
|
|
299
378
|
checkActAlone,
|
|
300
379
|
};
|
|
@@ -101,6 +101,11 @@ const DESKS = Object.freeze([
|
|
|
101
101
|
['automation.switch', 'automation'], ['automation.goals', 'automation'], ['automation.runners', 'automation'],
|
|
102
102
|
['people.invite', 'people'], ['release.core', 'release'],
|
|
103
103
|
['rules.settings', 'rules'], ['rules.modules', 'rules'], ['treasury.backup', 'treasury'],
|
|
104
|
+
// the Planning desks (task 1004560): the goals in their worked order, and the
|
|
105
|
+
// ideas a ratifier may take from here
|
|
106
|
+
['planning.order', 'planning'], ['planning.ratify', 'planning'],
|
|
107
|
+
// task 1004561: a builder's monthly spending limit, and a sign-off undone
|
|
108
|
+
['treasury.budgets', 'treasury'], ['review.signed', 'review'],
|
|
104
109
|
].map(([key, building]) => Object.freeze({ key, building })));
|
|
105
110
|
|
|
106
111
|
module.exports = {
|
|
@@ -250,8 +250,19 @@ function normalizeDeskRow(raw, { desk, building }) {
|
|
|
250
250
|
opened_at: iso(raw.opened_at),
|
|
251
251
|
ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
|
|
252
252
|
acts,
|
|
253
|
+
...deskExtras(raw),
|
|
253
254
|
};
|
|
254
255
|
}
|
|
256
|
+
// Two optional shapes a desk row may carry (task 1004560): `pips`, promises kept
|
|
257
|
+
// of total drawn as squares (at most 12), and `set`, the ordered set the row sits
|
|
258
|
+
// in (a version id) so the desk can reorder its rows and save the order whole.
|
|
259
|
+
function deskExtras(raw) {
|
|
260
|
+
const out = {};
|
|
261
|
+
const p = raw.pips;
|
|
262
|
+
if (p && Number.isInteger(p.of) && p.of > 0 && p.of <= 12 && Number.isInteger(p.on) && p.on >= 0 && p.on <= p.of) out.pips = { on: p.on, of: p.of };
|
|
263
|
+
if (typeof raw.set === 'string' && /^[A-Za-z0-9._-]{1,64}$/.test(raw.set)) out.set = raw.set;
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
255
266
|
|
|
256
267
|
// One floor as a contributor handed it → { count, line, lines, hot }. Pure.
|
|
257
268
|
// Anything that is not a plain object is not a floor (→ null, read as
|
|
@@ -110,6 +110,7 @@ const governmentPort = {
|
|
|
110
110
|
// is no counterpart that records an outcome, and there must never be one: an
|
|
111
111
|
// outcome is written by a close, never by the module reading it.
|
|
112
112
|
boardPassForIdea: board.boardPassForIdea,
|
|
113
|
+
boardPassedIdeaIds: board.boardPassedIdeaIds,
|
|
113
114
|
// task 1004429 (BV2.PS04): a finished genesis stage is put to the board — the
|
|
114
115
|
// genesis home (task 1004423) calls this when a stage's tasks are done. The
|
|
115
116
|
// finished-tasks gate lives inside board.js; the item is an ordinary one under
|
|
@@ -789,7 +790,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
789
790
|
proposedBy: req.builder.id,
|
|
790
791
|
});
|
|
791
792
|
if (!out.ok) {
|
|
792
|
-
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
|
|
793
|
+
if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this' || out.reason === 'action_not_wired') {
|
|
793
794
|
return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
|
|
794
795
|
}
|
|
795
796
|
return res.fail('invalid_matter', 400, {
|
|
@@ -512,6 +512,60 @@
|
|
|
512
512
|
ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
|
|
513
513
|
ACT_ROUTES.invite = { invite: function (id, inp) { return str(inp.option).trim() ? post('/access-requests/invite', { github_login: str(inp.option).trim() }) : null; } };
|
|
514
514
|
ACT_ROUTES.autonomy_fence = ACT_ROUTES.autobongos_fence;
|
|
515
|
+
// The Planning desks (task 1004560). A blocker is decided by one of its chips,
|
|
516
|
+
// whose words are the resolution note; a criterion proposal ratifies in one
|
|
517
|
+
// tap; a Full Idea the board carried becomes a goal in the version its chip names.
|
|
518
|
+
ACT_ROUTES.blockers = {
|
|
519
|
+
decide: function (id, inp) { return str(inp.option).trim() ? post('/blockers/' + id + '/resolve', { resolution_note: 'Decided on the floor desk: ' + str(inp.option).trim() + '.' }) : null; },
|
|
520
|
+
};
|
|
521
|
+
// task 1004561: a builder's monthly limit (PATCH /builders/:id/budget; the
|
|
522
|
+
// 'none' chip clears it), and a sign-off undone (POST /done-when/:id/unsatisfy,
|
|
523
|
+
// the chip's words as the reason it logs).
|
|
524
|
+
ACT_ROUTES.builder_budget = {
|
|
525
|
+
budget: function (id, inp) { return inp.option ? call('PATCH', '/builders/' + id + '/budget', { monthly_budget_usd: inp.option === 'none' ? null : Number(inp.option) }) : null; },
|
|
526
|
+
};
|
|
527
|
+
ACT_ROUTES.signed_criterion = { unsign: function (id, inp) { return post('/done-when/' + id + '/unsatisfy', { reason: str(inp.label) || 'undone on the floor desk' }); } };
|
|
528
|
+
// The same acts at a building a sitting decides (task 1004561): filed as the
|
|
529
|
+
// building's matter, which carries the act out once it passes. Each returns
|
|
530
|
+
// { title, action } for POST /government/board/matters.
|
|
531
|
+
var COUNCIL_ACTS = {
|
|
532
|
+
autobongos_fence: {
|
|
533
|
+
switch_on: function () { return { title: 'Switch unattended building on', action: { type: 'switch_on' } }; },
|
|
534
|
+
switch_off: function () { return { title: 'Switch unattended building off', action: { type: 'switch_off' } }; },
|
|
535
|
+
allow: function (id, inp) { return inp.option ? { title: 'Allow ' + str(inp.label || 'goal ' + inp.option) + ' to build unattended', action: { type: 'allow_goal', goal_id: Number(inp.option) } } : null; },
|
|
536
|
+
},
|
|
537
|
+
criteria_uat: { force: function (id, inp, who) { return { title: 'Sign off: ' + who, action: { type: 'satisfy_criterion', criterion_id: Number(id) } }; } },
|
|
538
|
+
override_requests: { release: function (id, inp, who) { return { title: 'Release past its check: ' + who, action: { type: 'approve_override', request_id: Number(id) } }; } },
|
|
539
|
+
gate_approvals: { release: function (id, inp, who) { return { title: 'Release from the gate: ' + who, action: { type: 'approve_gate', pr: Number(id) } }; } },
|
|
540
|
+
builder_budget: {
|
|
541
|
+
budget: function (id, inp, who) { return inp.option ? { title: who + ': ' + str(inp.label || inp.option), action: { type: 'set_budget', builder_id: Number(id), monthly_usd: inp.option === 'none' ? null : Number(inp.option) } } : null; },
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
COUNCIL_ACTS.autonomy_fence = COUNCIL_ACTS.autobongos_fence;
|
|
545
|
+
COUNCIL_ACTS.autonomy_goal = { allow: COUNCIL_ACTS.autobongos_fence.allow };
|
|
546
|
+
function councilRequest(s, key, inp) {
|
|
547
|
+
var make = COUNCIL_ACTS[s.kind] && COUNCIL_ACTS[s.kind][key];
|
|
548
|
+
var m = make ? make(decoded(s.id), inp, str(s.who) || 'a matter') : null;
|
|
549
|
+
return m ? post('/government/board/matters', { area: s.council, title: str(m.title).slice(0, 200), action: m.action }) : null;
|
|
550
|
+
}
|
|
551
|
+
ACT_ROUTES.criterion_proposal = { ratify: function (id) { return post('/inbox/' + id + '/ratify', { notes_md: 'Ratified on the floor desk.' }); } };
|
|
552
|
+
ACT_ROUTES.full_idea = {
|
|
553
|
+
ratify_goal: function (id, inp) { return inp.option ? post('/inbox/' + id + '/ratify-goal', { target_version: str(inp.option), notes_md: 'Ratified on the floor desk.' }) : null; },
|
|
554
|
+
};
|
|
555
|
+
// The Priorities desk's save (task 1004560): the new order goes to
|
|
556
|
+
// POST /goals/reorder, or, where the decision table puts Planning to a sitting,
|
|
557
|
+
// it is filed as a Planning matter for the sitting to decide. save is the desk
|
|
558
|
+
// model's { set, ids, names, council }.
|
|
559
|
+
function orderRequest(save) {
|
|
560
|
+
var s = save || {};
|
|
561
|
+
var ids = (s.ids || []).map(Number).filter(function (n) { return n > 0 && Math.floor(n) === n; });
|
|
562
|
+
if (!ids.length) return null;
|
|
563
|
+
if (s.council) {
|
|
564
|
+
var lines = (s.names || []).map(function (n, i) { return (i + 1) + '. ' + str(n); });
|
|
565
|
+
return post('/government/board/matters', { area: 'planning', title: 'A new order for the goals', detail_md: 'Proposed on the floor desk, for version ' + str(s.set) + ':\n\n' + lines.join('\n') });
|
|
566
|
+
}
|
|
567
|
+
return post('/goals/reorder', { version_id: str(s.set), ordered_ids: ids });
|
|
568
|
+
}
|
|
515
569
|
function decoded(encoded) { return decodeURIComponent(encoded); }
|
|
516
570
|
// input: { reason, section } for object; { text } for revise.
|
|
517
571
|
function request(cv, key, input) {
|
|
@@ -541,6 +595,7 @@
|
|
|
541
595
|
if (key === 'fine') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'approve' });
|
|
542
596
|
if (key === 'refuse') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'deny' });
|
|
543
597
|
}
|
|
598
|
+
if (s.council && COUNCIL_ACTS[s.kind] && COUNCIL_ACTS[s.kind][key]) return councilRequest(s, key, inp);
|
|
544
599
|
if (ACT_ROUTES[s.kind] && ACT_ROUTES[s.kind][key]) return ACT_ROUTES[s.kind][key](enc(s.id), inp);
|
|
545
600
|
if (s.kind === 'override_requests' && key === 'release') return post('/override-requests/' + enc(s.id) + '/decide', { decision: 'approve' });
|
|
546
601
|
if (s.kind === 'gate_approvals' && key === 'release') return post('/gate-approvals/' + enc(s.id) + '/approve', {});
|
|
@@ -565,6 +620,9 @@
|
|
|
565
620
|
var r = res || {};
|
|
566
621
|
var who = cv && cv.subject && cv.subject.who ? cv.subject.who : 'They';
|
|
567
622
|
var b = cv ? cv.building : null;
|
|
623
|
+
if (cv && cv.subject && cv.subject.council && COUNCIL_ACTS[cv.subject.kind] && COUNCIL_ACTS[cv.subject.kind][key]) {
|
|
624
|
+
return { toast: 'Sent to the council. It is carried out when they pass it.', building: b, kind: 'light', close: false };
|
|
625
|
+
}
|
|
568
626
|
if (key === 'for') {
|
|
569
627
|
if (r.item_closed && r.outcome === 'passed') return { toast: 'Passed. It is in force from today.', building: b, kind: 'rise', close: false };
|
|
570
628
|
if (r.item_closed) return { toast: 'The sitting closed: it went back to its author.', building: b, kind: 'settle', close: false };
|
|
@@ -594,8 +652,10 @@
|
|
|
594
652
|
tier: '{who} has a new tier.', deactivate: '{who} is deactivated.', give: 'Rank given.', take: 'Rank taken back.',
|
|
595
653
|
check: 'Checking it. The answer lands on this floor in a couple of minutes; nothing moves.', take_core: 'Taking it. The core moves on the runner\'s next tick.',
|
|
596
654
|
rot: 'Rot timer set.', module_on: 'Turned on.', module_off: 'Turned off.', backup: 'Backing up now.', invite: '{who} is invited.',
|
|
655
|
+
decide: 'Decided. The work it held moves again.', ratify: 'Ratified. The promise is on its version.', ratify_goal: 'Ratified. It is a goal now.',
|
|
656
|
+
budget: '{who} has a new monthly limit.', unsign: 'Undone. The promise is open again.',
|
|
597
657
|
};
|
|
598
|
-
var ACT_KINDS = { check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
|
|
658
|
+
var ACT_KINDS = { budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
|
|
599
659
|
|
|
600
660
|
// A refused act, in words. The server's own message wins when it has one.
|
|
601
661
|
var REFUSALS = {
|
|
@@ -786,7 +846,7 @@
|
|
|
786
846
|
|
|
787
847
|
var api = {
|
|
788
848
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
789
|
-
model: model, actsCard: actsCard, request: request, check: check, answer: answer, refusal: refusal, render: render,
|
|
849
|
+
model: model, actsCard: actsCard, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
|
|
790
850
|
memberWords: memberWords, windowWords: windowWords,
|
|
791
851
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
792
852
|
change: changeOf,
|
|
@@ -38,10 +38,20 @@
|
|
|
38
38
|
// GovDesk.fit(s, n, words) the line formatter
|
|
39
39
|
// GovDesk.breaks(desk) [] when the desk keeps the line rule
|
|
40
40
|
// GovDesk.todayPips(n) the street header's meter
|
|
41
|
+
// GovDesk.moved(order, key, by) a desk order with one row moved
|
|
41
42
|
// GovDesk.LIMITS, ACT_WORDS, CHIPS, NOUNS, CLEAR
|
|
42
43
|
// A desk row is { key, matter, ini, name, fact, facts[], actions[{ key, label,
|
|
43
44
|
// tone, go, ask }], chips { for, ask, options[{ key, label, input }] } | null,
|
|
44
45
|
// wait, needs, card }: `card` is the GovMatterCard model the act is sent with.
|
|
46
|
+
//
|
|
47
|
+
// THE PLANNING DESKS (task 1004560). A row may carry `pips` [{ key, on }]: a
|
|
48
|
+
// goal's promises kept of total, or a vote's seats (filled = a yes). A row of an
|
|
49
|
+
// ordered set (the Priorities desk: `order` is its version) is moved with its
|
|
50
|
+
// two arrows; input.order is the order the viewer has made, and a changed order
|
|
51
|
+
// gives the desk a `save` { line, label, set, ids, names, council }: the
|
|
52
|
+
// council when the decision table puts Planning to a sitting that cannot act
|
|
53
|
+
// alone, so the save files a Planning matter instead of setting the order. A
|
|
54
|
+
// blocker's chips stand open (`show`): deciding it IS picking one.
|
|
45
55
|
|
|
46
56
|
(function (root) {
|
|
47
57
|
'use strict';
|
|
@@ -54,7 +64,7 @@
|
|
|
54
64
|
nameWords: 3, name: 20, fact: 26, facts: 3, factLen: 24, actions: 2,
|
|
55
65
|
tiles: 3, tileValue: 8, tileWords: 2, tileLabel: 14,
|
|
56
66
|
status: 44, clearLine: 30, clearSub: 40, record: 4, recordLen: 30,
|
|
57
|
-
chips: 5, chip: 16, todayTarget: 10,
|
|
67
|
+
chips: 5, chip: 16, todayTarget: 10, pips: 12, saveLine: 48,
|
|
58
68
|
});
|
|
59
69
|
|
|
60
70
|
// One line, never more: the house rule for every row, tile and status. A cap
|
|
@@ -105,15 +115,29 @@
|
|
|
105
115
|
switch_on: 'switch on', switch_off: 'switch off', allow: 'allow', disallow: 'disallow', first: 'first',
|
|
106
116
|
pause: 'pause', resume: 'resume', tier: 'tier', deactivate: 'deactivate', give: 'give', take: 'take back',
|
|
107
117
|
check: 'check', take_core: 'take it', rot: 'set', module_on: 'turn on', module_off: 'turn off', backup: 'back up', invite: 'invite', open: 'open',
|
|
118
|
+
// the Planning desks (task 1004560)
|
|
119
|
+
decide: 'decide', ratify: 'ratify', ratify_goal: 'make goal', up: 'move up', down: 'move down',
|
|
120
|
+
// task 1004561: a builder's monthly limit, and a sign-off undone
|
|
121
|
+
budget: 'set limit', unsign: 'undo',
|
|
108
122
|
});
|
|
109
123
|
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
110
|
-
const ASKS = Object.freeze({ check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
124
|
+
const ASKS = Object.freeze({ budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
111
125
|
// How a floor counts rows nobody is waiting on (desk rows), by its desk.
|
|
112
126
|
const DESK_NOUNS = Object.freeze({
|
|
113
127
|
'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
|
|
114
128
|
'automation.switch': 'switch', 'automation.goals': 'allowed', 'automation.runners': 'runners',
|
|
115
129
|
'people.invite': 'invites', 'release.core': 'update', 'rules.settings': 'settings', 'rules.modules': 'modules', 'treasury.backup': 'backups',
|
|
130
|
+
'planning.order': 'goals in order', 'planning.ratify': 'to ratify',
|
|
131
|
+
'treasury.budgets': 'builders', 'review.signed': 'signed off',
|
|
116
132
|
});
|
|
133
|
+
// Is this building decided by a sitting that cannot act alone (task 1004561)?
|
|
134
|
+
// The decision table's row, as the docket's buildings carry it: then a desk act
|
|
135
|
+
// is filed as the building's matter, and carried out once the sitting passes it.
|
|
136
|
+
function councilOf(docket, key) {
|
|
137
|
+
const b = docket && Array.isArray(docket.buildings) ? docket.buildings.find((x) => x && x.key === key) : null;
|
|
138
|
+
const d = b && b.decision ? b.decision : null;
|
|
139
|
+
return !!(d && d.sitting && ['decide', 'first'].indexOf(d.rule) < 0);
|
|
140
|
+
}
|
|
117
141
|
// An objection's reason as one tap: the five sections the server accepts
|
|
118
142
|
// (street-card.js SECTIONS), each in at most 16 characters.
|
|
119
143
|
const CHIPS = Object.freeze({ scope: 'what it changes', purpose: 'its purpose', why_matters: 'why it matters', fit: 'how it fits', success: 'how to measure' });
|
|
@@ -185,6 +209,8 @@
|
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
const cv = C && C.model ? C.model({ docket: ctx.docket, key: m.id, room: ctx.room, constitution: ctx.constitution, now: now, viewerId: ctx.viewerId }) : null;
|
|
212
|
+
// a release held for a sitting goes to it as a matter (task 1004561)
|
|
213
|
+
if (cv && cv.subject && cv.subject.itemId == null) cv.subject.council = councilOf(ctx.docket, cv.building) ? cv.building : null;
|
|
188
214
|
const actions = [];
|
|
189
215
|
let chips = null;
|
|
190
216
|
if (cv && !cv.readOnly && !cv.loading) {
|
|
@@ -200,6 +226,8 @@
|
|
|
200
226
|
return {
|
|
201
227
|
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
202
228
|
actions: actions, chips: chips, choices: chips ? { [chips.for]: chips } : {}, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
229
|
+
// a vote shows its seats: one pip each, filled for a yes
|
|
230
|
+
pips: cv && Array.isArray(cv.seats) && cv.seats.length ? cv.seats.slice(0, LIMITS.pips).map((s, i) => ({ key: 's' + i, on: s.pos === 'for' })) : null,
|
|
203
231
|
};
|
|
204
232
|
}
|
|
205
233
|
|
|
@@ -210,6 +238,8 @@
|
|
|
210
238
|
// goes to the matter's own page (a sign-off that needs a recording).
|
|
211
239
|
function actedRow(acts, card, ctx) {
|
|
212
240
|
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || 'goal ' + g.id)]));
|
|
241
|
+
// an act at a building a sitting decides is filed as its matter (task 1004561)
|
|
242
|
+
if (card && card.subject) card.subject.council = councilOf(ctx.docket, card.building) ? card.building : null;
|
|
213
243
|
const actions = [];
|
|
214
244
|
const choices = {};
|
|
215
245
|
acts.forEach((a) => {
|
|
@@ -232,7 +262,9 @@
|
|
|
232
262
|
actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
|
|
233
263
|
});
|
|
234
264
|
const first = actions.find((a) => a.ask);
|
|
235
|
-
|
|
265
|
+
// deciding a blocker is picking one of its options: its chips stand open
|
|
266
|
+
const show = first && first.key === 'decide' && choices.decide ? 'decide' : null;
|
|
267
|
+
return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, show: show, wait: actions.length ? '' : 'not from here', card: card };
|
|
236
268
|
}
|
|
237
269
|
|
|
238
270
|
// A desk row (task 1004559): something on this floor the viewer may act on that
|
|
@@ -253,6 +285,8 @@
|
|
|
253
285
|
return Object.assign(actedRow(d.acts || [], card || { href: null }, ctx), {
|
|
254
286
|
key: str(d.id), matter: str(d.id), ini: initials(name), name: name, fact: fit(d.fact, LIMITS.fact), facts: facts,
|
|
255
287
|
needs: d.needs === true, source: d.desk, opened: d.opened_at || null, desk: true, tool: d.tool === true, over: d.over || null,
|
|
288
|
+
order: d.set || null, goalId: d.set && ref.id != null ? str(ref.id) : null, full: full,
|
|
289
|
+
pips: d.pips ? Array.from({ length: Math.min(d.pips.of, LIMITS.pips) }, (_, i) => ({ key: 'p' + i, on: i < d.pips.on })) : null,
|
|
256
290
|
});
|
|
257
291
|
}
|
|
258
292
|
// The desk rows of one floor: those of the desk its PLAN entry names.
|
|
@@ -292,13 +326,27 @@
|
|
|
292
326
|
const spec = plan.floors[floor];
|
|
293
327
|
const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
|
|
294
328
|
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done, goals: o.goals || [] };
|
|
295
|
-
|
|
329
|
+
let rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
|
|
330
|
+
// the Priorities desk: the ordered rows in the order the viewer has made
|
|
331
|
+
const ordered = rows.filter((r) => r.order);
|
|
332
|
+
let save = null;
|
|
333
|
+
if (ordered.length > 1) {
|
|
334
|
+
const base = ordered.map((r) => r.key);
|
|
335
|
+
const want = Array.isArray(o.order) ? o.order.map(str) : null;
|
|
336
|
+
const mine = want && want.length === base.length && base.every((k) => want.indexOf(k) >= 0) ? want : base;
|
|
337
|
+
const sorted = mine.map((k) => ordered.find((r) => r.key === k));
|
|
338
|
+
sorted.forEach((r, i) => { r.pos = i + 1; r.top = i === 0; r.bottom = i === sorted.length - 1; });
|
|
339
|
+
rows = rows.filter((r) => !r.order).concat(sorted);
|
|
340
|
+
if (mine.join('|') !== base.join('|')) save = saveOf(docket, o.key, sorted);
|
|
341
|
+
}
|
|
296
342
|
const bind = spec[2] || {};
|
|
297
343
|
const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
|
|
298
344
|
const needs = rows.filter((r) => r.needs);
|
|
299
345
|
const noun = NOUNS[src] || 'open';
|
|
300
346
|
// rows nobody waits on: 'moving' for matters, the desk's own noun for desk rows
|
|
301
|
-
|
|
347
|
+
// (rows waiting on no one, when all of them are desk rows, count in their desk's noun: 'to ratify')
|
|
348
|
+
const rest = rows.filter((r) => !r.needs && !r.tool);
|
|
349
|
+
const moving = rest.length && rest.every((r) => r.desk) ? (DESK_NOUNS[rest[0].source] || 'here') : 'moving';
|
|
302
350
|
const counted = rows.filter((r) => !r.tool);
|
|
303
351
|
const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
|
|
304
352
|
const cleared = !!o.cleared && rows.length === 0;
|
|
@@ -320,7 +368,13 @@
|
|
|
320
368
|
const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
|
|
321
369
|
tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
|
|
322
370
|
};
|
|
323
|
-
if (
|
|
371
|
+
if (ordered.length > 1) {
|
|
372
|
+
// the order's one number: the share of its goals' promises kept
|
|
373
|
+
const on = ordered.reduce((a, r) => a + (r.pips || []).filter((p) => p.on).length, 0);
|
|
374
|
+
const of = ordered.reduce((a, r) => a + (r.pips || []).length, 0);
|
|
375
|
+
tile(needs.length, noun);
|
|
376
|
+
if (of) tile(Math.round((100 * on) / of) + '%', 'promises kept');
|
|
377
|
+
} else if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
|
|
324
378
|
else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
|
|
325
379
|
tile(history.length, 'on record');
|
|
326
380
|
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
|
|
@@ -330,13 +384,50 @@
|
|
|
330
384
|
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
331
385
|
status: fit(status, LIMITS.status), hot: hot,
|
|
332
386
|
tiles: tiles.slice(0, LIMITS.tiles), rows: rows,
|
|
333
|
-
quiet: quiet, cleared: cleared,
|
|
387
|
+
quiet: quiet, cleared: cleared, save: save,
|
|
334
388
|
clearLine: fit(cleared ? (CLEAR[src] || 'Floor clear') : 'Nothing needs you here', LIMITS.clearLine),
|
|
335
389
|
clearSub: fit(cleared ? 'lit up · ' + (o.today || 0) + ' decided today' : fl.line, LIMITS.clearSub),
|
|
336
390
|
record: record,
|
|
337
391
|
};
|
|
338
392
|
}
|
|
339
393
|
|
|
394
|
+
// ---------- the Priorities desk's save ----------
|
|
395
|
+
// Who decides the order: the decision table's Planning row, as the docket's
|
|
396
|
+
// buildings carry it. A sitting whose rule cannot act alone means the save
|
|
397
|
+
// files a matter for it; the yes it needs is read from its seats.
|
|
398
|
+
function yesNeeded(rule, n) {
|
|
399
|
+
if (!(n > 0)) return null;
|
|
400
|
+
if (rule === 'majority') return Math.floor(n / 2) + 1;
|
|
401
|
+
if (rule === 'unanimous') return n;
|
|
402
|
+
if (rule === 'signoff') return 1;
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
function saveOf(docket, key, sorted) {
|
|
406
|
+
const b = docket && Array.isArray(docket.buildings) ? docket.buildings.find((x) => x && x.key === key) : null;
|
|
407
|
+
const d = b && b.decision ? b.decision : null;
|
|
408
|
+
const council = councilOf(docket, key);
|
|
409
|
+
const yes = council ? yesNeeded(d.rule, Number(b.holder_count)) : null;
|
|
410
|
+
const line = !council ? 'new order · saved for everyone'
|
|
411
|
+
: d.rule === 'consent' ? 'new order · to the council · unless one objects'
|
|
412
|
+
: 'new order · goes to the council' + (yes ? ' · ' + yes + ' yes needed' : '');
|
|
413
|
+
// what the toast says once it is sent: whole sentences, never the line cut up
|
|
414
|
+
const said = !council ? 'Saved. Goals are worked in this order now.'
|
|
415
|
+
: d.rule === 'consent' ? 'The new order is with the council. It passes unless someone objects.'
|
|
416
|
+
: 'The new order is with the council.' + (yes ? ' It needs ' + yes + ' yes.' : '');
|
|
417
|
+
return {
|
|
418
|
+
line: fit(line, LIMITS.saveLine), label: council ? 'send to council' : 'save order', council: council, said: said,
|
|
419
|
+
set: sorted[0].order, ids: sorted.map((r) => r.goalId), names: sorted.map((r) => r.full || r.name),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// An order with one row moved one place (by -1 up, +1 down); the ends hold.
|
|
423
|
+
function moved(order, key, by) {
|
|
424
|
+
const o = (order || []).slice();
|
|
425
|
+
const i = o.indexOf(key), j = i + by;
|
|
426
|
+
if (i < 0 || j < 0 || j >= o.length) return o;
|
|
427
|
+
o[i] = o[j]; o[j] = key;
|
|
428
|
+
return o;
|
|
429
|
+
}
|
|
430
|
+
|
|
340
431
|
// ---------- the rule as a checker ----------
|
|
341
432
|
function breaks(desk) {
|
|
342
433
|
const out = [];
|
|
@@ -364,7 +455,9 @@
|
|
|
364
455
|
if ((!c.field && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
|
|
365
456
|
});
|
|
366
457
|
if (r.actions.some((a) => a.ask && !(r.choices || {})[a.key])) bad('an ask without its own chips', r.name);
|
|
458
|
+
if (r.pips && r.pips.length > LIMITS.pips) bad('more than twelve pips', r.pips.length);
|
|
367
459
|
});
|
|
460
|
+
if (desk.save && (!oneLine(desk.save.line) || desk.save.line.length > LIMITS.saveLine)) bad('save line', desk.save.line);
|
|
368
461
|
if (desk.record.length > LIMITS.record || desk.record.some((x) => !oneLine(x.text) || x.text.length > LIMITS.recordLen)) bad('record line', desk.record.map((x) => x.text));
|
|
369
462
|
return out;
|
|
370
463
|
}
|
|
@@ -379,7 +472,7 @@
|
|
|
379
472
|
|
|
380
473
|
const api = {
|
|
381
474
|
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, ASKS: ASKS, DESK_NOUNS: DESK_NOUNS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
382
|
-
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
|
|
475
|
+
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips, moved: moved,
|
|
383
476
|
};
|
|
384
477
|
root.GovDesk = api;
|
|
385
478
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|