@bongos/core 1.21.34 → 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 +65 -50
- 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 +4 -0
- package/docs/page-inventory.json +10 -1
- package/docs/page-readings.json +356 -251
- package/modules/autonomy/governor-docket.js +68 -1
- 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-floors.js +20 -0
- package/modules/government/docket-vocab.js +19 -0
- package/modules/government/docket.js +114 -2
- package/modules/government/routes/government.js +15 -1
- package/modules/hall-ui/public/street-card.js +157 -1
- package/modules/hall-ui/public/street-desk.js +205 -11
- package/modules/hall-ui/public/street-inside.js +11 -8
- package/modules/hall-ui/public/street.css +16 -0
- package/modules/hall-ui/public/street.js +97 -9
- package/modules/hall-ui/public/street.states.json +355 -6
- package/modules/ideas/governor-docket.js +91 -0
- package/modules/lifecycle/governor-docket.js +93 -2
- 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/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 +16 -0
- package/src/bongos/route-rank-check.js +2 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +96 -1
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/governor_matter_acts.mjs +241 -0
- package/tests/hall_matter_card.mjs +11 -2
- package/tests/hall_street_desk.mjs +473 -4
- package/tests/hall_street_inside.mjs +3 -3
|
@@ -440,8 +440,133 @@
|
|
|
440
440
|
return sourceCard(cv, m);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// ---------- the acts the server hands a row (task 1004559) ----------
|
|
444
|
+
// A docket matter or desk row may carry `acts`: what THIS viewer may do, which
|
|
445
|
+
// the owning module decided from the same atom its route checks. The floor
|
|
446
|
+
// desk sends them through this card: { kind, id, who } is all a route needs.
|
|
447
|
+
// kind is the matter's source, or the desk row's ref type.
|
|
448
|
+
function actsCard(o) {
|
|
449
|
+
var x = o || {};
|
|
450
|
+
return {
|
|
451
|
+
key: str(x.key), building: x.building || null, where: '', when: '', title: str(x.title), href: safeHref(x.href),
|
|
452
|
+
source: str(x.kind), quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
453
|
+
result: '', actions: [], textActions: [], objectForm: null, reviseForm: null, readOnly: false, loading: false, lines: [],
|
|
454
|
+
subject: { kind: str(x.kind), id: str(x.id), who: str(x.who) },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
443
458
|
// ---------- act → route ----------
|
|
444
459
|
function post(url, body) { return { method: 'POST', url: API + url, body: body || {} }; }
|
|
460
|
+
function call(method, url, body) { return { method: method, url: API + url, body: body || {} }; }
|
|
461
|
+
// The routes behind the server-handed acts. inp.option is the chip's key, and
|
|
462
|
+
// inp.label its words (sent as the reason where a route keeps one).
|
|
463
|
+
var ACT_ROUTES = {
|
|
464
|
+
redteam_reports: {
|
|
465
|
+
confirm: function (id) { return post('/security/reports/' + id + '/confirm', {}); },
|
|
466
|
+
dispute: function (id) { return post('/security/reports/' + id + '/dispute', {}); },
|
|
467
|
+
},
|
|
468
|
+
vulnerability_report: { fix: function (id) { return post('/security/reports/' + id + '/fix', {}); } },
|
|
469
|
+
removal_proposals: {
|
|
470
|
+
remove: function (id) { return post('/removal-proposals/' + id + '/decide', { verdict: 'confirmed' }); },
|
|
471
|
+
keep: function (id) { return post('/removal-proposals/' + id + '/decide', { verdict: 'dismissed' }); },
|
|
472
|
+
},
|
|
473
|
+
criteria_uat: {
|
|
474
|
+
signoff: function (id) { return post('/done-when/' + id + '/uat', { kind: 'backend_signoff', live_attested: true }); },
|
|
475
|
+
force: function (id, inp) { return post('/done-when/' + id + '/satisfy', { override_reason: 'Signed off on the floor desk: ' + str(inp.label || 'forced') + '.' }); },
|
|
476
|
+
},
|
|
477
|
+
autobongos_fence: {
|
|
478
|
+
switch_on: function () { return post('/autonomy/fence', { enabled: true }); },
|
|
479
|
+
switch_off: function (id, inp) { return post('/autonomy/fence', { enabled: false, reason: str(inp.label) || 'switched off on the floor desk' }); },
|
|
480
|
+
allow: function (id, inp) { return inp.option ? post('/autonomy/fence/goals', { goal_id: Number(inp.option) }) : null; },
|
|
481
|
+
},
|
|
482
|
+
autonomy_goal: {
|
|
483
|
+
first: function (id) { return post('/autonomy/fence/priority', { goal_id: Number(id) }); },
|
|
484
|
+
disallow: function (id) { return call('DELETE', '/autonomy/fence/goals/' + id); },
|
|
485
|
+
allow: function (id, inp) { return inp.option ? post('/autonomy/fence/goals', { goal_id: Number(inp.option) }) : null; },
|
|
486
|
+
},
|
|
487
|
+
autonomy_runner: {
|
|
488
|
+
pause: function (id, inp) { return post('/autonomy/builders/' + id + '/pause', { paused: true, reason: str(inp.label) || 'paused on the floor desk' }); },
|
|
489
|
+
resume: function (id) { return post('/autonomy/builders/' + id + '/pause', { paused: false }); },
|
|
490
|
+
},
|
|
491
|
+
builder: {
|
|
492
|
+
tier: function (id, inp) { return inp.option ? call('PATCH', '/builders/' + id + '/rank', { rank: str(inp.option) }) : null; },
|
|
493
|
+
deactivate: function (id, inp) { return call('PATCH', '/builders/' + id + '/status', { status: 'inactive', reason: str(inp.label) || 'deactivated on the floor desk' }); },
|
|
494
|
+
},
|
|
495
|
+
custom_rank: {
|
|
496
|
+
give: function (id, inp) { return inp.option ? post('/government/assignments', { builder_id: Number(inp.option), rank_key: decoded(id) }) : null; },
|
|
497
|
+
take: function (id, inp) { return inp.option ? call('DELETE', '/government/assignments/' + encodeURIComponent(str(inp.option)) + '/' + id) : null; },
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
// The core update's two steps (POST /provisioning/instances/:id/core-upgrade):
|
|
501
|
+
// the server hands 'take' only once a dry run passed for that very version, and
|
|
502
|
+
// the chip carries the version, so no card moves the pin without that check.
|
|
503
|
+
ACT_ROUTES.core_pin = {
|
|
504
|
+
check: function (id, inp) { return inp.option ? post('/provisioning/instances/' + id + '/core-upgrade', { to: str(inp.option), dry_run: true }) : null; },
|
|
505
|
+
take: function (id, inp) { return inp.option ? post('/provisioning/instances/' + id + '/core-upgrade', { to: str(inp.option) }) : null; },
|
|
506
|
+
};
|
|
507
|
+
ACT_ROUTES.project_setting = { rot: function (id, inp) { return inp.option ? call('PATCH', '/project-settings/rot-days', { days: Number(inp.option) }) : null; } };
|
|
508
|
+
ACT_ROUTES.module = {
|
|
509
|
+
module_on: function (id) { return post('/modules/' + id + '/enable', { enabled: true }); },
|
|
510
|
+
module_off: function (id) { return post('/modules/' + id + '/enable', { enabled: false }); },
|
|
511
|
+
};
|
|
512
|
+
ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
|
|
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
|
+
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
|
+
}
|
|
569
|
+
function decoded(encoded) { return decodeURIComponent(encoded); }
|
|
445
570
|
// input: { reason, section } for object; { text } for revise.
|
|
446
571
|
function request(cv, key, input) {
|
|
447
572
|
if (!cv || !cv.subject) return null;
|
|
@@ -470,6 +595,8 @@
|
|
|
470
595
|
if (key === 'fine') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'approve' });
|
|
471
596
|
if (key === 'refuse') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'deny' });
|
|
472
597
|
}
|
|
598
|
+
if (s.council && COUNCIL_ACTS[s.kind] && COUNCIL_ACTS[s.kind][key]) return councilRequest(s, key, inp);
|
|
599
|
+
if (ACT_ROUTES[s.kind] && ACT_ROUTES[s.kind][key]) return ACT_ROUTES[s.kind][key](enc(s.id), inp);
|
|
473
600
|
if (s.kind === 'override_requests' && key === 'release') return post('/override-requests/' + enc(s.id) + '/decide', { decision: 'approve' });
|
|
474
601
|
if (s.kind === 'gate_approvals' && key === 'release') return post('/gate-approvals/' + enc(s.id) + '/approve', {});
|
|
475
602
|
return null; // hold, abstain and the core's release (a link) record nothing here
|
|
@@ -493,6 +620,9 @@
|
|
|
493
620
|
var r = res || {};
|
|
494
621
|
var who = cv && cv.subject && cv.subject.who ? cv.subject.who : 'They';
|
|
495
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
|
+
}
|
|
496
626
|
if (key === 'for') {
|
|
497
627
|
if (r.item_closed && r.outcome === 'passed') return { toast: 'Passed. It is in force from today.', building: b, kind: 'rise', close: false };
|
|
498
628
|
if (r.item_closed) return { toast: 'The sitting closed: it went back to its author.', building: b, kind: 'settle', close: false };
|
|
@@ -505,9 +635,28 @@
|
|
|
505
635
|
if (key === 'release') return { toast: cv && cv.source === 'gate_approvals' ? 'Released from the gate. It merges and goes live.' : 'Released. It goes live.', building: b, kind: 'rise', close: true };
|
|
506
636
|
if (key === 'fine') return { toast: cv && cv.source === 'goal_join_requests' ? 'Noted. ' + who + ' joins the team.' : 'Noted. ' + who + ' is invited in.', building: b, kind: 'rise', close: false };
|
|
507
637
|
if (key === 'refuse') return { toast: 'Noted. ' + who + ' is not let in.', building: b, kind: 'settle', close: false };
|
|
638
|
+
if (key === 'take' && cv && cv.subject && cv.subject.kind === 'core_pin') return { toast: ACT_TOASTS.take_core, building: b, kind: 'rise', close: false };
|
|
639
|
+
if (key === 'invite') return { toast: ACT_TOASTS.invite.replace('{who}', r.github_login || 'They'), building: b, kind: 'light', close: false };
|
|
640
|
+
if (ACT_TOASTS[key]) return { toast: ACT_TOASTS[key].replace('{who}', who), building: b, kind: ACT_KINDS[key] || 'light', close: false };
|
|
508
641
|
return { toast: '', building: null, kind: null, close: false };
|
|
509
642
|
}
|
|
510
643
|
|
|
644
|
+
// What the street says after a server-handed act (task 1004559).
|
|
645
|
+
var ACT_TOASTS = {
|
|
646
|
+
confirm: 'Confirmed. The report is on the record.', dispute: 'Disputed. The report is closed as not a flaw.', fix: 'Marked fixed.',
|
|
647
|
+
remove: 'Removal confirmed.', keep: 'Kept. The removal is dismissed.',
|
|
648
|
+
signoff: 'Signed off. The promise counts as kept.', force: 'Signed off by force. The reason is on the record.',
|
|
649
|
+
switch_on: 'Switched on. Unattended building can start.', switch_off: 'Switched off. Nothing new starts unattended.',
|
|
650
|
+
allow: 'Allowed. It may build that goal now.', disallow: 'No longer allowed.', first: 'First in line now.',
|
|
651
|
+
pause: '{who} is paused.', resume: '{who} may run again.',
|
|
652
|
+
tier: '{who} has a new tier.', deactivate: '{who} is deactivated.', give: 'Rank given.', take: 'Rank taken back.',
|
|
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.',
|
|
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.',
|
|
657
|
+
};
|
|
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' };
|
|
659
|
+
|
|
511
660
|
// A refused act, in words. The server's own message wins when it has one.
|
|
512
661
|
var REFUSALS = {
|
|
513
662
|
not_board_member: 'Only the members of this sitting may vote on it.',
|
|
@@ -522,6 +671,13 @@
|
|
|
522
671
|
proposer_would_lose_their_seat: 'Re-filing this would take your own seat. Change it in the Board Room.',
|
|
523
672
|
no_change: 'That already is how it works.',
|
|
524
673
|
preview_readonly: 'This preview records nothing.',
|
|
674
|
+
decided_by_sitting: 'The council decides this now. Put it to a sitting.',
|
|
675
|
+
cannot_change_own_status: 'Ask another archon to change your own standing.',
|
|
676
|
+
already_member: 'They already have an account here.',
|
|
677
|
+
bad_github_login: 'That does not look like a GitHub name.',
|
|
678
|
+
backup_already_running: 'A backup is already running.',
|
|
679
|
+
intent_conflict: 'Another core run is going. Try again when it settles.',
|
|
680
|
+
criterion_needs_recording: 'This one needs a recording. Sign it off on its goal page.',
|
|
525
681
|
};
|
|
526
682
|
function refusal(status, body) {
|
|
527
683
|
var e = body && body.error ? body.error : {};
|
|
@@ -690,7 +846,7 @@
|
|
|
690
846
|
|
|
691
847
|
var api = {
|
|
692
848
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
693
|
-
model: model, 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,
|
|
694
850
|
memberWords: memberWords, windowWords: windowWords,
|
|
695
851
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
696
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
|
|
@@ -97,7 +107,37 @@
|
|
|
97
107
|
gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
|
|
98
108
|
});
|
|
99
109
|
// The card's acts under the desk's words (the board's button labels).
|
|
100
|
-
const ACT_WORDS = Object.freeze({
|
|
110
|
+
const ACT_WORDS = Object.freeze({
|
|
111
|
+
for: 'yes', object: 'no', fine: 'let in', refuse: 'not now', release: 'release', hold: 'hold',
|
|
112
|
+
// the acts the server hands a row (task 1004559), each its old page's route
|
|
113
|
+
confirm: 'confirm', dispute: 'dispute', fix: 'fixed', remove: 'remove', keep: 'keep',
|
|
114
|
+
signoff: 'sign off', record: 'record it', force: 'force',
|
|
115
|
+
switch_on: 'switch on', switch_off: 'switch off', allow: 'allow', disallow: 'disallow', first: 'first',
|
|
116
|
+
pause: 'pause', resume: 'resume', tier: 'tier', deactivate: 'deactivate', give: 'give', take: 'take back',
|
|
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',
|
|
122
|
+
});
|
|
123
|
+
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
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' });
|
|
125
|
+
// How a floor counts rows nobody is waiting on (desk rows), by its desk.
|
|
126
|
+
const DESK_NOUNS = Object.freeze({
|
|
127
|
+
'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
|
|
128
|
+
'automation.switch': 'switch', 'automation.goals': 'allowed', 'automation.runners': 'runners',
|
|
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',
|
|
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
|
+
}
|
|
101
141
|
// An objection's reason as one tap: the five sections the server accepts
|
|
102
142
|
// (street-card.js SECTIONS), each in at most 16 characters.
|
|
103
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' });
|
|
@@ -161,7 +201,16 @@
|
|
|
161
201
|
if (when) add('closes ' + when);
|
|
162
202
|
if (m.where) add(str(m.where).toLowerCase());
|
|
163
203
|
add(STATE[stateOf(m)]);
|
|
204
|
+
if (Array.isArray(m.acts) && m.acts.length && C && C.actsCard) {
|
|
205
|
+
const ac = C.actsCard({ key: m.id, kind: m.source, id: m.ref && m.ref.id != null ? m.ref.id : str(m.id).replace(/^[^:]*:/, ''), who: person || name, title: m.title, building: m.building, href: m.href });
|
|
206
|
+
return Object.assign(actedRow(m.acts, ac, ctx), {
|
|
207
|
+
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
208
|
+
needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
164
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;
|
|
165
214
|
const actions = [];
|
|
166
215
|
let chips = null;
|
|
167
216
|
if (cv && !cv.readOnly && !cv.loading) {
|
|
@@ -176,13 +225,90 @@
|
|
|
176
225
|
const wait = actions.length ? '' : cv && cv.loading ? 'reading…' : stateOf(m) === 'needs' ? 'not from here' : 'moving';
|
|
177
226
|
return {
|
|
178
227
|
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
179
|
-
actions: actions, chips: chips, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
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,
|
|
180
231
|
};
|
|
181
232
|
}
|
|
233
|
+
|
|
234
|
+
// The buttons and chips of a row whose acts the server handed it (task
|
|
235
|
+
// 1004559). An act with options asks first, with its options as one-tap chips;
|
|
236
|
+
// `pick: 'open_goals'` offers the open goals the street already reads; an act
|
|
237
|
+
// whose chips come out empty is left off rather than drawn dead. 'record'
|
|
238
|
+
// goes to the matter's own page (a sign-off that needs a recording).
|
|
239
|
+
function actedRow(acts, card, ctx) {
|
|
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;
|
|
243
|
+
const actions = [];
|
|
244
|
+
const choices = {};
|
|
245
|
+
acts.forEach((a) => {
|
|
246
|
+
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
247
|
+
const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
|
|
248
|
+
if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
|
|
249
|
+
let options = (a.options || []).map((o) => ({ key: str(o.key), label: fit(o.label, LIMITS.chip), input: { option: str(o.key), label: str(o.label) } }));
|
|
250
|
+
if (a.pick === 'open_goals') {
|
|
251
|
+
const except = new Set((a.except || []).map(str));
|
|
252
|
+
options = (ctx.goals || []).filter((g) => g && !except.has(str(g.id))).slice(0, LIMITS.chips)
|
|
253
|
+
.map((g) => ({ key: str(g.id), label: fit(titles.get(str(g.id)), LIMITS.chip), input: { option: str(g.id), label: titles.get(str(g.id)) } }));
|
|
254
|
+
if (!options.length) return;
|
|
255
|
+
}
|
|
256
|
+
options = options.slice(0, LIMITS.chips);
|
|
257
|
+
const goes = a.key === 'record' || a.key === 'open';
|
|
258
|
+
if (goes && !card.href) return;
|
|
259
|
+
const field = a.field || null;
|
|
260
|
+
const ask = options.length > 0 || !!field;
|
|
261
|
+
if (ask) choices[a.key] = field ? { for: a.key, ask: ASKS[word] || 'which', field: field, options: [] } : { for: a.key, ask: ASKS[word] || 'which', options: options };
|
|
262
|
+
actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
|
|
263
|
+
});
|
|
264
|
+
const first = actions.find((a) => a.ask);
|
|
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 };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// A desk row (task 1004559): something on this floor the viewer may act on that
|
|
271
|
+
// no one is waiting on, read by the module that owns its route.
|
|
272
|
+
function deskRowOf(d, ctx) {
|
|
273
|
+
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || '')]));
|
|
274
|
+
const hit = /^goal (\d+)$/.exec(str(d.name));
|
|
275
|
+
const full = hit && titles.get(hit[1]) ? titles.get(hit[1]) : str(d.name);
|
|
276
|
+
const name = fit(full, LIMITS.name, LIMITS.nameWords);
|
|
277
|
+
const age = ageWord(d.opened_at, ctx.now || 0);
|
|
278
|
+
const facts = [];
|
|
279
|
+
const add = (t) => { const v = fit(t, LIMITS.factLen); if (v && facts.indexOf(v) < 0 && facts.length < LIMITS.facts) facts.push(v); };
|
|
280
|
+
if (full !== name) add(full);
|
|
281
|
+
(d.facts || []).forEach(add);
|
|
282
|
+
if (age) add('since ' + age);
|
|
283
|
+
const ref = d.ref || {};
|
|
284
|
+
const card = C && C.actsCard ? C.actsCard({ key: d.id, kind: ref.type, id: ref.id, who: full, title: full, building: d.building, href: d.href }) : null;
|
|
285
|
+
return Object.assign(actedRow(d.acts || [], card || { href: null }, ctx), {
|
|
286
|
+
key: str(d.id), matter: str(d.id), ini: initials(name), name: name, fact: fit(d.fact, LIMITS.fact), facts: facts,
|
|
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,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// The desk rows of one floor: those of the desk its PLAN entry names.
|
|
293
|
+
function deskRows(key, floor, docket, ctx) {
|
|
294
|
+
const plan = I && I.PLAN[key];
|
|
295
|
+
const spec = plan && plan.floors[floor];
|
|
296
|
+
const bind = (spec && spec[2]) || {};
|
|
297
|
+
if (!bind.desk || !docket || !Array.isArray(docket.desks)) return [];
|
|
298
|
+
// a floor may hold more than one desk (Rules in force: the settings and the modules)
|
|
299
|
+
const keys = [].concat(bind.desk);
|
|
300
|
+
return docket.desks.filter((x) => x && keys.indexOf(x.key) >= 0 && x.status === 'live' && Array.isArray(x.rows))
|
|
301
|
+
.reduce((all, x) => all.concat(x.rows.filter((r) => r && r.building === key)), []);
|
|
302
|
+
}
|
|
182
303
|
function rowsFor(key, floor, docket, ctx) {
|
|
183
304
|
const c = Object.assign({ docket: docket }, ctx || {});
|
|
184
305
|
const done = c.done || {};
|
|
185
|
-
|
|
306
|
+
const desked = deskRows(key, floor, docket).filter((d) => !done[str(d.id)]).map((d) => deskRowOf(d, c)).filter((r) => r.actions.length);
|
|
307
|
+
// a desk row that stands in for a source (the core update's check-then-take)
|
|
308
|
+
// replaces that source's matter rows on this floor
|
|
309
|
+
const over = desked.map((r) => r.over).filter(Boolean);
|
|
310
|
+
return floorMatters(key, floor, docket).filter((m) => !done[str(m.id)] && over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
|
|
311
|
+
.concat(desked)
|
|
186
312
|
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
187
313
|
}
|
|
188
314
|
|
|
@@ -199,19 +325,37 @@
|
|
|
199
325
|
const floor = Math.max(0, Math.min(Number(o.floor) || 0, plan.floors.length - 1));
|
|
200
326
|
const spec = plan.floors[floor];
|
|
201
327
|
const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
|
|
202
|
-
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done };
|
|
203
|
-
|
|
328
|
+
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done, goals: o.goals || [] };
|
|
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
|
+
}
|
|
204
342
|
const bind = spec[2] || {};
|
|
205
343
|
const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
|
|
206
344
|
const needs = rows.filter((r) => r.needs);
|
|
207
345
|
const noun = NOUNS[src] || 'open';
|
|
346
|
+
// rows nobody waits on: 'moving' for matters, the desk's own noun for desk rows
|
|
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';
|
|
350
|
+
const counted = rows.filter((r) => !r.tool);
|
|
208
351
|
const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
|
|
209
352
|
const cleared = !!o.cleared && rows.length === 0;
|
|
210
353
|
const quiet = rows.length === 0;
|
|
211
354
|
let status, hot = false;
|
|
212
355
|
if (!docket) status = 'the docket could not be read';
|
|
213
356
|
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
214
|
-
else if (
|
|
357
|
+
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
|
|
358
|
+
else if (rows.length) status = fl.line;
|
|
215
359
|
else if (cleared) status = 'floor clear';
|
|
216
360
|
else status = 'nothing needs you · ' + fl.line;
|
|
217
361
|
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
@@ -224,7 +368,13 @@
|
|
|
224
368
|
const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
|
|
225
369
|
tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
|
|
226
370
|
};
|
|
227
|
-
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); }
|
|
228
378
|
else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
|
|
229
379
|
tile(history.length, 'on record');
|
|
230
380
|
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
|
|
@@ -234,13 +384,50 @@
|
|
|
234
384
|
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
235
385
|
status: fit(status, LIMITS.status), hot: hot,
|
|
236
386
|
tiles: tiles.slice(0, LIMITS.tiles), rows: rows,
|
|
237
|
-
quiet: quiet, cleared: cleared,
|
|
387
|
+
quiet: quiet, cleared: cleared, save: save,
|
|
238
388
|
clearLine: fit(cleared ? (CLEAR[src] || 'Floor clear') : 'Nothing needs you here', LIMITS.clearLine),
|
|
239
389
|
clearSub: fit(cleared ? 'lit up · ' + (o.today || 0) + ' decided today' : fl.line, LIMITS.clearSub),
|
|
240
390
|
record: record,
|
|
241
391
|
};
|
|
242
392
|
}
|
|
243
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
|
+
|
|
244
431
|
// ---------- the rule as a checker ----------
|
|
245
432
|
function breaks(desk) {
|
|
246
433
|
const out = [];
|
|
@@ -263,7 +450,14 @@
|
|
|
263
450
|
if (r.facts.length > LIMITS.facts || r.facts.some((f) => !oneLine(f) || f.length > LIMITS.factLen)) bad('row facts', r.facts);
|
|
264
451
|
if (r.chips && (r.chips.options.length > LIMITS.chips || r.chips.options.some((c) => c.label.length > LIMITS.chip))) bad('chips', r.chips.options.map((c) => c.label));
|
|
265
452
|
if (r.actions.some((a) => a.ask) && !r.chips) bad('a reason without chips', r.name);
|
|
453
|
+
Object.keys(r.choices || {}).forEach((k) => {
|
|
454
|
+
const c = r.choices[k];
|
|
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));
|
|
456
|
+
});
|
|
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);
|
|
266
459
|
});
|
|
460
|
+
if (desk.save && (!oneLine(desk.save.line) || desk.save.line.length > LIMITS.saveLine)) bad('save line', desk.save.line);
|
|
267
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));
|
|
268
462
|
return out;
|
|
269
463
|
}
|
|
@@ -277,8 +471,8 @@
|
|
|
277
471
|
}
|
|
278
472
|
|
|
279
473
|
const api = {
|
|
280
|
-
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
281
|
-
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
|
|
474
|
+
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, ASKS: ASKS, DESK_NOUNS: DESK_NOUNS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
475
|
+
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips, moved: moved,
|
|
282
476
|
};
|
|
283
477
|
root.GovDesk = api;
|
|
284
478
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -112,6 +112,8 @@
|
|
|
112
112
|
const PLAN_SYMBOLS = {
|
|
113
113
|
'Rules in force': 'gavel', 'New rules': 'wrench', 'Security reports': 'shield', 'Removals': 'removal', 'Grade appeals': 'scales',
|
|
114
114
|
'Goals': 'flag', 'Promises': 'seal', 'Spending matters': 'coins',
|
|
115
|
+
// the Desks board's name for the blockers floor (task 1004560)
|
|
116
|
+
'Waiting on you': 'hourglass',
|
|
115
117
|
};
|
|
116
118
|
function floorSymbol(name) {
|
|
117
119
|
return PROTO_SYMBOLS[name] || PLAN_SYMBOLS[name] || 'crate';
|
|
@@ -331,42 +333,43 @@
|
|
|
331
333
|
rules: {
|
|
332
334
|
holds: 'Standing rules. Each one settles a kind of matter so the council never votes on it again.', rule: 'majority', cap: 8,
|
|
333
335
|
growth: 'When the hall fills, the council can open an annex for one area, such as money rules.', annex: 'room for an annex, such as money rules',
|
|
334
|
-
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules' }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
336
|
+
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules', desk: ['rules.settings', 'rules.modules'] }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
335
337
|
},
|
|
336
338
|
disputes: {
|
|
337
339
|
holds: 'Grade appeals, removal requests and security reports. Every ruling becomes precedent for the next one.', rule: 'decide', cap: 5,
|
|
338
340
|
growth: 'Busy weeks go to a panel of three once a second room is built.', annex: 'room for a second room, a panel of three',
|
|
339
|
-
floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
|
|
341
|
+
floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports', desk: 'disputes.confirmed' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
|
|
340
342
|
},
|
|
341
343
|
people: {
|
|
342
344
|
holds: 'Who belongs, the rank each person holds, and who sits in which seat.', rule: 'consent', cap: 8,
|
|
343
345
|
growth: 'A new floor opens whenever the charter creates a new seat.', annex: 'room for an annex of seats',
|
|
344
|
-
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
346
|
+
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people', desk: 'people.builders' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests', desk: 'people.invite' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks', desk: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
345
347
|
},
|
|
346
348
|
planning: {
|
|
347
349
|
holds: 'Goals, and the promises that say when each one is done. Kept promises are what the project owes its users.', rule: 'majority', cap: 9,
|
|
348
350
|
growth: 'When it fills, the next new goal opens a second planning office.', annex: 'room for a second planning office',
|
|
349
|
-
|
|
351
|
+
// the three desks first, as on the Desks board (task 1004560)
|
|
352
|
+
floors: [['Priorities', 'the order goals are worked in', { floor: 'planning.priorities', desk: 'planning.order' }], ['Ideas to ratify', 'ideas waiting to be ratified', { floor: 'planning.ideas', desk: 'planning.ratify' }], ['Waiting on you', 'blockers holding work', { source: 'blockers' }], ['Goals', 'the open goals', { floor: 'planning.goals', goals: true }], ['Promises', 'what the goals say they deliver', { reading: 'promises' }], ['Versions', 'what each version holds', { floor: 'planning.versions' }]],
|
|
350
353
|
},
|
|
351
354
|
automation: {
|
|
352
355
|
holds: 'AI builders working on their own. You choose which goals they may build and how much they may spend, and you can stop them at any time.', rule: 'decide', cap: 6,
|
|
353
356
|
growth: 'A new floor opens for each goal you let it build.', annex: 'room for an annex',
|
|
354
|
-
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals' }], ['Running now', 'builds in progress', { floor: 'automation.running' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
357
|
+
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence', desk: 'automation.switch' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals', desk: 'automation.goals' }], ['Running now', 'builds in progress', { floor: 'automation.running', desk: 'automation.runners' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
355
358
|
},
|
|
356
359
|
review: {
|
|
357
360
|
holds: 'Testing finished work and signing it off. Work does not count until someone who did not build it says it works.', rule: 'signoff', cap: 5,
|
|
358
361
|
growth: 'When one queue of testers is not enough, an annex can take design reviews.', vacantGrowth: 'Nothing is built here until the charter says who holds the seat.', annex: 'room for an annex, such as design review',
|
|
359
|
-
floors: [['Testers', 'who signs off finished work', { floor: 'review.testers' }], ['Waiting outside', 'finished work waiting for a tester', { source: 'criteria_uat', held: 'Being tested' }], ['Signed off', 'work a tester has signed off', { floor: 'review.signed_off' }]],
|
|
362
|
+
floors: [['Testers', 'who signs off finished work', { floor: 'review.testers' }], ['Waiting outside', 'finished work waiting for a tester', { source: 'criteria_uat', held: 'Being tested' }], ['Signed off', 'work a tester has signed off', { floor: 'review.signed_off', desk: 'review.signed' }]],
|
|
360
363
|
},
|
|
361
364
|
release: {
|
|
362
365
|
holds: 'Putting finished work live, and taking updates to Bongos itself.', rule: 'decide', cap: 7,
|
|
363
366
|
growth: 'Room for a second gate if releases are ever split by area.', annex: 'room for a second gate',
|
|
364
|
-
floors: [['Overrides', 'past a failed check or grade', { source: 'override_requests' }], ['Modules', 'listed modules, and their re-checks', { source: 'module_rechecks' }], ['Held back', 'work held from going live', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
367
|
+
floors: [['Overrides', 'past a failed check or grade', { source: 'override_requests' }], ['Modules', 'listed modules, and their re-checks', { source: 'module_rechecks' }], ['Held back', 'work held from going live', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update', desk: 'release.core' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
365
368
|
},
|
|
366
369
|
treasury: {
|
|
367
370
|
holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
|
|
368
371
|
growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
|
|
369
|
-
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
372
|
+
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money', desk: 'treasury.budgets' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
370
373
|
},
|
|
371
374
|
});
|
|
372
375
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|