@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
|
@@ -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';
|
|
@@ -346,7 +348,8 @@
|
|
|
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,
|
|
@@ -356,7 +359,7 @@
|
|
|
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,
|
|
@@ -366,7 +369,7 @@
|
|
|
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', desk: 'treasury.backup' }], ['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'];
|
|
@@ -314,6 +314,18 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
314
314
|
.desk-chip:hover { filter: brightness(1.08); }
|
|
315
315
|
.desk-chip[disabled] { opacity: 0.55; cursor: progress; }
|
|
316
316
|
.desk-wait { font-size: 13px; color: var(--ink-faint); border: 1px dashed var(--street-rule-strong); border-radius: 999px; padding: 6px 12px; white-space: nowrap; }
|
|
317
|
+
/* the Planning desks (task 1004560): a goal's place in the order, its promise
|
|
318
|
+
pips (or a vote's seats), the two arrows, and the save bar under a moved order */
|
|
319
|
+
.desk-pos { width: 22px; flex: none; font-size: 13px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
|
|
320
|
+
.desk-pips { display: flex; gap: 4px; flex: none; }
|
|
321
|
+
.desk-pip { width: 12px; height: 12px; border-radius: 3px; box-sizing: border-box; border: 1px solid var(--street-rule-strong); }
|
|
322
|
+
.desk-pip--on { background: var(--ink); border-color: var(--ink); }
|
|
323
|
+
.desk-arrow { width: 34px; height: 34px; flex: none; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
324
|
+
.desk-arrow svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
325
|
+
.desk-arrow[disabled] { opacity: 0.55; cursor: progress; }
|
|
326
|
+
.desk-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
327
|
+
.desk-save { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; padding: 16px 0 0; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
|
|
328
|
+
.desk-save__line { font-size: 13.5px; color: var(--ink-soft); margin-right: auto; }
|
|
317
329
|
.desk-facts, .desk-choices { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 10px 0 2px 52px; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
|
|
318
330
|
.desk-fact { font-size: 13px; color: var(--ink-soft); background: var(--street-wash); border-radius: 6px; padding: 6px 10px; white-space: nowrap; }
|
|
319
331
|
.desk-choices__ask { font-size: 12.5px; color: var(--ink-faint); margin-right: 4px; }
|
|
@@ -373,6 +385,9 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
373
385
|
.inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
|
|
374
386
|
.desk-row__main { flex-wrap: wrap; row-gap: 8px; }
|
|
375
387
|
.desk-name { flex-basis: calc(100% - 52px); }
|
|
388
|
+
.desk-pos ~ .desk-name { flex-basis: calc(100% - 88px); }
|
|
389
|
+
.desk-pos ~ .desk-pips { margin-left: 88px; }
|
|
390
|
+
.desk-pips ~ .desk-acts { margin-left: auto; }
|
|
376
391
|
.desk-acts { margin-left: 52px; }
|
|
377
392
|
.desk-facts, .desk-choices { padding-left: 0; }
|
|
378
393
|
.desk-tile { min-width: 0; padding: 10px 14px; }
|
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
|
|
83
83
|
// the desk's own state: rows you acted on this visit, what you did per floor,
|
|
84
84
|
// the floors you cleared, the row whose facts or chips are open
|
|
85
|
-
|
|
85
|
+
// order: the Priorities desk's order as the viewer has moved it, per floor (task 1004560)
|
|
86
|
+
var desk = { done: {}, mine: {}, cleared: {}, order: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
|
|
86
87
|
var TODAY_KEY = 'gov-desk-today';
|
|
87
88
|
var today = { day: '', n: 0 };
|
|
88
89
|
|
|
@@ -553,7 +554,7 @@
|
|
|
553
554
|
var D = window.GovDesk, st = view.inside;
|
|
554
555
|
if (!D || !st) return;
|
|
555
556
|
var k = st.key + ':' + bv.selFloor;
|
|
556
|
-
var dm = D.deskModel({ key: st.key, floor: bv.selFloor, docket: data.docket, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), room: data.room, constitution: data.constitution, done: desk.done, mine: desk.mine[k] || [], cleared: !!desk.cleared[k], today: today.n, sym: bv.sym });
|
|
557
|
+
var dm = D.deskModel({ key: st.key, floor: bv.selFloor, docket: data.docket, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), room: data.room, constitution: data.constitution, done: desk.done, mine: desk.mine[k] || [], cleared: !!desk.cleared[k], today: today.n, sym: bv.sym, order: desk.order[k] || null });
|
|
557
558
|
desk.model = dm;
|
|
558
559
|
el.className = 'inside-open desk' + (dm.quiet ? ' desk--quiet' : '');
|
|
559
560
|
el.setAttribute('data-desk', dm.key);
|
|
@@ -595,6 +596,14 @@
|
|
|
595
596
|
dm.rows.forEach(function (r) { list.appendChild(deskRow(r)); });
|
|
596
597
|
el.appendChild(list);
|
|
597
598
|
}
|
|
599
|
+
// a changed order: what saving does, then undo and save (task 1004560)
|
|
600
|
+
if (dm.save) {
|
|
601
|
+
var bar = node('div', 'desk-save');
|
|
602
|
+
bar.appendChild(node('span', 'desk-save__line', dm.save.line));
|
|
603
|
+
bar.appendChild(chipBtn('desk-chip--quiet', 'undo', function () { desk.order[k] = null; draw(); }));
|
|
604
|
+
bar.appendChild(chipBtn('desk-chip--primary', dm.save.label, function () { saveOrder(dm, k); }));
|
|
605
|
+
el.appendChild(bar);
|
|
606
|
+
}
|
|
598
607
|
// the record line: what you just did here, then what this building closed
|
|
599
608
|
var rec = node('div', 'desk-record');
|
|
600
609
|
rec.appendChild(node('span', 'desk-record__label', 'record'));
|
|
@@ -606,6 +615,7 @@
|
|
|
606
615
|
var row = node('div', 'desk-row' + (r.needs ? ' desk-row--needs' : ''));
|
|
607
616
|
row.setAttribute('data-row', r.key);
|
|
608
617
|
var main = node('div', 'desk-row__main');
|
|
618
|
+
if (r.pos) main.appendChild(node('span', 'desk-pos', String(r.pos)));
|
|
609
619
|
main.appendChild(node('span', 'desk-ini', r.ini));
|
|
610
620
|
var name = node('button', 'desk-name');
|
|
611
621
|
name.setAttribute('type', 'button');
|
|
@@ -615,10 +625,33 @@
|
|
|
615
625
|
name.appendChild(node('span', 'desk-name__f', r.fact));
|
|
616
626
|
name.addEventListener('click', function () { desk.open = desk.open === r.key ? null : r.key; desk.ask = null; draw(); focusRow(r.key, 'name'); });
|
|
617
627
|
main.appendChild(name);
|
|
628
|
+
// promises kept of total, or a vote's seats: one pip each
|
|
629
|
+
if (r.pips && r.pips.length) {
|
|
630
|
+
var pips = node('span', 'desk-pips');
|
|
631
|
+
pips.setAttribute('aria-hidden', 'true');
|
|
632
|
+
r.pips.forEach(function (p) { pips.appendChild(node('span', 'desk-pip' + (p.on ? ' desk-pip--on' : ''))); });
|
|
633
|
+
main.appendChild(pips);
|
|
634
|
+
}
|
|
618
635
|
var acts = node('span', 'desk-acts');
|
|
619
636
|
if (r.wait) acts.appendChild(node('span', 'desk-wait', r.wait));
|
|
637
|
+
if (r.order) {
|
|
638
|
+
// the order's two arrows: the move is the viewer's until saved
|
|
639
|
+
[['up', -1, 'M6 15l6-6 6 6'], ['down', 1, 'M6 9l6 6 6-6']].forEach(function (m) {
|
|
640
|
+
if (!r.actions.some(function (a) { return a.key === m[0]; })) return;
|
|
641
|
+
var b = node('button', 'desk-arrow');
|
|
642
|
+
b.setAttribute('type', 'button');
|
|
643
|
+
b.setAttribute('aria-label', 'move ' + r.name + ' ' + m[0]);
|
|
644
|
+
// the top goal cannot go up, nor the last down
|
|
645
|
+
if (desk.busy || (m[1] < 0 && r.top) || (m[1] > 0 && r.bottom)) b.setAttribute('disabled', '');
|
|
646
|
+
b.appendChild(icon(m[2], null, 14));
|
|
647
|
+
b.addEventListener('click', function () { moveRow(r, m[1]); });
|
|
648
|
+
acts.appendChild(b);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
620
651
|
// the primary button sits rightmost, as on the board
|
|
621
|
-
|
|
652
|
+
// a row whose chips stand open is decided by its chip alone: no button for it
|
|
653
|
+
if (!r.order) r.actions.slice().reverse().forEach(function (a) {
|
|
654
|
+
if (r.show && a.key === r.show) return;
|
|
622
655
|
acts.appendChild(chipBtn('desk-chip--' + a.tone, a.label, function () {
|
|
623
656
|
if (a.go) { window.location.href = a.go; return; }
|
|
624
657
|
if (a.ask) { var at = r.key + '|' + a.key; desk.ask = desk.ask === at ? null : at; desk.open = null; draw(); focusRow(r.key, 'chip'); return; }
|
|
@@ -633,6 +666,8 @@
|
|
|
633
666
|
row.appendChild(facts);
|
|
634
667
|
}
|
|
635
668
|
var asked = desk.ask && desk.ask.indexOf(r.key + '|') === 0 ? (r.choices || {})[desk.ask.slice(r.key.length + 1)] : null;
|
|
669
|
+
// a blocker's options stand open: deciding it is picking one
|
|
670
|
+
if (!asked && r.show) asked = (r.choices || {})[r.show] || null;
|
|
636
671
|
if (asked) {
|
|
637
672
|
var chips = node('div', 'desk-choices');
|
|
638
673
|
chips.appendChild(node('span', 'desk-choices__ask', asked.ask));
|
|
@@ -653,6 +688,35 @@
|
|
|
653
688
|
}
|
|
654
689
|
return row;
|
|
655
690
|
}
|
|
691
|
+
// The Priorities desk (task 1004560): a move is kept in this visit until saved.
|
|
692
|
+
function moveRow(r, by) {
|
|
693
|
+
var D = window.GovDesk, st = view.inside;
|
|
694
|
+
if (!D || !st || !desk.model) return;
|
|
695
|
+
var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
|
|
696
|
+
var now = desk.model.rows.filter(function (x) { return x.order; }).map(function (x) { return x.key; });
|
|
697
|
+
desk.order[k] = D.moved(now, r.key, by);
|
|
698
|
+
draw();
|
|
699
|
+
focusRow(r.key, 'arrow');
|
|
700
|
+
}
|
|
701
|
+
// Save: the order goes to POST /goals/reorder, or is filed for the council;
|
|
702
|
+
// the server's own checks decide either way.
|
|
703
|
+
function saveOrder(dm, k) {
|
|
704
|
+
var C = window.GovMatterCard;
|
|
705
|
+
var req = C && C.orderRequest ? C.orderRequest(dm.save) : null;
|
|
706
|
+
if (!req || desk.busy) return Promise.resolve(false);
|
|
707
|
+
desk.busy = true;
|
|
708
|
+
draw();
|
|
709
|
+
return send(req).then(function (out) {
|
|
710
|
+
desk.busy = false;
|
|
711
|
+
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
712
|
+
desk.order[k] = null;
|
|
713
|
+
desk.mine[k] = [dm.save.council ? 'order sent to council' : 'order set'].concat(desk.mine[k] || []);
|
|
714
|
+
bumpToday();
|
|
715
|
+
say(dm.save.said);
|
|
716
|
+
draw();
|
|
717
|
+
return load().then(function () { return true; });
|
|
718
|
+
});
|
|
719
|
+
}
|
|
656
720
|
function deskRowsEl() {
|
|
657
721
|
var st = view.insideModel;
|
|
658
722
|
var on = $(st && st.flipA ? 'insideOpenA' : 'insideOpenB');
|
|
@@ -667,6 +731,7 @@
|
|
|
667
731
|
if (rows[i].getAttribute('data-row') !== key) continue;
|
|
668
732
|
var main = rows[i].children[0];
|
|
669
733
|
var target = what === 'chip' && rows[i].children[1] ? rows[i].children[1].children[1] : main && main.children[1];
|
|
734
|
+
if (what === 'arrow' && main) target = main.querySelector ? main.querySelector('.desk-arrow') : target;
|
|
670
735
|
if (target && target.focus) target.focus();
|
|
671
736
|
return;
|
|
672
737
|
}
|
|
@@ -685,6 +750,8 @@
|
|
|
685
750
|
switch_on: 'switched on', switch_off: 'switched off', allow: 'allowed', disallow: 'disallowed', first: 'put first',
|
|
686
751
|
pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
|
|
687
752
|
check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
|
|
753
|
+
decide: 'decided', ratify: 'ratified', ratify_goal: 'made a goal of',
|
|
754
|
+
budget: 'set the limit of', unsign: 'undid',
|
|
688
755
|
};
|
|
689
756
|
// One act from a row: checked, sent through the card's own request (the
|
|
690
757
|
// server's permission checks decide), then the row clears, the floor counts
|
|
@@ -699,7 +766,9 @@
|
|
|
699
766
|
var req = C.request(r.card, key, input);
|
|
700
767
|
var settle = function (body) {
|
|
701
768
|
var a = C.answer(r.card, key, body);
|
|
702
|
-
|
|
769
|
+
// an act at a building a sitting decides went to it as a matter (task 1004561)
|
|
770
|
+
var sent = r.card.subject && r.card.subject.council && req && /\/government\/board\/matters$/.test(req.url);
|
|
771
|
+
desk.mine[k] = [(sent ? 'sent to council:' : DONE_WORDS[key]) + ' ' + r.name].concat(desk.mine[k] || []);
|
|
703
772
|
desk.done[r.matter] = true;
|
|
704
773
|
desk.open = null; desk.ask = null;
|
|
705
774
|
if (desk.model && !desk.model.rows.some(function (x) { return x.matter !== r.matter; })) desk.cleared[k] = true;
|
|
@@ -681,6 +681,130 @@
|
|
|
681
681
|
".desk-rows"
|
|
682
682
|
]
|
|
683
683
|
}
|
|
684
|
+
},
|
|
685
|
+
"desk-planning-priorities": {
|
|
686
|
+
"auth": true,
|
|
687
|
+
"_": "Task 1004560: Planning, Priorities. The open goals in order with promise pips and two arrows; one goal moved down, so the save bar says the new order goes to the council and how many yes it needs, with undo.",
|
|
688
|
+
"url": "/builders/street#building=planning",
|
|
689
|
+
"actions": [
|
|
690
|
+
[
|
|
691
|
+
"wait",
|
|
692
|
+
900
|
|
693
|
+
],
|
|
694
|
+
[
|
|
695
|
+
"clickIfVisible",
|
|
696
|
+
"[data-craft-close]"
|
|
697
|
+
],
|
|
698
|
+
[
|
|
699
|
+
"wait",
|
|
700
|
+
300
|
|
701
|
+
],
|
|
702
|
+
[
|
|
703
|
+
"click",
|
|
704
|
+
"[data-floor=\"0\"]"
|
|
705
|
+
],
|
|
706
|
+
[
|
|
707
|
+
"wait",
|
|
708
|
+
600
|
|
709
|
+
],
|
|
710
|
+
[
|
|
711
|
+
"click",
|
|
712
|
+
".desk-arrow[aria-label$=\" down\"]"
|
|
713
|
+
],
|
|
714
|
+
[
|
|
715
|
+
"wait",
|
|
716
|
+
400
|
|
717
|
+
]
|
|
718
|
+
],
|
|
719
|
+
"expect": {
|
|
720
|
+
"visible": [
|
|
721
|
+
".desk-rows",
|
|
722
|
+
".desk-pips",
|
|
723
|
+
".desk-save"
|
|
724
|
+
]
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
"desk-planning-ideas": {
|
|
728
|
+
"auth": true,
|
|
729
|
+
"_": "Task 1004560: Planning, Ideas to ratify. A Full Idea in its vote with seat pips, a promise to ratify in one tap, and a carried Full Idea made a goal from a version chip.",
|
|
730
|
+
"url": "/builders/street#building=planning",
|
|
731
|
+
"actions": [
|
|
732
|
+
[
|
|
733
|
+
"wait",
|
|
734
|
+
900
|
|
735
|
+
],
|
|
736
|
+
[
|
|
737
|
+
"clickIfVisible",
|
|
738
|
+
"[data-craft-close]"
|
|
739
|
+
],
|
|
740
|
+
[
|
|
741
|
+
"wait",
|
|
742
|
+
300
|
|
743
|
+
],
|
|
744
|
+
[
|
|
745
|
+
"click",
|
|
746
|
+
"[data-floor=\"1\"]"
|
|
747
|
+
],
|
|
748
|
+
[
|
|
749
|
+
"wait",
|
|
750
|
+
600
|
|
751
|
+
],
|
|
752
|
+
[
|
|
753
|
+
"focus",
|
|
754
|
+
".desk-rows .desk-row:last-child .desk-name"
|
|
755
|
+
],
|
|
756
|
+
[
|
|
757
|
+
"wait",
|
|
758
|
+
300
|
|
759
|
+
]
|
|
760
|
+
],
|
|
761
|
+
"expect": {
|
|
762
|
+
"visible": [
|
|
763
|
+
".desk-rows"
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
"desk-planning-blockers": {
|
|
768
|
+
"auth": true,
|
|
769
|
+
"_": "Task 1004560: Planning, Waiting on you. Each blocker decided by one of its options as chips standing open; go ahead, not now, need more when it lists none.",
|
|
770
|
+
"url": "/builders/street#building=planning",
|
|
771
|
+
"actions": [
|
|
772
|
+
[
|
|
773
|
+
"wait",
|
|
774
|
+
900
|
|
775
|
+
],
|
|
776
|
+
[
|
|
777
|
+
"clickIfVisible",
|
|
778
|
+
"[data-craft-close]"
|
|
779
|
+
],
|
|
780
|
+
[
|
|
781
|
+
"wait",
|
|
782
|
+
300
|
|
783
|
+
],
|
|
784
|
+
[
|
|
785
|
+
"click",
|
|
786
|
+
"[data-floor=\"2\"]"
|
|
787
|
+
],
|
|
788
|
+
[
|
|
789
|
+
"wait",
|
|
790
|
+
600
|
|
791
|
+
],
|
|
792
|
+
[
|
|
793
|
+
"focus",
|
|
794
|
+
".desk-rows .desk-row:last-child .desk-name"
|
|
795
|
+
],
|
|
796
|
+
[
|
|
797
|
+
"wait",
|
|
798
|
+
300
|
|
799
|
+
]
|
|
800
|
+
],
|
|
801
|
+
"expect": {
|
|
802
|
+
"visible": [
|
|
803
|
+
".desk-rows",
|
|
804
|
+
".desk-choices",
|
|
805
|
+
".desk-status--hot"
|
|
806
|
+
]
|
|
807
|
+
}
|
|
684
808
|
}
|
|
685
809
|
},
|
|
686
810
|
"reducedMotion": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
const api = require('../../src/module-api');
|
|
21
21
|
const blockers = require('./blockers');
|
|
22
|
+
const ratify = require('./ratify');
|
|
22
23
|
|
|
23
24
|
const POINT = 'governor.docket';
|
|
24
25
|
const DOCKET_BLOCKER_LIMIT = 50;
|
|
@@ -41,9 +42,96 @@ function blockerDocketMatter(b, mayResolve) {
|
|
|
41
42
|
tally: null,
|
|
42
43
|
href: `/blockers#blocker-${b.id}`,
|
|
43
44
|
ref: { type: 'blocker', id: b.id },
|
|
45
|
+
// the floor desk's one act (task 1004560): decide, by one of its options
|
|
46
|
+
...(mayResolve ? { acts: [{ key: 'decide', options: blockerOptions(b.body_md) }] } : {}),
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
// A blocker's options as one-tap chips (task 1004560). Its own, when its body
|
|
51
|
+
// lists them as "Option A: …" lines (two to five of them); otherwise the three
|
|
52
|
+
// answers every blocker takes. The chosen key becomes the resolution note, so
|
|
53
|
+
// a key is the option's own words (at most 64 characters) and the label its
|
|
54
|
+
// first words. Pure.
|
|
55
|
+
const DEFAULT_CHOICES = Object.freeze(['go ahead', 'not now', 'need more']);
|
|
56
|
+
const OPTION_LINE = /^\s*(?:[-*]|\d+[.)])?\s*(?:\*\*)?option\s+[A-Za-z0-9]{1,2}(?:\*\*)?\s*[:.)—-]+\s*(?:\*\*)?(.+)$/i;
|
|
57
|
+
function plainWords(s) { return String(s).replace(/[*_`[\]]+/g, '').replace(/\([^)]*\)/g, '').replace(/\s+/g, ' ').trim(); }
|
|
58
|
+
function shortLabel(s) {
|
|
59
|
+
let out = '';
|
|
60
|
+
for (const w of s.split(' ')) { const next = out ? out + ' ' + w : w; if (next.length > 16) break; out = next; }
|
|
61
|
+
return out || s.slice(0, 16);
|
|
62
|
+
}
|
|
63
|
+
function blockerOptions(bodyMd) {
|
|
64
|
+
const found = [];
|
|
65
|
+
for (const line of String(bodyMd || '').split(/\r?\n/)) {
|
|
66
|
+
const m = OPTION_LINE.exec(line);
|
|
67
|
+
if (!m) continue;
|
|
68
|
+
const text = plainWords(m[1]).split(/\s[—–-]\s|[.;:]\s/)[0].trim();
|
|
69
|
+
if (text && !found.some((o) => o.key === text.slice(0, 64))) found.push({ key: text.slice(0, 64), label: shortLabel(text) });
|
|
70
|
+
}
|
|
71
|
+
if (found.length >= 2 && found.length <= 5) return found;
|
|
72
|
+
return DEFAULT_CHOICES.map((t) => ({ key: t, label: t }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The Ideas-to-ratify desk (task 1004560): the ideas a ratifier may take from
|
|
76
|
+
// the floor, under idea.ratify (both ratify routes' atom). A criterion proposal
|
|
77
|
+
// ratifies in one tap (POST /inbox/:id/ratify). A Full Idea the board has
|
|
78
|
+
// carried becomes a goal (POST /inbox/:id/ratify-goal) with its target version
|
|
79
|
+
// as a chip — only when its answers compose a goal on their own; one that needs
|
|
80
|
+
// the ratifier to write its criterion opens its page instead. A Full Idea still
|
|
81
|
+
// in its sitting is not here: it is the board's vote row (ADR 0175 §8). Pure.
|
|
82
|
+
function ratifyDeskRows(ideas, versions, passed) {
|
|
83
|
+
const chips = (versions || []).slice(0, 5).map((v) => ({ key: String(v.id), label: String(v.id) }));
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const i of ideas || []) {
|
|
86
|
+
if (!i || i.status !== 'open') continue;
|
|
87
|
+
const href = `/idea/${i.id}`;
|
|
88
|
+
if (i.kind === 'criterion-proposal') {
|
|
89
|
+
let p = null;
|
|
90
|
+
try { p = ratify.parseProposalBody(i.body_md); } catch (_) { p = null; }
|
|
91
|
+
if (!p || !p.target_version) continue;
|
|
92
|
+
out.push({
|
|
93
|
+
id: `p${i.id}`, name: String(i.title || 'a promise'), ref: { type: 'criterion_proposal', id: i.id }, href, opened_at: i.created_at,
|
|
94
|
+
fact: `a promise for ${p.target_version}`, facts: [String(p.criterion_md || '').slice(0, 80)],
|
|
95
|
+
acts: [{ key: 'ratify' }, { key: 'open' }],
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (i.grade !== 'full' || !passed.has(String(i.id))) continue;
|
|
100
|
+
const composed = ratify.composeGoalFromFullIdea({ ideaId: i.id, templateKey: i.template_key, fieldValues: i.field_values });
|
|
101
|
+
const fits = composed && composed.ok && chips.length > 0;
|
|
102
|
+
out.push({
|
|
103
|
+
id: `g${i.id}`, name: String(i.title || 'a full idea'), ref: { type: 'full_idea', id: i.id }, href, opened_at: i.created_at,
|
|
104
|
+
fact: 'the board carried it', facts: [fits ? 'becomes a goal' : 'needs its page to finish'],
|
|
105
|
+
acts: fits ? [{ key: 'ratify_goal', options: chips }, { key: 'open' }] : [{ key: 'open' }],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
// The desk reads the oldest 30 open proposals and Full Ideas (older first, so
|
|
111
|
+
// nothing waits forever behind newer ones); which Full Ideas the board carried
|
|
112
|
+
// is one read for all of them (government.boardPassedIdeaIds).
|
|
113
|
+
const RATIFY_IDEAS_LIMIT = 30;
|
|
114
|
+
async function readRatifyDesk() {
|
|
115
|
+
const { rows: ideas } = await api.pool.query(
|
|
116
|
+
`SELECT i.id, i.title, i.kind, i.status, i.body_md, i.created_at,
|
|
117
|
+
COALESCE(g.grade, 'quick') AS grade, g.template_key, g.field_values
|
|
118
|
+
FROM idea_inbox i LEFT JOIN ideas_grades g ON g.idea_id = i.id
|
|
119
|
+
WHERE i.status = 'open' AND (i.kind = 'criterion-proposal' OR g.grade = 'full')
|
|
120
|
+
ORDER BY i.created_at, i.id LIMIT $1`,
|
|
121
|
+
[RATIFY_IDEAS_LIMIT],
|
|
122
|
+
);
|
|
123
|
+
const government = api.resolveOptional && api.resolveOptional('government');
|
|
124
|
+
const passed = new Set();
|
|
125
|
+
const full = ideas.filter((i) => i.grade === 'full').map((i) => i.id);
|
|
126
|
+
if (full.length && government && typeof government.boardPassedIdeaIds === 'function') {
|
|
127
|
+
(await government.boardPassedIdeaIds(full)).forEach((id) => passed.add(String(id)));
|
|
128
|
+
}
|
|
129
|
+
const { rows: versions } = passed.size
|
|
130
|
+
? await api.pool.query(`SELECT id, name FROM versions WHERE status IN ('building', 'planning') ORDER BY (status = 'building') DESC, id LIMIT 5`)
|
|
131
|
+
: { rows: [] };
|
|
132
|
+
return ratifyDeskRows(ideas, versions, passed);
|
|
133
|
+
}
|
|
134
|
+
|
|
47
135
|
// The rules-at-work reading from this month's routed filings. Pure.
|
|
48
136
|
function rulesReading(settledThisMonth) {
|
|
49
137
|
const n = Number(settledThisMonth) || 0;
|
|
@@ -114,6 +202,7 @@ function contributeIdeasToDocket() {
|
|
|
114
202
|
return rows.map((b) => blockerDocketMatter(b, mayResolve));
|
|
115
203
|
},
|
|
116
204
|
});
|
|
205
|
+
api.contribute(POINT, { desk: 'planning.ratify', gate: { permission: 'idea.ratify' }, read: () => readRatifyDesk() });
|
|
117
206
|
// A count, no rows: any builder may read it.
|
|
118
207
|
api.contribute(POINT, {
|
|
119
208
|
reading: 'rules',
|
|
@@ -127,6 +216,8 @@ function contributeIdeasToDocket() {
|
|
|
127
216
|
module.exports = {
|
|
128
217
|
contributeIdeasToDocket,
|
|
129
218
|
blockerDocketMatter,
|
|
219
|
+
blockerOptions,
|
|
220
|
+
ratifyDeskRows,
|
|
130
221
|
rulesReading,
|
|
131
222
|
rulesFloor,
|
|
132
223
|
rulesHistory,
|
|
@@ -181,6 +181,37 @@ function prioritiesFloor(goals, versionId) {
|
|
|
181
181
|
return { count: '', line: 'the order goals are worked in', lines: [[top, versionId ? `version ${versionId}` : ''], [`${plural(mine.length, 'goal is', 'goals are')} in the order.`, '']], hot: false };
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// The Priorities desk (task 1004560): the building version's open goals in their
|
|
185
|
+
// worked order, each with its promises kept of total, for a viewer who may set
|
|
186
|
+
// the order — POST /goals/reorder's own atom (goal.reorder), or, where Planning
|
|
187
|
+
// is put to a sitting, the right to file the matter (board.item.open). Which of
|
|
188
|
+
// the two a save takes is the decision table's call, read by the screen from the
|
|
189
|
+
// docket's own buildings row; the route the save reaches still decides. Pure.
|
|
190
|
+
const ORDER_PIPS = 12;
|
|
191
|
+
function orderDeskRows(goals, criteria, versionId, mayOrder) {
|
|
192
|
+
if (!mayOrder || !versionId) return [];
|
|
193
|
+
const by = new Map();
|
|
194
|
+
for (const c of criteria || []) {
|
|
195
|
+
if (c.goal_id == null) continue;
|
|
196
|
+
const k = String(c.goal_id);
|
|
197
|
+
if (!by.has(k)) by.set(k, { kept: 0, total: 0 });
|
|
198
|
+
const e = by.get(k); e.total += 1; if (c.satisfied === true) e.kept += 1;
|
|
199
|
+
}
|
|
200
|
+
const mine = (goals || []).filter((g) => g && g.status === 'open' && g.version_id === versionId);
|
|
201
|
+
if (mine.length < 2) return []; // one goal has no order to set
|
|
202
|
+
return mine.map((g) => {
|
|
203
|
+
const e = by.get(String(g.id));
|
|
204
|
+
const of = e ? Math.min(e.total, ORDER_PIPS) : 0;
|
|
205
|
+
return {
|
|
206
|
+
id: g.id, name: String(g.title), ref: { type: 'goal', id: g.id }, set: String(versionId),
|
|
207
|
+
fact: e ? `${e.kept} of ${e.total} promises kept` : 'no promises yet',
|
|
208
|
+
facts: [`version ${versionId}`],
|
|
209
|
+
...(of ? { pips: { on: Math.min(e.kept, of), of } } : {}),
|
|
210
|
+
acts: [{ key: 'up' }, { key: 'down' }],
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
// Planning · Versions: what is building, and the last that shipped.
|
|
185
216
|
function versionsFloor(versions, goals) {
|
|
186
217
|
const all = versions || [];
|
|
@@ -210,6 +241,26 @@ function signedOffFloor(criteria, now) {
|
|
|
210
241
|
};
|
|
211
242
|
}
|
|
212
243
|
|
|
244
|
+
// The Review desk's signed-off rows (task 1004561): the newest promises signed
|
|
245
|
+
// off, each with one act, undo, for a viewer holding criterion.unsatisfy (POST
|
|
246
|
+
// /done-when/:id/unsatisfy's own atom). The reason is one tap. Pure.
|
|
247
|
+
const UNDO_REASONS = Object.freeze([
|
|
248
|
+
{ key: 'broke', label: 'it broke again' },
|
|
249
|
+
{ key: 'untested', label: 'not truly tested' },
|
|
250
|
+
{ key: 'error', label: 'signed in error' },
|
|
251
|
+
]);
|
|
252
|
+
const SIGNED_ROWS = 8;
|
|
253
|
+
function signedDeskRows(criteria) {
|
|
254
|
+
return (criteria || []).filter((c) => c && c.satisfied === true && c.checked_at)
|
|
255
|
+
.sort((a, b) => Date.parse(b.checked_at) - Date.parse(a.checked_at))
|
|
256
|
+
.slice(0, SIGNED_ROWS)
|
|
257
|
+
.map((c) => ({
|
|
258
|
+
id: c.id, name: String(c.criterion_md), fact: `signed off ${dayWord(c.checked_at)}`,
|
|
259
|
+
facts: c.goal_title ? [String(c.goal_title)] : [], ref: { type: 'signed_criterion', id: c.id },
|
|
260
|
+
acts: [{ key: 'unsign', options: UNDO_REASONS }],
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
213
264
|
// Release · Live this month: shipped work, newest first.
|
|
214
265
|
function liveMonthFloor(shipped, now) {
|
|
215
266
|
const month = (shipped || []).filter((t) => t.shipped_at && Date.parse(t.shipped_at) >= monthStart(now));
|
|
@@ -284,6 +335,15 @@ function contributeLifecycleFloors() {
|
|
|
284
335
|
floor: 'planning.priorities', gate: any,
|
|
285
336
|
read: async (_ctx, { once } = {}) => prioritiesFloor(await openGoals(once), await db.currentBuildingVersionId()),
|
|
286
337
|
});
|
|
338
|
+
api.contribute(POINT, {
|
|
339
|
+
desk: 'planning.order', gate: any,
|
|
340
|
+
read: async (ctx, { once } = {}) => {
|
|
341
|
+
const may = ctx.has('goal.reorder') || ctx.has('board.item.open');
|
|
342
|
+
if (!may) return [];
|
|
343
|
+
const v = await db.currentBuildingVersionId();
|
|
344
|
+
return orderDeskRows(await openGoals(once), v ? await doneWhen.listCriteria(v) : [], v, may);
|
|
345
|
+
},
|
|
346
|
+
});
|
|
287
347
|
api.contribute(POINT, {
|
|
288
348
|
floor: 'planning.versions', gate: any,
|
|
289
349
|
read: async () => versionsFloor(await db.listVersions(), await db.listGoals({})),
|
|
@@ -295,6 +355,13 @@ function contributeLifecycleFloors() {
|
|
|
295
355
|
return signedOffFloor(v ? await doneWhen.listCriteria(v) : [], now);
|
|
296
356
|
},
|
|
297
357
|
});
|
|
358
|
+
api.contribute(POINT, {
|
|
359
|
+
desk: 'review.signed', gate: { permission: 'criterion.unsatisfy' },
|
|
360
|
+
read: async () => {
|
|
361
|
+
const v = await db.currentBuildingVersionId();
|
|
362
|
+
return signedDeskRows(v ? await doneWhen.listCriteria(v) : []);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
298
365
|
api.contribute(POINT, {
|
|
299
366
|
floor: 'release.live_month', gate: any,
|
|
300
367
|
read: async (_ctx, { now } = {}) => liveMonthFloor(await db.recentShippedTasks(SHIPPED_LIMIT, { days: 31 }), now),
|
|
@@ -398,8 +465,10 @@ module.exports = {
|
|
|
398
465
|
promisesReading,
|
|
399
466
|
goalsFloor,
|
|
400
467
|
prioritiesFloor,
|
|
468
|
+
orderDeskRows,
|
|
401
469
|
versionsFloor,
|
|
402
470
|
signedOffFloor,
|
|
471
|
+
signedDeskRows,
|
|
403
472
|
liveMonthFloor,
|
|
404
473
|
heldBackFloor,
|
|
405
474
|
newcomersFloor,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/matter-acts.js — what a PASSED Review or Release matter
|
|
4
|
+
// carries out (task 1004561, goal 1000125). When a project's decision table puts
|
|
5
|
+
// Review or Release to a sitting, its passed matter acts here, after the close
|
|
6
|
+
// commits, through the SAME function the direct route calls:
|
|
7
|
+
// review satisfy_criterion → satisfyByOverride (POST /done-when/:id/satisfy)
|
|
8
|
+
// release approve_override → db.decideOverrideRequest (POST /override-requests/:id/decide)
|
|
9
|
+
// release approve_gate → approveGatePr (POST /gate-approvals/:pr/approve)
|
|
10
|
+
// The government module only checks the act's shape and calls in through the
|
|
11
|
+
// 'governor.matterAct' contribution point (a module never requires a sibling).
|
|
12
|
+
// Each act returns { done, reason? }: a refusal the route would give (already
|
|
13
|
+
// decided, not open, owner only) is a reason, never a throw.
|
|
14
|
+
|
|
15
|
+
const api = require('../../src/module-api');
|
|
16
|
+
const db = require('./db');
|
|
17
|
+
const doneWhen = require('./done-when');
|
|
18
|
+
|
|
19
|
+
const POINT = 'governor.matterAct';
|
|
20
|
+
const why = (area, ctx) => `A passed ${area} matter (matter ${ctx && ctx.matterId}) decided it.`;
|
|
21
|
+
|
|
22
|
+
// Required lazily: the route files build routers, and only their shared
|
|
23
|
+
// functions are needed here.
|
|
24
|
+
const routes = {
|
|
25
|
+
doneWhen: () => require('./routes/done-when'),
|
|
26
|
+
gate: () => require('./routes/gate-approvals'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ACTS = Object.freeze({
|
|
30
|
+
review: Object.freeze({
|
|
31
|
+
satisfy_criterion: async (action, ctx) => {
|
|
32
|
+
const c = await doneWhen.getCriterion(action.criterion_id);
|
|
33
|
+
if (!c) return { done: false, reason: 'criterion_not_found' };
|
|
34
|
+
if (c.satisfied === true) return { done: false, reason: 'already_satisfied' };
|
|
35
|
+
await routes.doneWhen().satisfyByOverride({ criterionId: action.criterion_id, signerId: ctx.decidedBy, reason: why('Review', ctx) });
|
|
36
|
+
return { done: true };
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
release: Object.freeze({
|
|
40
|
+
approve_override: async (action, ctx) => {
|
|
41
|
+
try {
|
|
42
|
+
await db.decideOverrideRequest({ requestId: action.request_id, archonId: ctx.decidedBy, decision: 'approve', note: why('Release', ctx) });
|
|
43
|
+
return { done: true };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err && err.code === 'REQUEST_NOT_OPEN') return { done: false, reason: 'request_not_open' };
|
|
46
|
+
if (err && err.code === 'OVERRIDE_OWNER_ONLY') return { done: false, reason: 'override_owner_only' };
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
approve_gate: async (action, ctx) => {
|
|
51
|
+
const out = await routes.gate().approveGatePr({ number: action.pr, login: `council, matter ${ctx && ctx.matterId}` });
|
|
52
|
+
return out.ok ? { done: true } : { done: false, reason: out.code };
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function contributeLifecycleMatterActs() {
|
|
58
|
+
for (const [area, acts] of Object.entries(ACTS)) {
|
|
59
|
+
for (const [type, act] of Object.entries(acts)) api.contribute(POINT, { area, type, act });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { ACTS, contributeLifecycleMatterActs };
|