@bongos/core 1.21.39 → 1.21.40
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 +30 -30
- package/docs/copy-inventory.md +3 -4
- package/docs/copy-registry.json +4 -13
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +14 -2
- package/docs/page-readings.json +387 -340
- package/modules/government/docket.js +14 -0
- package/modules/hall-ui/public/street-card.css +21 -2
- package/modules/hall-ui/public/street-card.js +198 -64
- package/modules/hall-ui/public/street-card.states.json +495 -7
- package/modules/hall-ui/public/street-desk.js +47 -2
- package/modules/hall-ui/public/street.css +1 -3
- package/modules/hall-ui/public/street.html +0 -1
- package/modules/hall-ui/public/street.js +42 -15
- package/modules/ideas/governor-docket.js +2 -0
- package/modules/lifecycle/governor-docket.js +4 -0
- package/modules/onboarding/routes/access-requests.js +2 -0
- package/modules/security/routes/reports.js +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/module-rechecks.js +12 -1
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +20 -0
- package/tests/hall_matter_card.mjs +143 -18
- package/tests/hall_street.mjs +1 -1
|
@@ -78,7 +78,9 @@
|
|
|
78
78
|
var $ = function (id) { return document.getElementById(id); };
|
|
79
79
|
var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
|
80
80
|
var data = { docket: null, goals: [], rollups: [], earnings: null, loaded: false, tasks: {}, room: null, constitution: null };
|
|
81
|
-
|
|
81
|
+
// ask: the act whose chips stand open; more: the detail is open in place;
|
|
82
|
+
// pick: which of the review queue the queue's card shows (task 1004566)
|
|
83
|
+
var card = { cv: null, form: null, ask: null, more: false, pick: null, error: '', busy: false, fresh: null, answer: null };
|
|
82
84
|
var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
|
|
83
85
|
// the desk's own state: rows you acted on this visit, what you did per floor,
|
|
84
86
|
// the floors you cleared, the row whose facts or chips are open
|
|
@@ -1041,21 +1043,42 @@
|
|
|
1041
1043
|
|
|
1042
1044
|
|
|
1043
1045
|
// ---------- the matter card (task 1004528: it acts) ----------
|
|
1046
|
+
// Its acts are the desk row's for the same item (GovDesk.cardFor, task
|
|
1047
|
+
// 1004566): the same words, chips, routes and permission checks. "more" grows
|
|
1048
|
+
// the card in place; nothing on it opens an older page to decide.
|
|
1044
1049
|
function fillCard() {
|
|
1045
|
-
var C = window.GovMatterCard;
|
|
1046
|
-
var
|
|
1050
|
+
var C = window.GovMatterCard, D = window.GovDesk;
|
|
1051
|
+
var ctx = { room: data.room, constitution: data.constitution, now: Date.now(), goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), pick: card.pick };
|
|
1052
|
+
var cv = D && D.cardFor ? D.cardFor(view.open, data.docket, ctx) : C ? C.model({ docket: data.docket, key: view.open, room: data.room, constitution: data.constitution, now: Date.now() }) : null;
|
|
1047
1053
|
if (!cv) { closeCard(false); return null; }
|
|
1048
1054
|
card.cv = cv;
|
|
1049
1055
|
$('streetCardWhere').textContent = cv.where;
|
|
1050
1056
|
$('streetCardWhen').textContent = cv.when;
|
|
1051
1057
|
$('streetCardTitle').textContent = cv.title;
|
|
1052
|
-
C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm }, { form: card.form, error: card.error, busy: card.busy, fresh: card.fresh });
|
|
1053
|
-
var
|
|
1054
|
-
if (cv.href) open.setAttribute('href', cv.href);
|
|
1055
|
-
open.hidden = !cv.href;
|
|
1058
|
+
C.render(document, $('streetCardBody'), cv, { act: actOn, openForm: openForm, ask: askFor, more: toggleMore, pick: pickQueued }, { form: card.form, ask: card.ask, more: card.more, error: card.error, busy: card.busy, fresh: card.fresh });
|
|
1059
|
+
var frame = $('streetCard'); if (frame) frame.setAttribute('data-more', card.more ? 'open' : 'closed');
|
|
1056
1060
|
placeCard();
|
|
1057
1061
|
return cv;
|
|
1058
1062
|
}
|
|
1063
|
+
function askFor(key) {
|
|
1064
|
+
card.ask = card.ask === key ? null : key;
|
|
1065
|
+
card.error = '';
|
|
1066
|
+
fillCard();
|
|
1067
|
+
var body = $('streetCardBody'), first = body && body.querySelector ? body.querySelector('.mcard-chip, .mcard-chips__field') : null;
|
|
1068
|
+
if (card.ask && first && first.focus) first.focus();
|
|
1069
|
+
}
|
|
1070
|
+
function toggleMore() {
|
|
1071
|
+
card.more = !card.more;
|
|
1072
|
+
fillCard();
|
|
1073
|
+
var mb = $('streetCardBody'), t = mb && mb.querySelector ? mb.querySelector('[data-act="more"]') : null;
|
|
1074
|
+
if (t && t.focus) t.focus();
|
|
1075
|
+
}
|
|
1076
|
+
function pickQueued(key) {
|
|
1077
|
+
card.pick = key; card.ask = null; card.more = false; card.form = null; card.error = '';
|
|
1078
|
+
fillCard();
|
|
1079
|
+
var close = $('streetCardClose');
|
|
1080
|
+
if (close && close.focus) close.focus();
|
|
1081
|
+
}
|
|
1059
1082
|
function placeCard() {
|
|
1060
1083
|
var card = $('streetCard');
|
|
1061
1084
|
if (!card || !view.open || !view.model) return;
|
|
@@ -1071,7 +1094,7 @@
|
|
|
1071
1094
|
function openCard(key, opener) {
|
|
1072
1095
|
view.open = key;
|
|
1073
1096
|
view.opener = opener || null;
|
|
1074
|
-
card.form = null; card.error = ''; card.fresh = null;
|
|
1097
|
+
card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.fresh = null;
|
|
1075
1098
|
var el = $('streetCard');
|
|
1076
1099
|
if (!fillCard()) return;
|
|
1077
1100
|
el.hidden = false;
|
|
@@ -1084,7 +1107,7 @@
|
|
|
1084
1107
|
var was = view.opener;
|
|
1085
1108
|
view.open = null;
|
|
1086
1109
|
view.opener = null;
|
|
1087
|
-
card.cv = null; card.form = null; card.error = ''; card.busy = false; card.fresh = null;
|
|
1110
|
+
card.cv = null; card.form = null; card.ask = null; card.more = false; card.pick = null; card.error = ''; card.busy = false; card.fresh = null;
|
|
1088
1111
|
var el = $('streetCard');
|
|
1089
1112
|
if (el) el.hidden = true;
|
|
1090
1113
|
draw();
|
|
@@ -1123,32 +1146,36 @@
|
|
|
1123
1146
|
card.form = name || null;
|
|
1124
1147
|
card.error = '';
|
|
1125
1148
|
fillCard();
|
|
1126
|
-
var f = name === '
|
|
1149
|
+
var f = name === 'revise' ? $('mcardRevise') : $('streetCardClose');
|
|
1127
1150
|
if (f && f.focus) f.focus();
|
|
1128
1151
|
}
|
|
1129
1152
|
// One act: checked, sent to its own route (the server's permission checks
|
|
1130
1153
|
// decide), then answered. An act that records nothing (abstain, hold) is
|
|
1131
1154
|
// answered without a request.
|
|
1155
|
+
// An act from the card goes with the desk row's own card when a row stands
|
|
1156
|
+
// behind it (its subject: which route, the council's), else the model's.
|
|
1132
1157
|
function actOn(key, input) {
|
|
1133
1158
|
var C = window.GovMatterCard, cv = card.cv;
|
|
1134
1159
|
if (!C || !cv || card.busy) return Promise.resolve(false);
|
|
1135
|
-
var
|
|
1160
|
+
var subj = cv.row && cv.row.card ? cv.row.card : cv;
|
|
1161
|
+
var refused = C.check(subj, key, input);
|
|
1136
1162
|
if (refused) { card.error = refused; fillCard(); return Promise.resolve(false); }
|
|
1137
1163
|
card.error = '';
|
|
1138
|
-
var req = C.request(
|
|
1139
|
-
if (!req) return Promise.resolve(answered(
|
|
1164
|
+
var req = C.request(subj, key, input);
|
|
1165
|
+
if (!req) return Promise.resolve(answered(subj, key, {}, false));
|
|
1140
1166
|
card.busy = true;
|
|
1141
1167
|
fillCard();
|
|
1142
1168
|
return send(req).then(function (out) {
|
|
1143
1169
|
card.busy = false;
|
|
1144
1170
|
if (!out.ok) { card.error = C.refusal(out.status, out.body); if (view.open) fillCard(); return false; }
|
|
1145
|
-
return answered(
|
|
1171
|
+
return answered(subj, key, out.body, true);
|
|
1146
1172
|
});
|
|
1147
1173
|
}
|
|
1148
1174
|
function answered(cv, key, body, recorded) {
|
|
1149
1175
|
var a = window.GovMatterCard.answer(cv, key, body);
|
|
1150
1176
|
say(a.toast);
|
|
1151
|
-
card.form = null;
|
|
1177
|
+
card.form = null; card.ask = null;
|
|
1178
|
+
card.pick = null;
|
|
1152
1179
|
card.fresh = key === 'for' || key === 'object' ? 'you' : null;
|
|
1153
1180
|
if (a.building && a.kind) card.answer = { building: a.building, matter: cv.key, kind: a.kind, at: Date.now() };
|
|
1154
1181
|
if (a.close) closeCard(true);
|
|
@@ -42,6 +42,8 @@ function blockerDocketMatter(b, mayResolve) {
|
|
|
42
42
|
tally: null,
|
|
43
43
|
href: `/blockers#blocker-${b.id}`,
|
|
44
44
|
ref: { type: 'blocker', id: b.id },
|
|
45
|
+
// the card's "more" (task 1004566): the body GET /blockers shows everyone
|
|
46
|
+
detail: b.body_md || null,
|
|
45
47
|
// the floor desk's one act (task 1004560): decide, by one of its options
|
|
46
48
|
...(mayResolve ? { acts: [{ key: 'decide', options: blockerOptions(b.body_md) }] } : {}),
|
|
47
49
|
};
|
|
@@ -41,6 +41,8 @@ function overrideMatter(r, mayDecide) {
|
|
|
41
41
|
tally: null,
|
|
42
42
|
href: `/task/${encodeURIComponent(r.task_id)}`,
|
|
43
43
|
ref: { type: 'override_request', id: r.id },
|
|
44
|
+
// the card's "more" (task 1004566): the reason GET /override-requests shows
|
|
45
|
+
detail: r.rationale || null,
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -58,6 +60,8 @@ function removalMatter(r) {
|
|
|
58
60
|
tally: null,
|
|
59
61
|
href: `/task/${encodeURIComponent(r.proposal_task_id)}`,
|
|
60
62
|
ref: { type: 'task', id: r.proposal_task_id },
|
|
63
|
+
// the card's "more" (task 1004566): the proposal's own text, from the same read
|
|
64
|
+
detail: r.description || null,
|
|
61
65
|
// POST /removal-proposals/:id/decide, the docket source's own atom (task 1004559)
|
|
62
66
|
acts: [{ key: 'remove' }, { key: 'keep' }],
|
|
63
67
|
};
|
|
@@ -236,6 +236,8 @@ function accessRequestDocketMatter(r) {
|
|
|
236
236
|
tally: null,
|
|
237
237
|
href: '/watch',
|
|
238
238
|
ref: { type: 'access_request', id: r.id },
|
|
239
|
+
// the card's "more" (task 1004566): their note, as GET /access-requests shows it
|
|
240
|
+
detail: r.note || null,
|
|
239
241
|
};
|
|
240
242
|
}
|
|
241
243
|
|
|
@@ -56,6 +56,9 @@ function redteamDocketMatter(r) {
|
|
|
56
56
|
tally: null,
|
|
57
57
|
href: '/watch',
|
|
58
58
|
ref: { type: 'vulnerability_report', id: r.id },
|
|
59
|
+
// the card's "more" (task 1004566): what GET /security/reports shows this
|
|
60
|
+
// same atom — what it targets and the report's description
|
|
61
|
+
detail: [r.target ? `Target: ${r.target}` : '', r.description || ''].filter(Boolean).join('\n\n') || null,
|
|
59
62
|
// The floor desk's acts (task 1004559): the docket only lists this for a
|
|
60
63
|
// holder of the adjudicate atom, which both routes check.
|
|
61
64
|
acts: [{ key: 'confirm' }, { key: 'dispute' }],
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.40",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.40",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.40",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8998,5 +8998,11 @@
|
|
|
8998
8998
|
"id": "1004565",
|
|
8999
8999
|
"text": "Inside each building on the governor street, the separate \"on the table here\" list is gone. The floor desks already show every matter as a row you can act on, so the list only repeated them. The rest of the building panel look"
|
|
9000
9000
|
}
|
|
9001
|
+
],
|
|
9002
|
+
"1.21.40": [
|
|
9003
|
+
{
|
|
9004
|
+
"id": "1004566",
|
|
9005
|
+
"text": "Every card on the governor street now acts where it opens. Pressing a pin gives the same buttons as that item's row at its building desk: confirm or dispute a security report, remove or keep, decide a blocker by tapping an opt"
|
|
9006
|
+
}
|
|
9001
9007
|
]
|
|
9002
9008
|
}
|
|
@@ -53,6 +53,13 @@ async function listModuleRechecks({ limit = RECHECK_LIMIT, db = null } = {}) {
|
|
|
53
53
|
|
|
54
54
|
// One version as a docket matter. Pure. A failed gate needs a governor who may
|
|
55
55
|
// ask for a re-check; a pending part is a re-check already moving.
|
|
56
|
+
// The one-tap reasons a re-check may give (POST /store/modules/:key/versions/
|
|
57
|
+
// :version/reassess requires one; task 1004566).
|
|
58
|
+
const RECHECK_REASONS = Object.freeze([
|
|
59
|
+
{ key: 'fixed', label: 'fixed since' },
|
|
60
|
+
{ key: 'misfired', label: 'check misfired' },
|
|
61
|
+
{ key: 'again', label: 'look again' },
|
|
62
|
+
]);
|
|
56
63
|
function moduleRecheckMatter(r, mayRerun) {
|
|
57
64
|
const failed = r.security_failed === true;
|
|
58
65
|
const pending = Array.isArray(r.pending_parts) ? r.pending_parts.filter(Boolean) : [];
|
|
@@ -70,7 +77,11 @@ function moduleRecheckMatter(r, mayRerun) {
|
|
|
70
77
|
proposer: null,
|
|
71
78
|
tally: null,
|
|
72
79
|
href: '/modules',
|
|
73
|
-
|
|
80
|
+
// "<key>@<version>": the reassess route's two path parts (task 1004566)
|
|
81
|
+
ref: { type: 'module_version', id: `${r.module_key}@${r.version}` },
|
|
82
|
+
// the one act a governor takes on it, under the reassess route's own atom,
|
|
83
|
+
// only when its security check failed (a pending check is still running)
|
|
84
|
+
...(failed && mayRerun ? { acts: [{ key: 'recheck', options: RECHECK_REASONS }] } : {}),
|
|
74
85
|
};
|
|
75
86
|
}
|
|
76
87
|
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.40'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -101,6 +101,26 @@ test('normalizeMatter: the one shape, prefixed id, ISO dates', () => {
|
|
|
101
101
|
assert.deepEqual(Object.keys(m).sort(), ['building', 'deadline', 'href', 'id', 'opened_at', 'proposer', 'ref', 'rule', 'short', 'source', 'state', 'tally', 'title', 'where'].sort());
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test('normalizeMatter carries a matter\'s detail for the card\'s "more" only as bounded text (task 1004566)', () => {
|
|
105
|
+
const base = { id: 1, title: 't' };
|
|
106
|
+
const at = { source: 'blockers', building: 'planning' };
|
|
107
|
+
assert.equal(docket.normalizeMatter({ ...base, detail: ' Option A: Render\n\nOption B: Fly ' }, at).detail, 'Option A: Render\n\nOption B: Fly', 'lines kept, ends trimmed');
|
|
108
|
+
assert.equal(docket.normalizeMatter({ ...base, detail: 'x'.repeat(5000) }, at).detail.length, 4000);
|
|
109
|
+
for (const bad of [null, '', ' ', 42, { md: 'x' }, ['x']]) assert.ok(!('detail' in docket.normalizeMatter({ ...base, detail: bad }, at)), JSON.stringify(bad));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the sources hand the detail their own list route shows, and a failed module check its one act (task 1004566)', () => {
|
|
113
|
+
const r = reportsRoute.redteamDocketMatter({ id: 2, title: 'x', status: 'open', target: '/login', description: 'No limit on tries.' });
|
|
114
|
+
assert.equal(r.detail, 'Target: /login\n\nNo limit on tries.');
|
|
115
|
+
assert.equal(reportsRoute.redteamDocketMatter({ id: 2, title: 'x', status: 'open' }).detail, null);
|
|
116
|
+
const failed = moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'image-tools', version: '1.2.0', security_failed: true }, true);
|
|
117
|
+
assert.deepEqual(failed.ref, { type: 'module_version', id: 'image-tools@1.2.0' }, 'the reassess route\'s two path parts');
|
|
118
|
+
assert.deepEqual(failed.acts.map((a) => a.key), ['recheck']);
|
|
119
|
+
assert.ok(failed.acts[0].options.length >= 2 && failed.acts[0].options.every((o) => o.label.length <= 16), 'reasons are one-tap chips');
|
|
120
|
+
assert.ok(!('acts' in moduleRechecks.moduleRecheckMatter({ version_id: 7, module_key: 'm', version: '1.0.0', security_failed: true }, false)), 'not without the reassess atom');
|
|
121
|
+
assert.ok(!('acts' in moduleRechecks.moduleRecheckMatter({ version_id: 8, module_key: 'm', version: '1.0.0', security_failed: false, pending_parts: ['tests'] }, true)), 'not while its check is still running');
|
|
122
|
+
});
|
|
123
|
+
|
|
104
124
|
test('normalizeMatter fails SAFE: unknown state → moving (never a false "needs you"), unknown rule → null', () => {
|
|
105
125
|
const m = docket.normalizeMatter({ id: 1, title: 't', state: 'urgent!!', rule: 'vibes', building: 'castle' }, { source: 's', building: 'review' });
|
|
106
126
|
assert.equal(m.state, 'moving');
|
|
@@ -31,6 +31,7 @@ const require = createRequire(import.meta.url);
|
|
|
31
31
|
require(path.join(PUB, 'city-draw.js'));
|
|
32
32
|
require(path.join(PUB, 'street-draw.js'));
|
|
33
33
|
const C = require(path.join(PUB, 'street-card.js'));
|
|
34
|
+
const D = require(path.join(PUB, 'street-desk.js'));
|
|
34
35
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
35
36
|
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
36
37
|
const VIEWER = '95';
|
|
@@ -77,8 +78,12 @@ test('an open unanimous sitting that waits on you: the proposer\'s words, the ch
|
|
|
77
78
|
assert.equal(cv.counsel.revise, false);
|
|
78
79
|
assert.deepEqual(cv.actions.map((a) => [a.key, a.label, a.tone]), [['for', 'vote for', 'primary'], ['object', 'object', 'quiet']]);
|
|
79
80
|
assert.deepEqual(cv.textActions.map((a) => a.key), ['abstain']);
|
|
80
|
-
assert.ok(cv.objectForm.sections.length >= 3);
|
|
81
81
|
assert.equal(cv.readOnly, false);
|
|
82
|
+
// more (task 1004566): the whole rationale, who and when, and every vote in order
|
|
83
|
+
assert.equal(cv.more.text[0].words, 'Three pieces of finished work are waiting because no one is allowed to test them.\n\nMore below.');
|
|
84
|
+
assert.equal(cv.more.who, 'Put up by Ana Ruiz.');
|
|
85
|
+
assert.equal(cv.more.when, 'opened 2026-10-01 · closes 2026-10-03');
|
|
86
|
+
assert.deepEqual(cv.more.history, ['2026-10-01 · Ana Ruiz voted for', '2026-10-01 · Theo voted for', '2026-10-01 · Mira voted for']);
|
|
82
87
|
});
|
|
83
88
|
|
|
84
89
|
test('a majority sitting with an objection, after your yes: voices for and against, what it still needs', () => {
|
|
@@ -104,7 +109,7 @@ test('a sitting that came back to you: counsel answers the objection, revise and
|
|
|
104
109
|
assert.equal(cv.readOnly, false);
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
test('passed, not yours,
|
|
112
|
+
test('passed, not yours, and a ballot not read yet are read-only; a Full Idea is voted on here with its sections under more', () => {
|
|
108
113
|
const passed = card([matter({ state: 'settled' })], 'board:41', room([], [item({ closed_at: '2026-10-02T09:00:00.000Z', outcome: 'passed', votes: [vote(4, 'Ana', 'yes'), vote(5, 'Theo', 'yes')] })]));
|
|
109
114
|
assert.equal(passed.result, 'Passed 2–0. In force from 2026-10-02.');
|
|
110
115
|
assert.deepEqual([passed.readOnly, passed.actions.length, passed.textActions.length], [true, 0, 0]);
|
|
@@ -113,9 +118,11 @@ test('passed, not yours, a Full Idea, and a ballot not read yet are read-only',
|
|
|
113
118
|
assert.equal(notMine.actions.length, 0);
|
|
114
119
|
assert.ok(!notMine.seats.some((s) => s.pos === 'you'), 'no seat drawn for you when you have none');
|
|
115
120
|
const idea = card([matter({ building: 'planning', state: 'needs' })], 'board:41', room([item({ subject_type: 'full_idea', subject: { title: 'An idea', author_id: '4', field_values: { purpose: 'It does a thing.' } } })]));
|
|
116
|
-
assert.
|
|
117
|
-
assert.
|
|
118
|
-
assert.
|
|
121
|
+
assert.deepEqual(idea.actions.map((a) => a.key), ['for', 'object'], 'a Full Idea is voted on from the card (task 1004566)');
|
|
122
|
+
assert.equal(idea.result, 'Read its sections under more, then vote.');
|
|
123
|
+
assert.deepEqual(idea.more.text, [{ label: 'what it is for', words: 'It does a thing.' }], 'its sections are in front of the voter');
|
|
124
|
+
const ownIdea = card([matter({ building: 'planning', state: 'moving' })], 'board:41', room([item({ subject_type: 'full_idea', subject: { title: 'An idea', author_id: VIEWER, field_values: { purpose: 'Mine.' } }, votes: [] })]));
|
|
125
|
+
assert.deepEqual([ownIdea.readOnly, ownIdea.actions.length, ownIdea.result], [true, 0, 'Yours. It is with the sitting now.'], 'its author never votes on it');
|
|
119
126
|
const loading = card([matter()], 'board:41', null);
|
|
120
127
|
assert.deepEqual([loading.loading, loading.readOnly, loading.result], [true, true, 'Reading the sitting…']);
|
|
121
128
|
const gone = card([matter()], 'board:41', room([]));
|
|
@@ -134,8 +141,10 @@ test('a constitution amendment shows each changed setting struck and inserted ag
|
|
|
134
141
|
|
|
135
142
|
test('admissions: fine by me or object when it is yours, read-only when it is not', () => {
|
|
136
143
|
const m = { id: 'access_requests:7', source: 'access_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Admit Kai as a builder', short: 'admit kai', where: 'people · waiting to join · vouched', proposer: { login: 'kai', name: 'Kai' }, tally: null, href: '/watch', ref: { type: 'access_request', id: '7' } };
|
|
137
|
-
const cv = card([m], 'access_requests:7');
|
|
138
|
-
assert.equal(cv.quote
|
|
144
|
+
const cv = card([{ ...m, detail: 'I build the docs site.' }], 'access_requests:7');
|
|
145
|
+
assert.equal(cv.quote, null, 'the compact card is the title, one fact and its acts');
|
|
146
|
+
assert.deepEqual(cv.more.text.map((t) => t.words), ['Kai asked to join as a builder, and someone vouched for them.', 'I build the docs site.']);
|
|
147
|
+
assert.equal(cv.more.who, 'Raised by Kai.');
|
|
139
148
|
assert.deepEqual(cv.actions.map((a) => [a.key, a.label]), [['fine', 'fine by me'], ['refuse', 'object']]);
|
|
140
149
|
const other = card([{ ...m, state: 'moving' }], 'access_requests:7');
|
|
141
150
|
assert.deepEqual([other.readOnly, other.actions.length], [true, 0]);
|
|
@@ -312,21 +321,20 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
|
|
|
312
321
|
assert.match(p.byId.get('streetPins').children[0].className, /street-pin--moving street-pin--answer/, 'the pin moved from needs to moving');
|
|
313
322
|
});
|
|
314
323
|
|
|
315
|
-
test('object: the
|
|
324
|
+
test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the reason and the part on the record', async () => {
|
|
316
325
|
const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, item_closed: true, outcome: 'returned' } } });
|
|
317
326
|
p.byId.get('streetPins').children[0].fire('click');
|
|
318
327
|
await settle(); await settle();
|
|
319
|
-
|
|
320
|
-
assert.equal(
|
|
321
|
-
byAct(
|
|
322
|
-
|
|
323
|
-
assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, '
|
|
324
|
-
assert.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
byAct(p.byId.get('streetCardBody'), 'object-send').fire('click');
|
|
328
|
+
const body = () => p.byId.get('streetCardBody');
|
|
329
|
+
assert.equal(byAct(body(), 'object').textContent, D.ACT_WORDS.object, 'the desk row\'s word');
|
|
330
|
+
assert.equal(byAct(body(), 'object').attrs['aria-expanded'], 'false');
|
|
331
|
+
byAct(body(), 'object').fire('click');
|
|
332
|
+
assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, 'asking sends nothing');
|
|
333
|
+
assert.equal(byAct(body(), 'object').attrs['aria-expanded'], 'true');
|
|
334
|
+
assert.deepEqual(walk(body(), (n) => n.className === 'mcard-chip').map((n) => n.textContent), Object.values(D.CHIPS), 'the same chips as the desk row');
|
|
335
|
+
byAct(body(), 'chip-scope').fire('click');
|
|
328
336
|
await settle(); await settle(); await settle();
|
|
329
|
-
assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => c.body), [{ direction: 'no', reason_md: '
|
|
337
|
+
assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => c.body), [{ direction: 'no', reason_md: 'Objects to what it changes (one tap on the floor desk).', section_key: 'scope' }]);
|
|
330
338
|
assert.equal(p.byId.get('streetToast').textContent, 'Your objection is on the record. It goes back to its author to revise.');
|
|
331
339
|
});
|
|
332
340
|
|
|
@@ -382,6 +390,123 @@ test('the card\'s motion is CSS only and dies under reduced motion; tokens only;
|
|
|
382
390
|
assert.match(html, /id="streetToast" role="status" aria-live="polite"/);
|
|
383
391
|
});
|
|
384
392
|
|
|
393
|
+
// ── one action model, and more in place (task 1004566) ──────────────────────
|
|
394
|
+
// Every docket source in the shape its route hands it, with the acts its owning
|
|
395
|
+
// module gives a viewer who may act. The card for each must offer the desk
|
|
396
|
+
// row's acts for the same item and link nowhere.
|
|
397
|
+
const SOURCE_FIXTURES = () => {
|
|
398
|
+
const ms = [
|
|
399
|
+
matter(),
|
|
400
|
+
matter({ id: 'board:43', building: 'planning', short: 'idea to ratify', title: 'Ask before a big change', ref: { type: 'board_item', id: '43' } }),
|
|
401
|
+
{ id: 'blockers:11', source: 'blockers', building: 'planning', rule: 'decide', state: 'needs', title: 'Hosting for the demo', short: 'blocker', where: 'planning · holds 2 tasks', opened_at: '2026-09-30T09:00:00.000Z', proposer: null, tally: null, href: '/blockers#blocker-11', ref: { type: 'blocker', id: '11' }, acts: [{ key: 'decide', options: [{ key: 'go ahead', label: 'go ahead' }, { key: 'not now', label: 'not now' }] }], detail: 'Option A: Render\nOption B: Fly' },
|
|
402
|
+
{ id: 'access_requests:7', source: 'access_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Admit Kai as a builder', where: 'people · waiting to join', proposer: { login: 'kai', name: 'Kai' }, href: '/watch', ref: { type: 'access_request', id: '7' } },
|
|
403
|
+
{ id: 'goal_join_requests:5', source: 'goal_join_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Bo asks to join Sign-up', proposer: { login: 'bo' }, href: '/goals#/goal/1000125', ref: { type: 'goal_membership_request', id: '5' } },
|
|
404
|
+
{ id: 'override_requests:9', source: 'override_requests', building: 'release', rule: 'decide', state: 'needs', title: 'Override: the new sign-up', proposer: { login: 'lu' }, href: '/task/12', ref: { type: 'override_request', id: '9' }, detail: 'The grader timed out twice.' },
|
|
405
|
+
{ id: 'removal_proposals:61', source: 'removal_proposals', building: 'disputes', rule: 'decide', state: 'needs', title: 'Old search page', href: '/task/61', ref: { type: 'task', id: '61' }, acts: [{ key: 'remove' }, { key: 'keep' }] },
|
|
406
|
+
{ id: 'redteam_reports:4', source: 'redteam_reports', building: 'disputes', rule: 'decide', state: 'needs', title: 'Security report: sign-in limit skipped', href: '/watch', ref: { type: 'vulnerability_report', id: '4' }, acts: [{ key: 'confirm' }, { key: 'dispute' }] },
|
|
407
|
+
{ id: 'criteria_uat:3', source: 'criteria_uat', building: 'review', rule: 'signoff', state: 'needs', title: 'The export runs nightly', href: '/goals#/goal/7', ref: { type: 'criterion', id: '3' }, acts: [{ key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] }, { key: 'force', options: [{ key: 'tested_by_hand', label: 'tested by hand' }] }] },
|
|
408
|
+
{ id: 'core_update:1.21.20', source: 'core_update', building: 'release', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } },
|
|
409
|
+
{ id: 'gate_approvals:88', source: 'gate_approvals', building: 'release', rule: 'decide', state: 'needs', title: 'PR 88 waits at the gate', href: '/gate', ref: { type: 'pull_request', id: '88' } },
|
|
410
|
+
{ id: 'autobongos_fence:project', source: 'autobongos_fence', building: 'automation', rule: 'decide', state: 'needs', title: 'Unattended building is off', href: '/autobongos', ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_on' }] },
|
|
411
|
+
{ id: 'module_rechecks:7', source: 'module_rechecks', building: 'release', rule: 'rule', state: 'needs', title: 'image-tools 1.2.0 failed its security check', href: '/modules', ref: { type: 'module_version', id: 'image-tools@1.2.0' }, acts: [{ key: 'recheck', options: [{ key: 'fixed', label: 'fixed since' }] }] },
|
|
412
|
+
];
|
|
413
|
+
const d = docket(ms);
|
|
414
|
+
d.desks = [{ key: 'release.core', building: 'release', status: 'live', rows: [{ id: 'release.core:41', desk: 'release.core', building: 'release', name: 'Core 1.21.20', fact: 'not checked yet', facts: [], over: 'core_update', ref: { type: 'core_pin', id: '41' }, acts: [{ key: 'check', options: [{ key: '1.21.20', label: '1.21.20' }] }] }] }];
|
|
415
|
+
const idea = item({ id: '43', subject_type: 'full_idea', subject: { title: 'Ask before a big change', author_id: '4', field_values: { purpose: 'Ask first.', success: 'No surprises.' } }, votes: [] });
|
|
416
|
+
return { d, r: room([item(), idea]) };
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
test('every docket source kind opens a card with the desk row\'s acts when the viewer may act, and links nowhere', () => {
|
|
420
|
+
const expected = [...read('../../government/docket.js').split('const EXPECTED_SOURCES')[1].split(']);')[0].matchAll(/source: '([a-z_]+)'/g)].map((x) => x[1]);
|
|
421
|
+
const { d, r } = SOURCE_FIXTURES();
|
|
422
|
+
assert.deepEqual([...new Set(d.matters.map((m) => m.source))].sort(), expected.slice().sort(), 'a fixture for every source the docket names');
|
|
423
|
+
const { document } = fakeDom();
|
|
424
|
+
const matched = [];
|
|
425
|
+
for (const m of d.matters) {
|
|
426
|
+
const cv = D.cardFor(m.id, d, { room: r, now: NOW });
|
|
427
|
+
assert.ok(cv && cv.row && cv.row.actions.length, `${m.id}: the card acts`);
|
|
428
|
+
assert.equal(cv.readOnly, false, m.id);
|
|
429
|
+
// one action model: the card's acts ARE the desk row's for the same item
|
|
430
|
+
const row = [0, 1, 2, 3, 4, 5, 6, 7].reduce((all, f) => all.concat(D.rowsFor(m.building, f, d, { room: r, now: NOW })), [])
|
|
431
|
+
.find((x) => x.matter === m.id || (x.desk && m.source === 'core_update' && x.over === 'core_update'));
|
|
432
|
+
if (row) { matched.push(m.id); assert.deepEqual(cv.row.actions.map((a) => [a.key, a.label]), row.actions.map((a) => [a.key, a.label]), `${m.id}: the same acts as its desk row`); }
|
|
433
|
+
const host = document.createElement('div');
|
|
434
|
+
C.render(document, host, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: true });
|
|
435
|
+
const links = walk(host, (n) => n.tagName === 'a' || (n.attrs && 'href' in n.attrs));
|
|
436
|
+
assert.deepEqual(links.map((n) => n.attrs.href), [], `${m.id}: no link out`);
|
|
437
|
+
assert.ok(walk(host, (n) => n.attrs && n.attrs['data-act'] === 'more').length === 1, `${m.id}: a more toggle`);
|
|
438
|
+
}
|
|
439
|
+
assert.deepEqual(matched.sort(), d.matters.map((m) => m.id).sort(), 'the comparison ran for every kind');
|
|
440
|
+
// the core keeps check-then-take: its card is the Release desk's core row
|
|
441
|
+
const core = D.cardFor('core_update:1.21.20', d, { now: NOW });
|
|
442
|
+
assert.deepEqual(core.row.actions.map((a) => a.key), ['check']);
|
|
443
|
+
assert.deepEqual(C.request(core.row.card, 'check', { option: '1.21.20' }), { method: 'POST', url: '/api/bongos/provisioning/instances/41/core-upgrade', body: { to: '1.21.20', dry_run: true } });
|
|
444
|
+
// a module version is checked again through the reassess route, with a one-tap reason
|
|
445
|
+
const rc = D.cardFor('module_rechecks:7', d, { now: NOW });
|
|
446
|
+
assert.deepEqual(C.request(rc.row.card, 'recheck', { option: 'fixed', label: 'fixed since' }), { method: 'POST', url: '/api/bongos/store/modules/image-tools/versions/1.2.0/reassess', body: { reason: 'Asked on the street: fixed since.' } });
|
|
447
|
+
// the review queue's pin opens its first piece of work, with that work's acts
|
|
448
|
+
const q = D.cardFor('queue', d, { room: r, now: NOW });
|
|
449
|
+
assert.equal(q.key, 'criteria_uat:3');
|
|
450
|
+
assert.deepEqual(q.row.actions.map((a) => a.key), ['signoff', 'force']);
|
|
451
|
+
// the page carries no "open it where it lives" link
|
|
452
|
+
assert.doesNotMatch(read('street.html'), /streetCardOpen|where it lives/);
|
|
453
|
+
assert.doesNotMatch(read('street.js'), /streetCardOpen/);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('a viewer who may not act gets the card without acts and still no link out', () => {
|
|
457
|
+
const { d, r } = SOURCE_FIXTURES();
|
|
458
|
+
d.matters = d.matters.map((m) => { const x = { ...m, state: 'moving' }; delete x.acts; return x; });
|
|
459
|
+
d.desks = []; // a desk row is only handed to a viewer who may act on it
|
|
460
|
+
const { document } = fakeDom();
|
|
461
|
+
for (const m of d.matters.filter((x) => x.source !== 'board')) {
|
|
462
|
+
const cv = D.cardFor(m.id, d, { room: r, now: NOW });
|
|
463
|
+
assert.equal(cv.row, null, `${m.id}: nothing to act on`);
|
|
464
|
+
const host = document.createElement('div');
|
|
465
|
+
C.render(document, host, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: true });
|
|
466
|
+
assert.deepEqual(walk(host, (n) => n.attrs && 'href' in n.attrs).map((n) => n.attrs.href), [], m.id);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('more expands the same card in place: the full text, who and when, and the record; less folds it', async () => {
|
|
471
|
+
const { d } = SOURCE_FIXTURES();
|
|
472
|
+
d.matters = d.matters.filter((m) => m.source === 'blockers');
|
|
473
|
+
const p = await bootPage({ feeds: { ...BASE(d) } });
|
|
474
|
+
p.byId.get('streetPins').children[0].fire('click');
|
|
475
|
+
await settle(); await settle();
|
|
476
|
+
const body = () => p.byId.get('streetCardBody');
|
|
477
|
+
const toggle = () => byAct(body(), 'more');
|
|
478
|
+
assert.equal(toggle().attrs['aria-expanded'], 'false');
|
|
479
|
+
assert.equal(walk(body(), (n) => n.className === 'mcard-more').length, 0, 'compact first');
|
|
480
|
+
// compact: one fact and at most two buttons (the line rule)
|
|
481
|
+
assert.equal(walk(body(), (n) => n.className === 'street-card__result').length, 1);
|
|
482
|
+
assert.ok(walk(body(), (n) => /^mcard-btn /.test(n.className)).length <= 2);
|
|
483
|
+
toggle().fire('click');
|
|
484
|
+
assert.equal(toggle().attrs['aria-expanded'], 'true');
|
|
485
|
+
assert.equal(toggle().attrs['aria-controls'], 'mcardMore');
|
|
486
|
+
assert.equal(p.byId.get('streetCard').attrs['data-more'], 'open');
|
|
487
|
+
const more = walk(body(), (n) => n.className === 'mcard-more')[0];
|
|
488
|
+
assert.equal(more.attrs.id, 'mcardMore');
|
|
489
|
+
assert.deepEqual(walk(more, (n) => n.className === 'mcard-more__text').map((n) => n.textContent), ['Option A: Render\nOption B: Fly']);
|
|
490
|
+
assert.equal(walk(more, (n) => n.className === 'mcard-more__meta')[0].textContent, 'opened 2026-09-30');
|
|
491
|
+
assert.deepEqual(walk(more, (n) => n.tagName === 'li').map((n) => n.textContent), ['how it is decided: decides alone'], 'the where line is the card head already');
|
|
492
|
+
assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, 'opening more sends nothing');
|
|
493
|
+
assert.equal(p.window.location.hash, '', 'and goes nowhere');
|
|
494
|
+
// the blocker's options stand open, as on the desk: deciding it is picking one
|
|
495
|
+
byAct(body(), 'chip-go ahead');
|
|
496
|
+
assert.ok(byAct(body(), 'chip-go ahead'), 'its options stand open');
|
|
497
|
+
toggle().fire('click');
|
|
498
|
+
assert.equal(walk(body(), (n) => n.className === 'mcard-more').length, 0, 'less folds it');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('the docket hands a matter its detail only as text, bounded, and only when there is some', () => {
|
|
502
|
+
const src = read('../../government/docket.js');
|
|
503
|
+
assert.match(src, /\.\.\.detailField\(raw\.detail\)/);
|
|
504
|
+
assert.match(src, /const MAX_DETAIL = 4000;/);
|
|
505
|
+
for (const [f, re] of [['../../ideas/governor-docket.js', /detail: b\.body_md/], ['../../lifecycle/governor-docket.js', /detail: r\.rationale/], ['../../lifecycle/governor-docket.js', /detail: r\.description/], ['../../security/routes/reports.js', /detail: \[r\.target/], ['../../onboarding/routes/access-requests.js', /detail: r\.note/]]) {
|
|
506
|
+
assert.match(read(f), re, f);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
385
510
|
// ── the Board Room names the new subjects ───────────────────────────────────
|
|
386
511
|
|
|
387
512
|
test('the Board Room labels a building\'s matter as that building\'s matter, and a who-decides change as its own amendment', () => {
|
package/tests/hall_street.mjs
CHANGED
|
@@ -505,7 +505,7 @@ test('a pin opens its short card, Escape closes it and focus goes back to the pi
|
|
|
505
505
|
const card = p.byId.get('streetCard');
|
|
506
506
|
assert.equal(card.hidden, false);
|
|
507
507
|
assert.equal(p.byId.get('streetCardTitle').textContent, '<img src=x onerror=alert(1)>', 'docket text lands as text');
|
|
508
|
-
assert.equal(p.byId.get('streetCardOpen').attrs.href, '
|
|
508
|
+
assert.equal(p.byId.get('streetCardOpen').attrs.href, undefined, 'the card links nowhere: it acts in place (task 1004566)');
|
|
509
509
|
assert.equal(p.focused.el, p.byId.get('streetCardClose'));
|
|
510
510
|
assert.equal(p.byId.get('streetPins').children[0].attrs['aria-expanded'], 'true');
|
|
511
511
|
const e = { key: 'Escape', prevented: false, preventDefault() { this.prevented = true; } };
|