@bongos/core 1.19.655 → 1.19.657
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 -25
- package/docs/copy-inventory.md +56 -56
- package/docs/copy-registry.json +57 -57
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/board-lib.js +165 -0
- package/modules/hall-ui/public/board-room.css +5 -16
- package/modules/hall-ui/public/board-room.html +3 -0
- package/modules/hall-ui/public/board-room.js +26 -86
- package/modules/hall-ui/public/government.css +10 -8
- package/modules/hall-ui/public/government.html +3 -0
- package/modules/hall-ui/public/government.js +31 -85
- package/modules/hall-ui/public/oversight.css +31 -0
- package/modules/lifecycle/db-rank-authz.js +30 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/government_board_room_copy.mjs +30 -14
- package/tests/government_claim_eligibility.mjs +82 -0
- package/tests/government_constitution_view.mjs +7 -2
- package/tests/rank_tier_single_source.mjs +28 -16
|
@@ -9,25 +9,14 @@
|
|
|
9
9
|
rows are the oversight family's (oversight.css); the ?v= cache-bust is
|
|
10
10
|
stamped from this file's content hash at boot (src/asset-version.js).
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stylesheet named for a different room in order to inherit them, which is what
|
|
18
|
-
the first cut of this carve did and what the review called out. The NAMES stay
|
|
12
|
+
`.gov-grid-note` AND `.gov-held-note` LIVE IN oversight.css (task 1003761).
|
|
13
|
+
They were duplicated here under a note calling the copy the cheaper of two bad
|
|
14
|
+
options, the alternative being this page loading a stylesheet named for a
|
|
15
|
+
different room. oversight.css is named for neither room and is already loaded
|
|
16
|
+
by both pages, which is the option that note did not weigh. The NAMES stay
|
|
19
17
|
`gov-*` because board-room.js emits them and
|
|
20
18
|
tests/government_board_room_copy.mjs reads them as literal markup. */
|
|
21
19
|
|
|
22
|
-
/* The framing line under a heading — the family voice, duplicated per the header
|
|
23
|
-
note above rather than inherited across pages. */
|
|
24
|
-
.gov-grid-note { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); }
|
|
25
|
-
.gov-grid-note strong { color: var(--ink); font-weight: 500; }
|
|
26
|
-
.gov-grid-note code { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere; }
|
|
27
|
-
|
|
28
|
-
/* "your vote" beside the reader's own row in the open ballot. */
|
|
29
|
-
.gov-held-note { font-size: 12.5px; color: var(--ok); white-space: nowrap; }
|
|
30
|
-
|
|
31
20
|
/* ---- the Board Room: one sitting, one panel -------------------------------------
|
|
32
21
|
The card order is task 1003270's (title, ballot, the idea, the apparatus); the
|
|
33
22
|
surface is the shell's .scroll (task 1003469). The subject of the vote is the
|
|
@@ -65,6 +65,9 @@
|
|
|
65
65
|
<script src="/builders/palette.js" defer></script>
|
|
66
66
|
<script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
|
|
67
67
|
<script src="/builders/hall-kit.js"></script>
|
|
68
|
+
<!-- task 1003761: the vocabulary this page and /government both speak. Must load
|
|
69
|
+
BEFORE board-room.js, which destructures window.OTBBoardLib at IIFE entry. -->
|
|
70
|
+
<script src="/builders/board-lib.js?v=2026-09-10-boardlib1"></script>
|
|
68
71
|
<script src="/builders/board-room.js?v=2026-09-09-board-carve"></script>
|
|
69
72
|
</body>
|
|
70
73
|
</html>
|
|
@@ -35,14 +35,23 @@
|
|
|
35
35
|
// reason and names one section. That requirement is why voting lives in the
|
|
36
36
|
// hall and not on a chat reaction.
|
|
37
37
|
//
|
|
38
|
-
// ON THE
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// government
|
|
45
|
-
//
|
|
38
|
+
// ON THE SHARED HELPERS (task 1003761). The vocabulary this page and the
|
|
39
|
+
// Constitution panel on /government BOTH speak now lives in board-lib.js, loaded
|
|
40
|
+
// before this script as window.OTBBoardLib: the ladder and `membershipRankKeys`
|
|
41
|
+
// (which mirror the SERVER's predicate grammar), `rankNames`, `fmtWindow`, the
|
|
42
|
+
// two pass-rule maps, and the `pill` / `groupHead` / `when` row pieces. The carve
|
|
43
|
+
// that split the two pages copied all of it, which is the two-copies state
|
|
44
|
+
// modules/government/CLAUDE.md forbids — "the grammar is written ONCE".
|
|
45
|
+
// tests/government_constitution_view.mjs holds the lib's parser equal to the
|
|
46
|
+
// server's. (This header used to name tests/government_board_vote_ui.mjs for
|
|
47
|
+
// that; it never asserted the parser, so the copy that lived here was pinned by
|
|
48
|
+
// nothing.)
|
|
49
|
+
//
|
|
50
|
+
// `fetchJson` / `sendJson` stay per-page: a hall page script is a self-contained
|
|
51
|
+
// IIFE over the DOM, and those two carry this page's own error copy.
|
|
52
|
+
//
|
|
53
|
+
// SECTION_LABELS and fmtRemaining stay HERE, not in the lib — this page is their
|
|
54
|
+
// only reader, and the lib is for what the two pages share.
|
|
46
55
|
//
|
|
47
56
|
// The function ORDER here is deliberate and matches the pre-carve source:
|
|
48
57
|
// tests/government_board_room_copy.mjs slices this file by function-name
|
|
@@ -57,8 +66,13 @@
|
|
|
57
66
|
// /lib/bongos-client.js. baseUrl:'' passes the full `${API}${path}` URLs below
|
|
58
67
|
// through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
59
68
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
60
|
-
const { escapeHtml
|
|
69
|
+
const { escapeHtml } = window.OTB;
|
|
61
70
|
const { emptyStateHtml } = window.OTBKit;
|
|
71
|
+
// The vocabulary /government and this page share (task 1003761). board-lib.js
|
|
72
|
+
// is loaded before this script; see the header note. The ladder and fmtInt went
|
|
73
|
+
// with it — groupHead was this page's only fmtInt reader.
|
|
74
|
+
const { membershipRankKeys, rankNames, fmtWindow,
|
|
75
|
+
PASS_RULE_PLAIN, pill, groupHead, when } = window.OTBBoardLib;
|
|
62
76
|
const toast = (msg) => window.OTB.toast(msg);
|
|
63
77
|
|
|
64
78
|
// ---- fetch helpers (mirror gate.js) --------------------------------------
|
|
@@ -98,52 +112,6 @@
|
|
|
98
112
|
};
|
|
99
113
|
const friendlyError = (code) => ERROR_COPY[code] || null;
|
|
100
114
|
|
|
101
|
-
// ---- shared row pieces ---------------------------------------------------
|
|
102
|
-
|
|
103
|
-
// A state pill in the shell's vocabulary. `mod` is one of the .fact-pill
|
|
104
|
-
// states (ok / warn / danger / info / quiet); the label is what the reader sees.
|
|
105
|
-
const pill = (mod, label) => `<span class="fact-pill fact-pill--${mod}">${escapeHtml(label)}</span>`;
|
|
106
|
-
const groupHead = (name, n, note) =>
|
|
107
|
-
`<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>` +
|
|
108
|
-
(n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`) +
|
|
109
|
-
(note ? `<span class="ov-group__note">${escapeHtml(note)}</span>` : '') +
|
|
110
|
-
'</div>';
|
|
111
|
-
// A timestamp the way the rows have always shown one: "2026-08-30 14:05".
|
|
112
|
-
const when = (iso) => escapeHtml(String(iso || '').slice(0, 16).replace('T', ' '));
|
|
113
|
-
|
|
114
|
-
// The predicate grammar, in plain language (task 1003094). This MIRRORS
|
|
115
|
-
// modules/government/membership-predicate.js — browser code cannot import the
|
|
116
|
-
// server module, the established pattern here (the five section labels do the
|
|
117
|
-
// same) — and tests/government_board_vote_ui.mjs holds the two equal.
|
|
118
|
-
//
|
|
119
|
-
// Rendering the raw predicate was fine while the only form was `rank:<key>`.
|
|
120
|
-
// It is not fine now: `rank:metic+` would read as "builders holding the metic+
|
|
121
|
-
// rank", which names a rank that does not exist and hides the very thing the
|
|
122
|
-
// suffix says.
|
|
123
|
-
const STANDARD_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
|
|
124
|
-
|
|
125
|
-
function membershipRankKeys(predicate) {
|
|
126
|
-
const m = String(predicate || '');
|
|
127
|
-
if (!m.startsWith('rank:')) return null;
|
|
128
|
-
const body = m.slice(5);
|
|
129
|
-
if (!body) return null;
|
|
130
|
-
const keys = [];
|
|
131
|
-
for (const element of body.split(',')) {
|
|
132
|
-
const match = /^([a-z][a-z0-9_-]*)(\+?)$/.exec(element);
|
|
133
|
-
if (!match) return null;
|
|
134
|
-
let expanded;
|
|
135
|
-
if (match[2] === '+') {
|
|
136
|
-
const i = STANDARD_LADDER.indexOf(match[1]);
|
|
137
|
-
if (i === -1) return null; // "and above" is undefined off the ladder
|
|
138
|
-
expanded = STANDARD_LADDER.slice(i);
|
|
139
|
-
} else {
|
|
140
|
-
expanded = [match[1]];
|
|
141
|
-
}
|
|
142
|
-
for (const k of expanded) if (keys.indexOf(k) === -1) keys.push(k);
|
|
143
|
-
}
|
|
144
|
-
return keys.length ? keys : null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
115
|
// ---- the Board Room panel (R16) --------------------------------------------
|
|
148
116
|
|
|
149
117
|
// The room renders what the server assembled: open items with the database's
|
|
@@ -257,37 +225,9 @@
|
|
|
257
225
|
//
|
|
258
226
|
// Read from the item's SNAPSHOT constitution (the rules in force when the
|
|
259
227
|
// window opened), never today's config — the same rules the server tallies
|
|
260
|
-
// under.
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// actionable fact and hiding it would help nobody.
|
|
264
|
-
function rankNames(keys) {
|
|
265
|
-
const labels = keys.map((k) => escapeHtml(window.OTB.rankLabel(k)));
|
|
266
|
-
if (labels.length === 1) return labels[0];
|
|
267
|
-
return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// A sitting length in the unit a person would say it in. The config stores
|
|
271
|
-
// minutes; "2880 minutes" is a number nobody converts in their head.
|
|
272
|
-
//
|
|
273
|
-
// DUPLICATED, KNOWINGLY: government.js keeps its own copy for the Constitution
|
|
274
|
-
// panel's windowSentence. Two readers, two files, one wording — if you change
|
|
275
|
-
// the phrasing here, change it there.
|
|
276
|
-
function fmtWindow(minutes) {
|
|
277
|
-
const m = Number(minutes);
|
|
278
|
-
if (!Number.isFinite(m) || m <= 0) return `${escapeHtml(String(minutes))} minutes`;
|
|
279
|
-
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
280
|
-
if (m % 1440 === 0) return plural(m / 1440, 'day');
|
|
281
|
-
if (m % 60 === 0) return plural(m / 60, 'hour');
|
|
282
|
-
return plural(m, 'minute');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const PASS_RULE_PLAIN = {
|
|
286
|
-
first_ratifier: 'The first vote decides it: a yes ratifies, an objection returns it to its author.',
|
|
287
|
-
consent: 'It passes unless someone objects — silence is agreement, and one reasoned objection returns it.',
|
|
288
|
-
majority: 'It passes only when more than half the board votes yes. Silence is not agreement here.',
|
|
289
|
-
unanimous: 'Everyone who votes must vote yes, and at least one member must. One objection sends it back for revision.',
|
|
290
|
-
};
|
|
228
|
+
// under. `rankNames`, `fmtWindow` and `PASS_RULE_PLAIN` come from board-lib.js
|
|
229
|
+
// (task 1003761) — the Constitution panel on /government says the same three
|
|
230
|
+
// things and used to say them from its own copies.
|
|
291
231
|
|
|
292
232
|
function decidedByHtml(item) {
|
|
293
233
|
const c = item.constitution || {};
|
|
@@ -23,11 +23,9 @@
|
|
|
23
23
|
color: var(--ink);
|
|
24
24
|
text-shadow: var(--type-halo);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere;
|
|
30
|
-
}
|
|
26
|
+
/* `.gov-grid-note` (and the `.ov-*` code rule it shared a declaration with) moved
|
|
27
|
+
to oversight.css in task 1003761 — /board-room renders it too and had a copy.
|
|
28
|
+
`.gov-h` stays: this page is its only renderer. */
|
|
31
29
|
|
|
32
30
|
/* Holds the ladder-rank reset button. Empty for every other rank, so it collapses
|
|
33
31
|
rather than reserving a gap — hence no margin on the empty box itself. */
|
|
@@ -57,9 +55,13 @@
|
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/* ---- the Board Room: MOVED OUT to board-room.css (ADR 0266) ----------------
|
|
60
|
-
The sitting card
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
The sitting card and the ballot controls went with the room when it became its
|
|
59
|
+
own page, and nothing here renders them.
|
|
60
|
+
CORRECTION (task 1003761): `.gov-held-note` went with them under the same
|
|
61
|
+
"nothing on THIS page renders them" reasoning, and that was wrong —
|
|
62
|
+
renderRankList marks a held Archon rank with one, so it rendered unstyled here
|
|
63
|
+
from the carve until this task. It lives in oversight.css now, which both
|
|
64
|
+
pages load. */
|
|
63
65
|
|
|
64
66
|
/* ---- the charter library ------------------------------------------------------- */
|
|
65
67
|
/* The configuration line under a charter: what an owner would type, in the
|
|
@@ -139,6 +139,9 @@
|
|
|
139
139
|
<script src="/builders/palette.js" defer></script>
|
|
140
140
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
141
141
|
<script src="/builders/hall-kit.js"></script>
|
|
142
|
+
<!-- task 1003761: the vocabulary this page and /board-room both speak. Must load
|
|
143
|
+
BEFORE government.js, which destructures window.OTBBoardLib at IIFE entry. -->
|
|
144
|
+
<script src="/builders/board-lib.js?v=2026-09-10-boardlib1"></script>
|
|
142
145
|
<script src="/builders/government.js?v=2026-09-02-gov-rows"></script>
|
|
143
146
|
</body>
|
|
144
147
|
</html>
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
80
80
|
const { escapeHtml, fmtInt } = window.OTB;
|
|
81
81
|
const { emptyStateHtml } = window.OTBKit;
|
|
82
|
+
// The vocabulary this page and the Board Room share (task 1003761).
|
|
83
|
+
// board-lib.js is loaded before this script.
|
|
84
|
+
const { STANDARD_LADDER, membershipRankKeys, rankNames, fmtWindow,
|
|
85
|
+
PASS_RULE_NAME, pill, groupHead, when } = window.OTBBoardLib;
|
|
82
86
|
const toast = (msg) => window.OTB.toast(msg);
|
|
83
87
|
|
|
84
88
|
// ---- fetch helpers (mirror gate.js) --------------------------------------
|
|
@@ -137,19 +141,6 @@
|
|
|
137
141
|
|
|
138
142
|
const selectedRank = () => ranks.find((r) => r.rank_key === selectedRankKey) || null;
|
|
139
143
|
|
|
140
|
-
// ---- shared row pieces ---------------------------------------------------
|
|
141
|
-
|
|
142
|
-
// A state pill in the shell's vocabulary. `mod` is one of the .fact-pill
|
|
143
|
-
// states (ok / warn / danger / info / quiet); the label is what the reader sees.
|
|
144
|
-
const pill = (mod, label) => `<span class="fact-pill fact-pill--${mod}">${escapeHtml(label)}</span>`;
|
|
145
|
-
const groupHead = (name, n, note) =>
|
|
146
|
-
`<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(name)}</span>` +
|
|
147
|
-
(n === undefined ? '' : `<span class="ov-group__n">${fmtInt(n)}</span>`) +
|
|
148
|
-
(note ? `<span class="ov-group__note">${escapeHtml(note)}</span>` : '') +
|
|
149
|
-
'</div>';
|
|
150
|
-
// A timestamp the way the rows have always shown one: "2026-08-30 14:05".
|
|
151
|
-
const when = (iso) => escapeHtml(String(iso || '').slice(0, 16).replace('T', ' '));
|
|
152
|
-
|
|
153
144
|
// ---- render: rank list ---------------------------------------------------
|
|
154
145
|
|
|
155
146
|
// One rank, one selectable oversight row: the label, the DESCRIPTION the
|
|
@@ -539,39 +530,12 @@
|
|
|
539
530
|
// switch can never be flipped quietly, and every change is auditable against
|
|
540
531
|
// the constitution of its day.
|
|
541
532
|
|
|
542
|
-
// The predicate grammar
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
// It is not fine now: `rank:metic+` would read as "builders holding the metic+
|
|
549
|
-
// rank", which names a rank that does not exist and hides the very thing the
|
|
550
|
-
// suffix says. A constitution panel that misdescribes who governs is the
|
|
551
|
-
// failure this whole task exists to fix, one layer up.
|
|
552
|
-
const STANDARD_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
|
|
553
|
-
|
|
554
|
-
function membershipRankKeys(predicate) {
|
|
555
|
-
const m = String(predicate || '');
|
|
556
|
-
if (!m.startsWith('rank:')) return null;
|
|
557
|
-
const body = m.slice(5);
|
|
558
|
-
if (!body) return null;
|
|
559
|
-
const keys = [];
|
|
560
|
-
for (const element of body.split(',')) {
|
|
561
|
-
const match = /^([a-z][a-z0-9_-]*)(\+?)$/.exec(element);
|
|
562
|
-
if (!match) return null;
|
|
563
|
-
let expanded;
|
|
564
|
-
if (match[2] === '+') {
|
|
565
|
-
const i = STANDARD_LADDER.indexOf(match[1]);
|
|
566
|
-
if (i === -1) return null; // "and above" is undefined off the ladder
|
|
567
|
-
expanded = STANDARD_LADDER.slice(i);
|
|
568
|
-
} else {
|
|
569
|
-
expanded = [match[1]];
|
|
570
|
-
}
|
|
571
|
-
for (const k of expanded) if (keys.indexOf(k) === -1) keys.push(k);
|
|
572
|
-
}
|
|
573
|
-
return keys.length ? keys : null;
|
|
574
|
-
}
|
|
533
|
+
// The predicate grammar and the ladder it expands against come from
|
|
534
|
+
// board-lib.js (task 1003761). They MIRROR modules/government/
|
|
535
|
+
// membership-predicate.js — browser code cannot import the server module —
|
|
536
|
+
// and the carve that took the Board Room onto its own page had left a second
|
|
537
|
+
// copy here. A constitution panel that misdescribes who governs is the failure
|
|
538
|
+
// this whole area exists to fix, and two copies is how it gets there.
|
|
575
539
|
|
|
576
540
|
function membershipSentence(board) {
|
|
577
541
|
const raw = String(board.membership || '');
|
|
@@ -682,18 +646,10 @@
|
|
|
682
646
|
close_early_on_full_turnout: 'Closing early on full turnout',
|
|
683
647
|
};
|
|
684
648
|
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
// landed a new reader on the Constitution side while the room was moving out.
|
|
690
|
-
// Its only caller is fieldValueHtml; membershipSentence above spells its own
|
|
691
|
-
// labels inline, so do not route that through here expecting one wording.
|
|
692
|
-
function rankNames(keys) {
|
|
693
|
-
const labels = keys.map((k) => escapeHtml(window.OTB.rankLabel(k)));
|
|
694
|
-
if (labels.length === 1) return labels[0];
|
|
695
|
-
return `${labels.slice(0, -1).join(', ')} or ${labels[labels.length - 1]}`;
|
|
696
|
-
}
|
|
649
|
+
// `rankNames` comes from board-lib.js (task 1003761). Its only caller here is
|
|
650
|
+
// fieldValueHtml; membershipSentence above spells its own labels inline because
|
|
651
|
+
// it wraps each in <strong>, so do not route that through it expecting one
|
|
652
|
+
// wording.
|
|
697
653
|
|
|
698
654
|
// One constitution VALUE, in the words a person would use — the same rule the
|
|
699
655
|
// dials keep, applied to the two halves of a divergence. Without this the
|
|
@@ -716,32 +672,11 @@
|
|
|
716
672
|
|
|
717
673
|
// One dial, one row: the dial's name as the title, the live sentence as the
|
|
718
674
|
// sub line. The sentence is trusted HTML from the helpers above.
|
|
719
|
-
// A sitting length in the unit a person would say it in. The config stores
|
|
720
|
-
// minutes; "2880 minutes" is a number nobody converts in their head.
|
|
721
675
|
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
function fmtWindow(minutes) {
|
|
727
|
-
const m = Number(minutes);
|
|
728
|
-
if (!Number.isFinite(m) || m <= 0) return `${escapeHtml(String(minutes))} minutes`;
|
|
729
|
-
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
730
|
-
if (m % 1440 === 0) return plural(m / 1440, 'day');
|
|
731
|
-
if (m % 60 === 0) return plural(m / 60, 'hour');
|
|
732
|
-
return plural(m, 'minute');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// The short name of a pass rule, for a place a whole sentence will not fit —
|
|
736
|
-
// the change-history line below. Also carved-region residue (ADR 0266): the
|
|
737
|
-
// sentence-length twin PASS_RULE_PLAIN went to board-room.js with the card,
|
|
738
|
-
// this one stayed with its only reader, historyRow.
|
|
739
|
-
const PASS_RULE_NAME = {
|
|
740
|
-
first_ratifier: 'by first ratifier',
|
|
741
|
-
consent: 'by consent',
|
|
742
|
-
majority: 'by majority',
|
|
743
|
-
unanimous: 'by unanimous agreement',
|
|
744
|
-
};
|
|
676
|
+
// `fmtWindow` and `PASS_RULE_NAME` come from board-lib.js (task 1003761).
|
|
677
|
+
// fmtWindow had a twin in board-room.js under a "keep the phrasing in step by
|
|
678
|
+
// hand" note; PASS_RULE_NAME sits beside its sentence-length twin
|
|
679
|
+
// PASS_RULE_PLAIN there, so the two renderings of one pass rule cannot drift.
|
|
745
680
|
|
|
746
681
|
function dialRow(name, sentenceHtml) {
|
|
747
682
|
return `<li class="ov-row"><div class="ov-row__main"><span class="ov-row__title">${escapeHtml(name)}</span><p class="ov-row__sub">${sentenceHtml}</p></div></li>`;
|
|
@@ -794,6 +729,14 @@
|
|
|
794
729
|
// `predicate: null` means the predicate is the owner's to choose (Foundation
|
|
795
730
|
// names their own custom rank), which is honest rather than unchecked — the
|
|
796
731
|
// test asserts such a charter says so in its `how`.
|
|
732
|
+
//
|
|
733
|
+
// A SECOND test pins these too, for a different reason. The `rank:xenos+` row
|
|
734
|
+
// spells the full ladder out in `seatsRanks`, and that literal is why this file
|
|
735
|
+
// stays in tests/rank_tier_single_source.mjs's STATIC_LADDER_FILES even though
|
|
736
|
+
// task 1003761 moved the page's real ladder to board-lib.js. It is documentation
|
|
737
|
+
// of the grammar rather than a second implementation of it — but a worked
|
|
738
|
+
// example that drifts teaches a ladder nobody has, so it is held to the
|
|
739
|
+
// canonical order.
|
|
797
740
|
const CHARTERS = [
|
|
798
741
|
{ key: 'monarchy', name: 'Monarchy / BDFL', selectable: true,
|
|
799
742
|
predicate: 'rank:archon', seatsRanks: ['archon'],
|
|
@@ -919,8 +862,11 @@
|
|
|
919
862
|
// What stayed behind, and why, so neither reads as a leftover:
|
|
920
863
|
// • fmtWindow — windowSentence (the Constitution's clock dial) reads it.
|
|
921
864
|
// • PASS_RULE_NAME — historyRow (the amendment history line) reads it.
|
|
922
|
-
// Both are
|
|
923
|
-
//
|
|
865
|
+
// Both READERS are still here; the two helpers themselves are not. Task 1003761
|
|
866
|
+
// hoisted them into board-lib.js, which this page destructures at IIFE entry.
|
|
867
|
+
// This block used to end "both are duplicated in board-room.js rather than
|
|
868
|
+
// shared; a hall page script is a self-contained IIFE, which is the convention
|
|
869
|
+
// here" — that was the justification for the copies, and it is retired.
|
|
924
870
|
//
|
|
925
871
|
// The old deep link is redirected below in init() — a fragment never reaches
|
|
926
872
|
// the server, so #board-room could not be caught by a route.
|
|
@@ -178,6 +178,37 @@ a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); t
|
|
|
178
178
|
.ov-danger { color: var(--danger); }
|
|
179
179
|
.ov-ok { color: var(--ok); }
|
|
180
180
|
|
|
181
|
+
/* ---- the government family's two shared lines (task 1003761) -----------------
|
|
182
|
+
Both are rendered by /government AND /board-room, so they belong to the family
|
|
183
|
+
sheet both pages load rather than to either page's own. ADR 0266's carve had
|
|
184
|
+
put them elsewhere, and each half of that was wrong in its own way:
|
|
185
|
+
|
|
186
|
+
`.gov-grid-note` was COPIED into board-room.css under a header note calling
|
|
187
|
+
the duplication the cheaper of two bad options — the alternative it weighed
|
|
188
|
+
was one page loading a sheet named for the other page's room. This sheet is
|
|
189
|
+
named for neither room, which is the option that was missing.
|
|
190
|
+
|
|
191
|
+
`.gov-held-note` was MOVED to board-room.css under a note in government.css
|
|
192
|
+
saying "nothing on THIS page renders them". government.js does: it marks a
|
|
193
|
+
held Archon rank with one (renderRankList). So that line has rendered unstyled
|
|
194
|
+
on /government since the carve — a live defect, not a tidiness point.
|
|
195
|
+
|
|
196
|
+
The NAMES stay `gov-*`: board-room.js and government.js emit them, and
|
|
197
|
+
tests/government_board_room_copy.mjs reads `.gov-grid-note` as literal markup.
|
|
198
|
+
The `.ov-*` selectors on the code rule came with it from government.css, where
|
|
199
|
+
they were already sharing the declaration. */
|
|
200
|
+
|
|
201
|
+
/* The framing line under a heading — the family's voice. */
|
|
202
|
+
.gov-grid-note { margin: 0 0 14px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); }
|
|
203
|
+
.gov-grid-note strong { color: var(--ink); font-weight: 500; }
|
|
204
|
+
.gov-grid-note code, .ov-row__sub code, .ov-row__title code, .ov-foot code {
|
|
205
|
+
font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); overflow-wrap: anywhere;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* "your vote" beside the reader's own row in the open ballot (/board-room), and
|
|
209
|
+
"held" beside an Archon-held rank (/government). */
|
|
210
|
+
.gov-held-note { font-size: 12.5px; color: var(--ok); white-space: nowrap; }
|
|
211
|
+
|
|
181
212
|
/* Detail lines under a row: a reason per line (the gate's holds), or a key and
|
|
182
213
|
a value (the harbor's make and cost). Hairlines between, none on the first. */
|
|
183
214
|
.ov-lines { list-style: none; margin: 10px 0 0; padding: 0; max-width: 72ch; }
|
|
@@ -80,15 +80,33 @@ function xenosClaimAllowed(rank, newcomerFriendly) {
|
|
|
80
80
|
|
|
81
81
|
// claimPermissionsFor — the builder's effective permission set for the claim
|
|
82
82
|
// decision, read through the `governance` kernel PORT (never by importing the
|
|
83
|
-
// module — the one-way rule, ADR 0083).
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// • the
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
83
|
+
// module — the one-way rule, ADR 0083).
|
|
84
|
+
//
|
|
85
|
+
// NULL means authority could not be ESTABLISHED, which claimEligibilityAllows
|
|
86
|
+
// reads as "fall back to the incumbent rank rule". Exactly two things mean that:
|
|
87
|
+
// • the governance module is not mounted (no port), so there is no authority
|
|
88
|
+
// system to ask;
|
|
89
|
+
// • the resolver threw, so the answer is unknown rather than empty.
|
|
90
|
+
//
|
|
91
|
+
// AN EMPTY SET IS AN ANSWER, NOT A FAILURE (task 1003816). It used to be folded
|
|
92
|
+
// into the null above, on the stated grounds that governance_002's backfill was
|
|
93
|
+
// run-once and nothing re-assigned a rank-role afterwards — so a builder holding
|
|
94
|
+
// no role row was a live gap rather than a real verdict. That is no longer true,
|
|
95
|
+
// and the three writes that closed it are all shipped:
|
|
96
|
+
// • BV1.R95b rotates the role inside the rank-change transaction, and syncs it
|
|
97
|
+
// on every sign-in (src/bongos/auth-admission.js safeSyncRankRole);
|
|
98
|
+
// • task 1003193 rotates it in-tx on offboard AND reactivation;
|
|
99
|
+
// • governance_005 repaired the rows already written.
|
|
100
|
+
// governance_005 states the intended reading in as many words — a builder with no
|
|
101
|
+
// role row "resolves to the empty set (denied everywhere)" — and
|
|
102
|
+
// syncGovernanceRankRole calls that same outcome "the fail-closed answer". Folding
|
|
103
|
+
// it back into the rank rule is what stopped those three from being true at the
|
|
104
|
+
// claim gate, so the empty set now flows through as a real deny.
|
|
105
|
+
//
|
|
106
|
+
// The residual way to hold no role row is the sign-in sync failing transiently
|
|
107
|
+
// (it is deliberately best-effort so a governance hiccup cannot block a sign-in).
|
|
108
|
+
// Denying the claim is the correct read of that state, and it self-repairs on the
|
|
109
|
+
// next successful sign-in.
|
|
92
110
|
//
|
|
93
111
|
// Called BEFORE the claim transaction opens: authority is a per-request read (ADR
|
|
94
112
|
// 0016 — resolved server-side, uncached), and taking a second pool connection while
|
|
@@ -98,7 +116,9 @@ async function claimPermissionsFor(builderId) {
|
|
|
98
116
|
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
99
117
|
try {
|
|
100
118
|
const keys = await governance.resolveBuilderPermissions(builderId);
|
|
101
|
-
|
|
119
|
+
// A non-array is a broken port contract, not a verdict — resolveBuilderPermissions
|
|
120
|
+
// always returns an array. Treat it as "could not establish", never as "denied".
|
|
121
|
+
if (!Array.isArray(keys)) return null;
|
|
102
122
|
return new Set(keys);
|
|
103
123
|
} catch (err) {
|
|
104
124
|
console.error('[gds] claim eligibility: permission resolve failed, falling back to the rank rule', err);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.657",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.657",
|
|
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.19.
|
|
3
|
+
"version": "1.19.657",
|
|
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/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.657'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -36,6 +36,10 @@ const CLIENT = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', '
|
|
|
36
36
|
const HTML = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.html'), 'utf8');
|
|
37
37
|
const BOARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-room.js'), 'utf8');
|
|
38
38
|
const BOARD_HTML = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-room.html'), 'utf8');
|
|
39
|
+
// The vocabulary both pages speak (task 1003761). The pass-rule maps and
|
|
40
|
+
// rankNames were one copy per page until the hoist; the assertions that read
|
|
41
|
+
// them come here now, and there is one place to read rather than two.
|
|
42
|
+
const LIB = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-lib.js'), 'utf8');
|
|
39
43
|
const { PASS_RULES } = require(path.join(ROOT, 'modules', 'government', 'config.js'));
|
|
40
44
|
|
|
41
45
|
const card = () => BOARD.slice(BOARD.indexOf('function itemCard'), BOARD.indexOf('async function loadBoard'));
|
|
@@ -47,7 +51,13 @@ const card = () => BOARD.slice(BOARD.indexOf('function itemCard'), BOARD.indexOf
|
|
|
47
51
|
const CODE = BOARD.split(/\r?\n/).filter((l) => !/^\s*\/\//.test(l)).join('\n');
|
|
48
52
|
|
|
49
53
|
test('every pass rule the server can hold has a plain-language sentence', () => {
|
|
50
|
-
|
|
54
|
+
// In board-lib.js since task 1003761. Both landmarks are asserted before the
|
|
55
|
+
// slice for the reason the next test spells out: an indexOf that misses
|
|
56
|
+
// returns -1, and slice(start, -1) would hand this most of the file.
|
|
57
|
+
const start = LIB.indexOf('const PASS_RULE_PLAIN');
|
|
58
|
+
const end = LIB.indexOf('shared row pieces');
|
|
59
|
+
assert.ok(start !== -1 && end > start, 'PASS_RULE_PLAIN sits above the row pieces in board-lib.js');
|
|
60
|
+
const block = LIB.slice(start, end);
|
|
51
61
|
assert.ok(PASS_RULES.length >= 3, `sanity: found the real rule set (${PASS_RULES})`);
|
|
52
62
|
for (const rule of PASS_RULES) {
|
|
53
63
|
assert.ok(block.includes(`${rule}:`), `PASS_RULE_PLAIN must explain '${rule}' — a rule with no copy renders its enum key`);
|
|
@@ -60,24 +70,24 @@ test('every pass rule the server can hold has a plain-language sentence', () =>
|
|
|
60
70
|
});
|
|
61
71
|
|
|
62
72
|
test('the short name map covers the same rule set (the history line)', () => {
|
|
63
|
-
// Its reader (historyRow, the amendment change-history)
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
const start =
|
|
70
|
-
const end =
|
|
71
|
-
assert.ok(start !== -1 && end > start, 'PASS_RULE_NAME sits above
|
|
72
|
-
const block =
|
|
73
|
+
// Its reader (historyRow, the amendment change-history) is on the government
|
|
74
|
+
// page; the map itself joined its sentence-length twin in board-lib.js in task
|
|
75
|
+
// 1003761, so the two renderings of one pass rule sit adjacent and cannot
|
|
76
|
+
// describe the same rule differently. The landmark check stays and matters:
|
|
77
|
+
// an indexOf that misses returns -1, and `slice(start, -1)` would hand this
|
|
78
|
+
// assertion most of the file and pass for the wrong reason.
|
|
79
|
+
const start = LIB.indexOf('const PASS_RULE_NAME');
|
|
80
|
+
const end = LIB.indexOf('const PASS_RULE_PLAIN');
|
|
81
|
+
assert.ok(start !== -1 && end > start, 'PASS_RULE_NAME sits above PASS_RULE_PLAIN in board-lib.js');
|
|
82
|
+
const block = LIB.slice(start, end);
|
|
73
83
|
for (const rule of PASS_RULES) {
|
|
74
84
|
assert.ok(block.includes(`${rule}:`), `PASS_RULE_NAME must name '${rule}'`);
|
|
75
85
|
}
|
|
76
86
|
});
|
|
77
87
|
|
|
78
88
|
test('a rank reaches the reader as a LABEL, never as the enum key', () => {
|
|
79
|
-
assert.match(
|
|
80
|
-
'the
|
|
89
|
+
assert.match(LIB, /function rankNames\(keys\)[\s\S]*?window\.OTB\.rankLabel\(k\)/,
|
|
90
|
+
'the shared vocabulary resolves rank keys through the branding pack');
|
|
81
91
|
const membership = CLIENT.slice(CLIENT.indexOf('function membershipSentence'), CLIENT.indexOf('function passRuleSentence'));
|
|
82
92
|
assert.match(membership, /window\.OTB\.rankLabel\(k\)/,
|
|
83
93
|
'so does the Constitution sentence — `archon` is an enum, "Owner" is what a person reads');
|
|
@@ -101,7 +111,13 @@ test('the card order is title → action → the idea → the apparatus', () =>
|
|
|
101
111
|
});
|
|
102
112
|
|
|
103
113
|
test('the idea renders OPEN — never behind a summary', () => {
|
|
104
|
-
|
|
114
|
+
// Ends at decidedByHtml, the declaration that now follows: rankNames was the
|
|
115
|
+
// old landmark and moved to board-lib.js in task 1003761. Asserted, not
|
|
116
|
+
// assumed — a missing landmark is -1 and would silently invert this test.
|
|
117
|
+
const from = BOARD.indexOf('function ideaSectionsHtml');
|
|
118
|
+
const to = BOARD.indexOf('function decidedByHtml');
|
|
119
|
+
assert.ok(from !== -1 && to > from, 'ideaSectionsHtml sits above decidedByHtml in board-room.js');
|
|
120
|
+
const sections = BOARD.slice(from, to);
|
|
105
121
|
assert.ok(!/<details/.test(sections),
|
|
106
122
|
'the substance of the vote is not a disclosure — that was the reported bug');
|
|
107
123
|
assert.match(sections, /gov-idea__body/, 'and it is set as prose, not as a config row');
|