@bongos/core 1.21.49 → 1.21.51
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 +27 -22
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +366 -365
- package/modules/government/docket.js +3 -1
- package/modules/hall-ui/public/street.css +6 -2
- package/modules/hall-ui/public/street.js +42 -20
- package/modules/hall-ui/records/governor-street.md +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/scrub-identity.js +9 -1
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +2 -0
- package/tests/hall_preview_governor_fixtures.mjs +75 -0
- package/tests/hall_preview_scrub.mjs +5 -0
- package/tests/hall_street_amend.mjs +21 -8
- package/tests/hall_street_desk.mjs +41 -0
|
@@ -338,7 +338,9 @@ function countStates(matters) {
|
|
|
338
338
|
function homeLine(counts) {
|
|
339
339
|
if (!counts.needs && !counts.moving) return 'nothing waits on you.';
|
|
340
340
|
const parts = [];
|
|
341
|
-
|
|
341
|
+
// with nothing on you but work still moving, the design says so first
|
|
342
|
+
// ("nothing waits on you · 2 moving"), task 1004531
|
|
343
|
+
parts.push(counts.needs ? `${counts.needs} ${counts.needs === 1 ? 'matter waits' : 'matters wait'} on you` : 'nothing waits on you');
|
|
342
344
|
if (counts.moving) parts.push(`${counts.moving} moving`);
|
|
343
345
|
return parts.join(' · ');
|
|
344
346
|
}
|
|
@@ -272,7 +272,12 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
272
272
|
.inside-draft { display: flex; flex-direction: column; gap: 12px; padding: 14px 0 4px; border-top: 1px solid var(--street-rule); }
|
|
273
273
|
.inside-draft[hidden] { display: none; }
|
|
274
274
|
.inside-draft__change { gap: 6px; }
|
|
275
|
-
.inside-draft__picks { display: grid; grid-template-columns: 1fr
|
|
275
|
+
.inside-draft__picks { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
|
276
|
+
/* who and how are one-tap chips (task 1004531); a long choice wraps inside its
|
|
277
|
+
chip. Scoped under .inside-draft so they outrank the desk chip rules below. */
|
|
278
|
+
.inside-draft__chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
279
|
+
.inside-draft .inside-draft__chip { height: auto; min-height: 30px; padding: 5px 14px; white-space: normal; text-align: left; line-height: 1.3; }
|
|
280
|
+
.inside-draft .inside-draft__chip--on { background: var(--ink); border-color: var(--ink); color: var(--bg); }
|
|
276
281
|
.inside-draft__fixed { font-size: 14px; line-height: 1.45; color: var(--ink-soft); min-height: 36px; display: flex; align-items: center; }
|
|
277
282
|
.inside-meter { display: flex; flex-direction: column; gap: 6px; }
|
|
278
283
|
.inside-meter__track { height: 6px; border-radius: 999px; background: var(--street-wash); overflow: hidden; }
|
|
@@ -382,7 +387,6 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
382
387
|
|
|
383
388
|
/* narrow: the drawing in its own sideways strip, the panel below it */
|
|
384
389
|
@media (max-width: 899px) {
|
|
385
|
-
.inside-draft__picks { grid-template-columns: 1fr; }
|
|
386
390
|
.inside { display: block; overflow-x: hidden; overflow-y: auto; }
|
|
387
391
|
.inside-sizer { width: auto !important; height: auto !important; margin: 0; overflow: visible; }
|
|
388
392
|
.inside-board { position: static; width: auto; height: auto; transform: none; display: flex; flex-direction: column; gap: 16px; padding: 8px 16px 24px; }
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
var desk = { done: {}, mine: {}, cleared: {}, order: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
|
|
91
91
|
var TODAY_KEY = 'gov-desk-today';
|
|
92
92
|
var today = { day: '', n: 0 };
|
|
93
|
+
var todayDrawn = null; // the count the meter's pips last drew: they are rebuilt only when it moves
|
|
93
94
|
|
|
94
95
|
// ---------- light / dark ----------
|
|
95
96
|
var chosen = null;
|
|
@@ -811,9 +812,13 @@
|
|
|
811
812
|
var D = window.GovDesk, pipsEl = $('streetTodayPips');
|
|
812
813
|
if (!D || !pipsEl) return;
|
|
813
814
|
var t = D.todayPips(today.n);
|
|
815
|
+
setText('streetTodayN', t.text);
|
|
816
|
+
// the pips are rebuilt only when the count moves, so the newest pops once
|
|
817
|
+
// and a redraw neither replays nor cuts its pop short (task 1004531)
|
|
818
|
+
if (todayDrawn === today.n && pipsEl.children && pipsEl.children.length) return;
|
|
819
|
+
todayDrawn = today.n;
|
|
814
820
|
clear(pipsEl);
|
|
815
821
|
t.pips.forEach(function (p) { pipsEl.appendChild(node('span', 'street-today__pip' + (p.on ? ' street-today__pip--on' : '') + (p.fresh ? ' desk-pop' : ''))); });
|
|
816
|
-
setText('streetTodayN', t.text);
|
|
817
822
|
}
|
|
818
823
|
|
|
819
824
|
function pickFloor(i) {
|
|
@@ -850,8 +855,7 @@
|
|
|
850
855
|
dr.d = A ? A.draftFor(st.key, c && c.board ? c : null) : { ok: false, reason: 'unread' };
|
|
851
856
|
dr.rev++;
|
|
852
857
|
drawDraft(true);
|
|
853
|
-
|
|
854
|
-
if (who && who.focus) who.focus();
|
|
858
|
+
focusDraft('insideDraftWho');
|
|
855
859
|
return !!(dr.d && dr.d.ok);
|
|
856
860
|
});
|
|
857
861
|
}
|
|
@@ -868,26 +872,42 @@
|
|
|
868
872
|
if (!dr) return;
|
|
869
873
|
dr.rev++;
|
|
870
874
|
drawDraft(true);
|
|
875
|
+
focusDraft(focusId);
|
|
876
|
+
}
|
|
877
|
+
// Focus one control of the draft; a group of chips takes it on its picked chip.
|
|
878
|
+
function focusDraft(focusId) {
|
|
871
879
|
var el = focusId ? $(focusId) : null;
|
|
880
|
+
if (el && el.getAttribute && el.getAttribute('role') === 'radiogroup') {
|
|
881
|
+
var kids = el.children || [], pick = kids[0] || null;
|
|
882
|
+
for (var i = 0; i < kids.length; i++) if (kids[i].getAttribute('aria-checked') === 'true') { pick = kids[i]; break; }
|
|
883
|
+
el = pick;
|
|
884
|
+
}
|
|
872
885
|
if (el && el.focus) el.focus();
|
|
873
886
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
887
|
+
// A draft choice is one tap (the owner's line rule: choices are one-tap
|
|
888
|
+
// chips, task 1004531): a radio group of chips, the picked one filled.
|
|
889
|
+
function draftChips(id, list, value, onPick) {
|
|
890
|
+
var g = node('div', 'inside-draft__chips');
|
|
891
|
+
g.setAttribute('id', id);
|
|
892
|
+
g.setAttribute('role', 'radiogroup');
|
|
893
|
+
g.setAttribute('aria-labelledby', id + 'Label');
|
|
877
894
|
list.forEach(function (o) {
|
|
878
|
-
var
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
895
|
+
var on = o.value === value;
|
|
896
|
+
var b = node('button', 'desk-chip desk-chip--choice inside-draft__chip' + (on ? ' inside-draft__chip--on' : ''), o.words);
|
|
897
|
+
b.setAttribute('type', 'button');
|
|
898
|
+
b.setAttribute('role', 'radio');
|
|
899
|
+
b.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
900
|
+
b.setAttribute('data-value', o.value);
|
|
901
|
+
b.addEventListener('click', function () { if (!on) onPick(o.value); });
|
|
902
|
+
g.appendChild(b);
|
|
882
903
|
});
|
|
883
|
-
|
|
884
|
-
sel.addEventListener('change', function () { onPick(sel.value); });
|
|
885
|
-
return sel;
|
|
904
|
+
return g;
|
|
886
905
|
}
|
|
887
|
-
function draftField(labelText, forId, control) {
|
|
906
|
+
function draftField(labelText, forId, control, group) {
|
|
888
907
|
var f = node('div', 'mcard-form');
|
|
889
|
-
var l = node(forId ? 'label' : 'span', 'mcard-form__label', labelText);
|
|
890
|
-
if (forId) l.setAttribute('for', forId);
|
|
908
|
+
var l = node(forId && !group ? 'label' : 'span', 'mcard-form__label', labelText);
|
|
909
|
+
if (forId && !group) l.setAttribute('for', forId);
|
|
910
|
+
if (group) l.setAttribute('id', forId + 'Label');
|
|
891
911
|
f.appendChild(l);
|
|
892
912
|
f.appendChild(control);
|
|
893
913
|
return f;
|
|
@@ -925,13 +945,13 @@
|
|
|
925
945
|
} else ch.appendChild(node('p', 'inside-note', 'This is how it works today. Pick who decides, or how, to change it.'));
|
|
926
946
|
box.appendChild(ch);
|
|
927
947
|
var picks = node('div', 'inside-draft__picks');
|
|
928
|
-
picks.appendChild(draftField('who decides', 'insideDraftWho',
|
|
948
|
+
picks.appendChild(draftField('who decides', 'insideDraftWho', draftChips('insideDraftWho', d.whoOptions, d.decided_by, function (v) {
|
|
929
949
|
dr.d = A.pick(dr.d, 'who', v); dr.error = ''; bumpDraft('insideDraftWho');
|
|
930
|
-
})));
|
|
950
|
+
}), true));
|
|
931
951
|
if (d.ruleOptions.length) {
|
|
932
|
-
picks.appendChild(draftField('how they decide', 'insideDraftRule',
|
|
952
|
+
picks.appendChild(draftField('how they decide', 'insideDraftRule', draftChips('insideDraftRule', d.ruleOptions, d.rule, function (v) {
|
|
933
953
|
dr.d = A.pick(dr.d, 'rule', v); dr.error = ''; bumpDraft('insideDraftRule');
|
|
934
|
-
})));
|
|
954
|
+
}), true));
|
|
935
955
|
} else {
|
|
936
956
|
picks.appendChild(draftField('how they decide', null, node('span', 'inside-draft__fixed', d.decided_by === 'nobody' ? 'nothing is decided until someone holds it' : 'decides alone, as today')));
|
|
937
957
|
}
|
|
@@ -1210,6 +1230,8 @@
|
|
|
1210
1230
|
card.pick = null;
|
|
1211
1231
|
card.fresh = key === 'for' || key === 'object' ? 'you' : null;
|
|
1212
1232
|
if (a.building && a.kind) card.answer = { building: a.building, matter: cv.key, kind: a.kind, at: Date.now() };
|
|
1233
|
+
// a decision from a card is a decision today, as one from a desk row is (task 1004531)
|
|
1234
|
+
if (recorded) { bumpToday(); drawToday(); }
|
|
1213
1235
|
if (a.close) closeCard(true);
|
|
1214
1236
|
else if (view.open) { fillCard(); var c = $('streetCardClose'); if (c && c.focus) c.focus(); }
|
|
1215
1237
|
if (!recorded) return true;
|
|
@@ -47,3 +47,24 @@ Every building and every floor desk was walked in light, dark and at phone width
|
|
|
47
47
|
- **The desk's toast is the board's**: an ink pill with the orange dot, low over the desk. The street's own toast is centred again (its enter animation had undone the centring).
|
|
48
48
|
|
|
49
49
|
Left as found, on purpose: the six readings stay under a building (task 1004526 marks the building's own reading), the old month drawers stay under the desk, and the recording sign-off still goes to its page (an open owner choice).
|
|
50
|
+
|
|
51
|
+
## The whole city, walked against the prototype (task 1004531)
|
|
52
|
+
|
|
53
|
+
The UAT walkthrough of goal 1000125: open the home city, start governing, find what needs you, enter every building and district, decide a vote from its card and watch the city change, and draft an amendment from a building, in light and dark at 1440 and at phone width (390 px), side by side with `Street.dc.html` (the prototype), `Desks.dc.html` and `Ground.dc.html`, judged by the owner's line rule. The live site is behind its password gate, so the walk ran on the real pages in the hall preview harness (`--fixture-me`). **The real-vote step was done in the harness only**: the vote went to the harness's canned answer for `POST /government/board/items/:id/votes`, and the docket read after it was stubbed in the page to show what the server would now say; no vote was cast on the live site. The amendment was filed the same way (the page's `POST /government/board/decision-rules` answered in the page, the harness refuses writes). The district interior was walked over twelve stand-in tasks answered in the page, since the harness serves one task list for every goal. Screenshots stay out of the repo: they are in the session scratchpad, `walk-1004531/` (53 shots).
|
|
54
|
+
|
|
55
|
+
What it found and fixed:
|
|
56
|
+
|
|
57
|
+
- **The preview fixtures disagreed with themselves.** The docket fixture's counts, home line, per-building counts and fairness reading had been written for the first seven matters and never re-derived as later tasks added more, so the city said "3 matters wait on you", the street header 4, its pins 13, and Disputes read "0 open" under two matters that need you. `buildDocket` derives all four from the matters, so live they cannot disagree; the fixture now holds the same derivation, and `tests/hall_preview_governor_fixtures.mjs` holds it to it.
|
|
58
|
+
- **Change how this works could not be walked.** The constitution fixture had no `decision_rules` block, so the drafter said it could not read how a building decides. It now carries the block the route serves (every building as today), and the same test drafts every building from it. The fixture scrubber had read a building's decided_by (gate, board, nobody) as a login; it keeps those words now.
|
|
59
|
+
- **The drafter's choices were dropdowns.** Who decides and how they decide are one-tap chips now (the line rule: choices are one-tap chips): a labelled radio group each, the picked chip filled in ink, a long choice wrapping inside its chip, stacked one under the other so the chips have the panel's width. Focus lands on the picked chip after each pick.
|
|
60
|
+
- **A vote from a street card did not count today.** The today meter only took a pip for an act on a desk row; a decision from a card is a decision today too, so a recorded card act now fills a pip (an act that records nothing, or is refused, does not).
|
|
61
|
+
- **The meter's newest pip popped on every redraw** (any pick, the card closing, the docket read after an act), and the read after an act cut the pop short. The pips are drawn again only when the count moves, so the newest pops once, as on the board.
|
|
62
|
+
- **The home line with nothing on you.** With nothing waiting on you but work still moving, the server said "2 moving"; the prototype says "nothing waits on you · 2 moving", and now so does the server (`homeLine`).
|
|
63
|
+
|
|
64
|
+
Walked and matching, left as found: the city (pins, the one button, its line), the street (the order, the group brackets, one pin per building in its tier, the crates and the tester count pin, the six readings), every building's section, lift and panel, the card (its pager, the change, the seats and tally, counsel, the yes/no acts under the desk's words from task 1004566), the toast, and a district's road of work.
|
|
65
|
+
|
|
66
|
+
Left on purpose, not counted as differences:
|
|
67
|
+
|
|
68
|
+
- **The city sits inside the hall's shell** (sidebar and top bar). The prototype's home page carries only the wordmark, the switch, one button and one line; standalone hall pages were retired by task 1002868, and the city page adopted the shell after its record was written.
|
|
69
|
+
- **The home line never says "pieces of work stuck at Review".** The prototype adds it while Review is a vacant lot; the city draws Review as held in this project (the header of `city-draw.js`), so the clause has nothing to stand on.
|
|
70
|
+
- **Open owner choices met on the walk, untouched:** the "1" on a single-item pin, the light-mode legend hint wording, the recording sign-off going to its page, failed passed-vote actions on the record, a Discord update on withdraw, and the Work categories link.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.51",
|
|
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.51",
|
|
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.51",
|
|
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
|
@@ -9062,5 +9062,17 @@
|
|
|
9062
9062
|
"id": "1004575",
|
|
9063
9063
|
"text": "New builders get setup steps that actually work end to end: the sign-in commands in Settings and the primer now run from a fresh copy of the project, the primer's terminal commands display properly, and the setup no longer lea"
|
|
9064
9064
|
}
|
|
9065
|
+
],
|
|
9066
|
+
"1.21.50": [
|
|
9067
|
+
{
|
|
9068
|
+
"id": "1004574",
|
|
9069
|
+
"text": "The builders hall now tells newcomers one sign-in command, npx bongos setup, the same on Mac, Windows and Linux — and copying the setup commands now pastes them line by line instead of jammed onto one line."
|
|
9070
|
+
}
|
|
9071
|
+
],
|
|
9072
|
+
"1.21.51": [
|
|
9073
|
+
{
|
|
9074
|
+
"id": "1004531",
|
|
9075
|
+
"text": "We walked the whole Governor City against its design, in light and dark and on a phone: the home city, the street, every building, a vote decided from its card, and an amendment drafted from a building. Fixes: drafting a chang"
|
|
9076
|
+
}
|
|
9065
9077
|
]
|
|
9066
9078
|
}
|
|
@@ -63,6 +63,14 @@ const isEmailKey = (k) => k === 'email' || /_email$/.test(k);
|
|
|
63
63
|
// `by` / `*_by` holds a builder ID almost everywhere (created_by, shipped_by); only a
|
|
64
64
|
// non-numeric value is a login (sandbox_review.by, who approved a staged change).
|
|
65
65
|
const isByKey = (k) => k === 'by' || /_by$/.test(k);
|
|
66
|
+
// A building's `decided_by` in the decision table (government decision-rules.js)
|
|
67
|
+
// is a word of its vocabulary, never a person: "gate" (as today), "board" (the
|
|
68
|
+
// charter), "nobody" (vacant). A rank predicate has a colon and never looks like a
|
|
69
|
+
// login. Task 1004531: the constitution fixture carries that table. Only on a
|
|
70
|
+
// table ROW (it names its building in `area`), so a login that happens to be one
|
|
71
|
+
// of the three words anywhere else is still scrubbed.
|
|
72
|
+
const DECISION_WORDS = new Set(['gate', 'board', 'nobody']);
|
|
73
|
+
const isDecisionWord = (k, v, obj) => k === 'decided_by' && DECISION_WORDS.has(v) && !!obj && typeof obj.area === 'string';
|
|
66
74
|
const looksLikeLogin = (v) => /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(v) && !/^\d+$/.test(v);
|
|
67
75
|
|
|
68
76
|
// The login a name-shaped key belongs to, or undefined when the key is not a person's
|
|
@@ -159,7 +167,7 @@ function createScrubber({ firstNumber = 1 } = {}) {
|
|
|
159
167
|
const n = isPersonRecord ? self() : numberFor([`email:${value.toLowerCase()}`]);
|
|
160
168
|
return `builder-${n}@example.com`;
|
|
161
169
|
}
|
|
162
|
-
if (isByKey(key) && looksLikeLogin(value)) {
|
|
170
|
+
if (isByKey(key) && looksLikeLogin(value) && !isDecisionWord(key, value, obj)) {
|
|
163
171
|
if (STAND_IN_LOGIN.test(value)) return value;
|
|
164
172
|
return `builder-${numberFor([`login:${value.toLowerCase()}`])}`;
|
|
165
173
|
}
|
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.51'; // 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');
|
|
@@ -144,6 +144,8 @@ test('homeLine speaks the design\'s words', () => {
|
|
|
144
144
|
assert.equal(docket.homeLine({ needs: 0, moving: 0, settled: 4 }), 'nothing waits on you.');
|
|
145
145
|
assert.equal(docket.homeLine({ needs: 3, moving: 2, settled: 0 }), '3 matters wait on you · 2 moving');
|
|
146
146
|
assert.equal(docket.homeLine({ needs: 1, moving: 0, settled: 0 }), '1 matter waits on you');
|
|
147
|
+
// the prototype's words when only moving work is left (task 1004531 walkthrough)
|
|
148
|
+
assert.equal(docket.homeLine({ needs: 0, moving: 2, settled: 1 }), 'nothing waits on you · 2 moving');
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
// ── 2. the board, per viewer ─────────────────────────────────────────────────
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// tests/hall_preview_governor_fixtures.mjs — the hall preview's governor
|
|
2
|
+
// fixtures hold the server's own invariants (task 1004531).
|
|
3
|
+
//
|
|
4
|
+
// The Governor City walkthroughs run on the hall preview harness, so its
|
|
5
|
+
// fixtures are what the UAT record is judged on. Two had drifted from the
|
|
6
|
+
// shapes the routes really serve:
|
|
7
|
+
// * the docket's counts, home line and per-building counts were written for
|
|
8
|
+
// the first seven matters and never re-derived as later tasks added more,
|
|
9
|
+
// so the city said "3 matters wait on you", the street header 4, and its
|
|
10
|
+
// pins 13 — buildDocket derives all three from the matters, so live they
|
|
11
|
+
// can never disagree; the fairness reading had drifted the same way, so
|
|
12
|
+
// Disputes read "0 open" under two matters that need you;
|
|
13
|
+
// * the constitution carried no decision_rules block, so "change how this
|
|
14
|
+
// works" could only say it could not read how a building decides.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/hall_preview_governor_fixtures.mjs
|
|
17
|
+
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const docketLib = require(path.join(ROOT, 'modules/government/docket.js'));
|
|
29
|
+
const DR = require(path.join(ROOT, 'modules/government/decision-rules.js'));
|
|
30
|
+
const DB = require(path.join(ROOT, 'modules/government/decision-board.js'));
|
|
31
|
+
const A = require(path.join(ROOT, 'modules/hall-ui/public/street-amend.js'));
|
|
32
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
33
|
+
|
|
34
|
+
// buildDocket's countStates, restated: every matter counted once in its state.
|
|
35
|
+
function countStates(ms) {
|
|
36
|
+
const c = { needs: 0, moving: 0, settled: 0 };
|
|
37
|
+
for (const m of ms) c[m.state] += 1;
|
|
38
|
+
return c;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('the docket fixture: counts and the home line are derived from its matters, as buildDocket derives them', () => {
|
|
42
|
+
const d = fx('government__docket');
|
|
43
|
+
assert.ok(d.matters.length > 7, 'the dense docket (task 1004569), not the first seven');
|
|
44
|
+
assert.deepEqual(d.counts, countStates(d.matters));
|
|
45
|
+
assert.equal(d.line, docketLib.homeLine(d.counts), 'the city line under start governing agrees with the street header');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the docket fixture: every building counts exactly the matters standing at it', () => {
|
|
49
|
+
const d = fx('government__docket');
|
|
50
|
+
for (const b of d.buildings) {
|
|
51
|
+
const here = d.matters.filter((m) => m.building === b.key);
|
|
52
|
+
assert.deepEqual(b.counts, countStates(here), b.key + ': the state line under the building agrees with its pins');
|
|
53
|
+
assert.deepEqual(b.matter_ids, here.map((m) => m.id), b.key + ': its matters, in docket order');
|
|
54
|
+
}
|
|
55
|
+
assert.equal(d.buildings.reduce((n, b) => n + b.matter_ids.length, 0), d.matters.length, 'every matter stands at one building');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the docket fixture: the fairness reading is derived from the matters at Disputes, as buildDocket derives it', () => {
|
|
59
|
+
// the reading's figure is the line under Disputes on the street, so a stale
|
|
60
|
+
// one says "0 open" under a building whose pin says two matters need you
|
|
61
|
+
const d = fx('government__docket');
|
|
62
|
+
const f = d.readings.find((r) => r.key === 'fairness');
|
|
63
|
+
const want = docketLib.fairnessReading(d.matters, d.sources);
|
|
64
|
+
assert.deepEqual({ value: f.value, figure: f.figure, line: f.line, hot: f.hot }, want);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the constitution fixture: it carries the decision_rules block the route serves, so every building can draft an amendment', () => {
|
|
68
|
+
const c = fx('government__constitution');
|
|
69
|
+
assert.deepEqual(c.decision_rules, JSON.parse(JSON.stringify(DB.constitutionBlock(DR.resolveAll([], { board: c.board })))), 'the server\'s own block for this board');
|
|
70
|
+
for (const area of c.decision_rules.areas.map((r) => r.area)) {
|
|
71
|
+
const d = A.draftFor(area, c);
|
|
72
|
+
assert.equal(d.ok, true, area + ': change how this works drafts');
|
|
73
|
+
assert.equal(A.check(d), 'Say why, so the council knows what it is voting on.', area + ': the draft is a real change, waiting only for the reason');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -130,6 +130,11 @@ test('what is not a person is left exactly alone', () => {
|
|
|
130
130
|
assert.equal(req.requests[0].created_by, '1000025', 'a numeric *_by is a builder id, not a login');
|
|
131
131
|
assert.equal(req.tasks[0].credits_reward, 30, "a task's bounty is not anyone's balance");
|
|
132
132
|
assert.equal(req.modules[0].author, 'Cloud Bongos', "a module's author credit names an organisation");
|
|
133
|
+
// the decision table's decided_by is a word of its vocabulary (task 1004531)
|
|
134
|
+
const table = s.scrub({ decision_rules: { areas: [{ area: 'charter', decided_by: 'board' }, { area: 'rules', decided_by: 'gate' }, { area: 'review', decided_by: 'nobody' }, { area: 'people', decided_by: 'rank:metic+' }] } });
|
|
135
|
+
assert.deepEqual(table.decision_rules.areas.map((a) => a.decided_by), ['board', 'gate', 'nobody', 'rank:metic+'], 'who decides a building is not a person');
|
|
136
|
+
assert.equal(s.scrub({ decided_by: 'Real-Person' }).decided_by.startsWith('builder-'), true, 'a login in decided_by is still scrubbed');
|
|
137
|
+
assert.equal(s.scrub({ id: '4', decided_by: 'gate' }).decided_by.startsWith('builder-'), true, 'off a table row, even a login spelled like a decision word is scrubbed');
|
|
133
138
|
});
|
|
134
139
|
|
|
135
140
|
test('scrubbing is a fixed point, and a new run numbers above the stand-ins already in use', () => {
|
|
@@ -184,7 +184,12 @@ test('the markup: the draft lives under the button, hidden, and the model loads
|
|
|
184
184
|
assert.ok(order[0] > 0 && order[0] < order[1] && order[1] < order[2], 'card, then the draft model, then the page');
|
|
185
185
|
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
186
186
|
assert.ok(css.includes('.inside-draft[hidden] { display: none; }'));
|
|
187
|
-
assert.
|
|
187
|
+
assert.ok(css.includes('.inside-draft__picks { display: grid; grid-template-columns: 1fr; gap: 12px; }'), 'who, then how, stacked: the chips need the width (task 1004531)');
|
|
188
|
+
// the chips ride .desk-chip, which comes later in the sheet at one class: two
|
|
189
|
+
// classes are what let the wrap and the fill win (a single class lost in the browser)
|
|
190
|
+
assert.match(css, /\.inside-draft \.inside-draft__chip \{[^}]*white-space: normal;/, 'a long choice wraps inside its chip');
|
|
191
|
+
assert.match(css, /\.inside-draft \.inside-draft__chip--on \{ background: var\(--ink\);/, 'the picked chip is filled');
|
|
192
|
+
assert.ok(css.indexOf('.desk-chip--quiet, .desk-chip--choice {') > css.indexOf('.inside-draft .inside-draft__chip--on'), 'the desk rule still comes later, so the scope is what wins');
|
|
188
193
|
assert.doesNotMatch(css.slice(css.indexOf('.inside-draft')), /#[0-9a-f]{3,8}\b/i, 'tokens only');
|
|
189
194
|
assert.doesNotMatch(read('street.js'), /not built yet/);
|
|
190
195
|
});
|
|
@@ -261,6 +266,8 @@ async function bootPage({ hash = '#building=review', cons = constitution(), answ
|
|
|
261
266
|
|
|
262
267
|
test('change how this works opens the drafted amendment: struck, inserted, who, how, why', async () => {
|
|
263
268
|
const p = await bootPage();
|
|
269
|
+
const chip = (id, v) => p.byId.get(id).kids.find((k) => k.attrs['data-value'] === v);
|
|
270
|
+
const picked = (id) => p.byId.get(id).kids.find((k) => k.attrs['aria-checked'] === 'true');
|
|
264
271
|
const box = p.byId.get('insideDraft');
|
|
265
272
|
assert.equal(box.hidden, true);
|
|
266
273
|
await p.byId.get('insideAmend').fire('click')[0];
|
|
@@ -270,17 +277,23 @@ test('change how this works opens the drafted amendment: struck, inserted, who,
|
|
|
270
277
|
const del = all(box).find((n) => n.tagName === 'del'), ins = all(box).find((n) => n.tagName === 'ins');
|
|
271
278
|
assert.equal(del.textContent, 'whoever may mark work as meeting its checks, decides alone');
|
|
272
279
|
assert.equal(ins.textContent, 'the archon rank, passes unless someone objects');
|
|
273
|
-
assert.equal(p.focused.el,
|
|
274
|
-
|
|
275
|
-
assert.equal(p.byId.get('insideDraftRule').value, 'consent');
|
|
276
|
-
// a pick redraws the words and keeps focus where it was
|
|
280
|
+
assert.equal(p.focused.el, picked('insideDraftWho'), 'focus lands on the picked chip of who decides');
|
|
281
|
+
// every choice is a one-tap chip in a labelled radio group, never a select (task 1004531)
|
|
277
282
|
const who = p.byId.get('insideDraftWho');
|
|
278
|
-
who.
|
|
279
|
-
|
|
283
|
+
assert.equal(who.attrs.role, 'radiogroup');
|
|
284
|
+
assert.ok(all(box).some((n) => n.attrs.id === 'insideDraftWhoLabel' && n._text === 'who decides'), 'the group is named by its label');
|
|
285
|
+
assert.ok(!all(box).some((n) => n.tagName === 'select'), 'no dropdowns');
|
|
286
|
+
assert.equal(who.kids.length, 6);
|
|
287
|
+
assert.ok(who.kids.every((k) => k.tagName === 'button' && k.attrs.role === 'radio'));
|
|
288
|
+
assert.equal(who.kids.filter((k) => k.attrs['aria-checked'] === 'true').length, 1, 'one picked');
|
|
289
|
+
assert.equal(picked('insideDraftRule').attrs['data-value'], 'consent');
|
|
290
|
+
// a pick is one tap: it redraws the words and keeps focus in the group
|
|
291
|
+
chip('insideDraftWho', 'nobody').fire('click');
|
|
280
292
|
assert.equal(all(box).find((n) => n.tagName === 'ins').textContent, 'no one yet');
|
|
281
293
|
assert.ok(!all(box).some((n) => n.attrs.id === 'insideDraftRule'), 'an empty building has no rule to pick');
|
|
282
294
|
assert.match(textOf(box), /nothing is decided until someone holds it/);
|
|
283
|
-
assert.equal(p.focused.el
|
|
295
|
+
assert.equal(p.focused.el, picked('insideDraftWho'));
|
|
296
|
+
assert.equal(p.focused.el.attrs['data-value'], 'nobody');
|
|
284
297
|
// a feed landing mid-sentence leaves the draft alone
|
|
285
298
|
const before = p.byId.get('insideDraftWhy');
|
|
286
299
|
before.value = 'half a thought';
|
|
@@ -894,3 +894,44 @@ test('walkthrough: a record item keeps its age, and what you did keeps its "now"
|
|
|
894
894
|
assert.deepEqual(desk.record.map((x) => x.text), ['voted yes A returned id… · now', 'The new sign-up · live · 2d', 'Mira holds the Treasurer… · 3w', 'Today it went out · today']);
|
|
895
895
|
assert.ok(desk.record.every((x) => x.text.length <= D.LIMITS.recordLen));
|
|
896
896
|
});
|
|
897
|
+
|
|
898
|
+
// ── the live walkthrough against the prototype (task 1004531) ─────────────────
|
|
899
|
+
|
|
900
|
+
const AR = () => [matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } })];
|
|
901
|
+
const settle = async () => { for (let i = 0; i < 5; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
902
|
+
|
|
903
|
+
test('walkthrough 1004531: the newest pip pops once and keeps its node, so a redraw neither replays nor cuts the pop', async () => {
|
|
904
|
+
const p = await bootPage('#building=people', AR());
|
|
905
|
+
const pips = () => p.byId.get('streetTodayPips').children;
|
|
906
|
+
const before = pips()[0];
|
|
907
|
+
p.window.GovStreetPage.draw();
|
|
908
|
+
assert.equal(pips()[0], before, 'an unchanged count keeps the meter as it stands');
|
|
909
|
+
p.part('desk-rows').children[0].children[0].children[2].children[1].fire('click');
|
|
910
|
+
await settle();
|
|
911
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
912
|
+
const fresh = pips()[0];
|
|
913
|
+
assert.notEqual(fresh, before, 'the count moved, so the pips were drawn again');
|
|
914
|
+
assert.match(fresh.className, /street-today__pip--on/);
|
|
915
|
+
assert.match(fresh.className, /desk-pop/, 'the newest pip pops');
|
|
916
|
+
p.window.GovStreetPage.draw();
|
|
917
|
+
assert.equal(pips()[0], fresh, 'the redraws after the act (the docket read again) leave the popping pip alone');
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
test('walkthrough 1004531: a decision from a street card is a decision today, as one from a desk row is', async () => {
|
|
921
|
+
const p = await bootPage('', AR());
|
|
922
|
+
p.window.GovStreetPage.openCard('access_requests:11');
|
|
923
|
+
await p.window.GovStreetPage.actOn('fine', {});
|
|
924
|
+
await settle();
|
|
925
|
+
assert.equal(p.sent.length, 1, 'the card sent its act');
|
|
926
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '1 decided');
|
|
927
|
+
assert.equal(JSON.parse(p.store.get('gov-desk-today')).n, 1, 'the day keeps its count');
|
|
928
|
+
assert.match(p.byId.get('streetTodayPips').children[0].className, /street-today__pip--on desk-pop/);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('walkthrough 1004531: a card act that records nothing takes no pip', async () => {
|
|
932
|
+
const p = await bootPage('', AR(), { refuse: true });
|
|
933
|
+
p.window.GovStreetPage.openCard('access_requests:11');
|
|
934
|
+
await p.window.GovStreetPage.actOn('fine', {});
|
|
935
|
+
await settle();
|
|
936
|
+
assert.equal(p.byId.get('streetTodayN').textContent, '0 decided', 'a refused act is not a decision');
|
|
937
|
+
});
|