@bongos/core 1.19.385 → 1.19.387
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 +22 -17
- package/docs/copy-inventory.md +79 -81
- package/docs/copy-registry.json +151 -169
- package/modules/hall-ui/public/government.css +70 -0
- package/modules/hall-ui/public/government.html +9 -2
- package/modules/hall-ui/public/government.js +175 -32
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/docs-entropy.js +11 -1
- package/src/module-api.js +5 -1
- package/tests/docs_entropy.mjs +86 -1
- package/tests/government_board_room_copy.mjs +131 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// tests/government_board_room_copy.mjs — the Board Room card reads as one
|
|
2
|
+
// decision (task 1003270, goal 1000068).
|
|
3
|
+
//
|
|
4
|
+
// The owner's report was that the room was "extremely poor and unclear": the
|
|
5
|
+
// idea being voted on was collapsed while raw config keys — `first_ratifier`,
|
|
6
|
+
// `rank:archon`, `why_matters` — were expanded above it, and the Vote button
|
|
7
|
+
// sat under a wall of prose. Copy rots back, so the two rules that came out of
|
|
8
|
+
// that pass are pinned here rather than left to the next reader's judgment:
|
|
9
|
+
//
|
|
10
|
+
// 1. NO RAW CONFIG KEY REACHES A READER. Every pass rule the server can hold
|
|
11
|
+
// has a plain sentence, checked against the REAL PASS_RULES list — a rule
|
|
12
|
+
// added server-side with no copy fails here instead of shipping a card
|
|
13
|
+
// that prints its enum. Rank keys go through OTB.rankLabel.
|
|
14
|
+
// 2. THE ORDER IS title → action → idea → apparatus. That order IS the fix;
|
|
15
|
+
// an edit that puts prose back between the title and the ballot has undone
|
|
16
|
+
// the task.
|
|
17
|
+
//
|
|
18
|
+
// Source pins, like tests/government_board_vote_ui.mjs: the hall script is an
|
|
19
|
+
// IIFE over the DOM and cannot be imported into node wholesale.
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const CLIENT = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.js'), 'utf8');
|
|
30
|
+
const HTML = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.html'), 'utf8');
|
|
31
|
+
const { PASS_RULES } = require(path.join(ROOT, 'modules', 'government', 'config.js'));
|
|
32
|
+
|
|
33
|
+
const card = () => CLIENT.slice(CLIENT.indexOf('function itemCard'), CLIENT.indexOf('async function loadBoard'));
|
|
34
|
+
|
|
35
|
+
// Deleted copy is asserted against the RENDERING source only. The comments in
|
|
36
|
+
// government.js deliberately quote what was removed and why — that history is
|
|
37
|
+
// the point of them, and a test that forbade it would push the record out of
|
|
38
|
+
// the file it explains.
|
|
39
|
+
const CODE = CLIENT.split(/\r?\n/).filter((l) => !/^\s*\/\//.test(l)).join('\n');
|
|
40
|
+
|
|
41
|
+
test('every pass rule the server can hold has a plain-language sentence', () => {
|
|
42
|
+
const block = CLIENT.slice(CLIENT.indexOf('const PASS_RULE_PLAIN'), CLIENT.indexOf('function decidedByHtml'));
|
|
43
|
+
assert.ok(PASS_RULES.length >= 3, `sanity: found the real rule set (${PASS_RULES})`);
|
|
44
|
+
for (const rule of PASS_RULES) {
|
|
45
|
+
assert.ok(block.includes(`${rule}:`), `PASS_RULE_PLAIN must explain '${rule}' — a rule with no copy renders its enum key`);
|
|
46
|
+
}
|
|
47
|
+
// And the sentence must not simply echo the key back at the reader.
|
|
48
|
+
for (const rule of PASS_RULES) {
|
|
49
|
+
const line = block.split('\n').find((l) => l.trim().startsWith(`${rule}:`));
|
|
50
|
+
assert.ok(!/'[^']*\b(first_ratifier|consent_rule|pass_rule)\b/.test(line), `'${rule}' copy leaks a config key: ${line}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the short name map covers the same rule set (the history line)', () => {
|
|
55
|
+
const block = CLIENT.slice(CLIENT.indexOf('const PASS_RULE_NAME'), CLIENT.indexOf('const PASS_RULE_PLAIN'));
|
|
56
|
+
for (const rule of PASS_RULES) {
|
|
57
|
+
assert.ok(block.includes(`${rule}:`), `PASS_RULE_NAME must name '${rule}'`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('a rank reaches the reader as a LABEL, never as the enum key', () => {
|
|
62
|
+
assert.match(CLIENT, /function rankNames\(keys\)[\s\S]*?window\.OTB\.rankLabel\(k\)/,
|
|
63
|
+
'the board card resolves rank keys through the branding pack');
|
|
64
|
+
const membership = CLIENT.slice(CLIENT.indexOf('function membershipSentence'), CLIENT.indexOf('function passRuleSentence'));
|
|
65
|
+
assert.match(membership, /window\.OTB\.rankLabel\(k\)/,
|
|
66
|
+
'so does the Constitution sentence — `archon` is an enum, "Owner" is what a person reads');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the card order is title → action → the idea → the apparatus', () => {
|
|
70
|
+
const c = card();
|
|
71
|
+
const at = (needle) => {
|
|
72
|
+
const i = c.indexOf(needle);
|
|
73
|
+
assert.notEqual(i, -1, `itemCard no longer contains ${needle}`);
|
|
74
|
+
return i;
|
|
75
|
+
};
|
|
76
|
+
assert.ok(at('gov-item__title') < at('gov-item__act'),
|
|
77
|
+
'the action follows the title directly — that adjacency is the whole fix');
|
|
78
|
+
assert.ok(at('gov-item__act') < at('ideaSectionsHtml(item)'),
|
|
79
|
+
'nothing may be inserted between the title and the ballot');
|
|
80
|
+
assert.ok(at('ideaSectionsHtml(item)') < at('decidedByHtml(item)'),
|
|
81
|
+
'the idea outranks the rules it is decided under');
|
|
82
|
+
assert.ok(at('decidedByHtml(item)') < at('ballotHtml(item, viewerId)'),
|
|
83
|
+
'who votes, then who has');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the idea renders OPEN — never behind a summary', () => {
|
|
87
|
+
const sections = CLIENT.slice(CLIENT.indexOf('function ideaSectionsHtml'), CLIENT.indexOf('function rankNames'));
|
|
88
|
+
assert.ok(!/<details/.test(sections),
|
|
89
|
+
'the substance of the vote is not a disclosure — that was the reported bug');
|
|
90
|
+
assert.match(sections, /gov-idea__body/, 'and it is set as prose, not as a config row');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the tally stays visible without a click (ADR 0175 §9)', () => {
|
|
94
|
+
const c = card();
|
|
95
|
+
assert.match(c, /gov-item__summary">\$\{tally\}/,
|
|
96
|
+
'a board whose votes are invisible constrains nobody — the count rides the summary');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the self-explaining copy is DELETED, not reworded', () => {
|
|
100
|
+
assert.ok(!/Casting arrives with the vote form/.test(CODE),
|
|
101
|
+
'the sentence that explained the UI to itself — and the form it promised has shipped');
|
|
102
|
+
assert.ok(!/tally: \$\{yes\} yes/.test(CLIENT), 'the raw-config tally line is gone');
|
|
103
|
+
assert.ok(!/The idea, in full/.test(CODE), 'so is the summary that hid the idea');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the section a voter faults is shown by NAME, in the ballot and the picker', () => {
|
|
107
|
+
const ballot = CLIENT.slice(CLIENT.indexOf('function ballotHtml'), CLIENT.indexOf('// ---- the vote form'));
|
|
108
|
+
assert.match(ballot, /SECTION_LABELS\[v\.section_key\]/,
|
|
109
|
+
'`why_matters` is a jsonb key; the objector picked a named section and the reader should see that name');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the objection still carries a reason AND one section (ADR 0175 §8)', () => {
|
|
113
|
+
// The requirement simplifying may not weaken: it is why voting lives in the
|
|
114
|
+
// hall instead of on a chat reaction.
|
|
115
|
+
const form = CLIENT.slice(CLIENT.indexOf('function voteFormHtml'), CLIENT.indexOf('async function castVoteFromRoom'));
|
|
116
|
+
assert.match(form, /data-vote-reason/, 'the reason box survives');
|
|
117
|
+
assert.match(form, /gov-vote__sections/, 'so does the five-way section picker');
|
|
118
|
+
assert.match(form, /Object\.keys\(SECTION_LABELS\)\.map/, 'with one radio per declared section');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the page frames its panels in one line each, not a paragraph', () => {
|
|
122
|
+
// The two intros used to run ~60 and ~50 words before anything actionable.
|
|
123
|
+
for (const heading of ['Constitution', 'Board Room']) {
|
|
124
|
+
const i = HTML.indexOf(`<h2 class="gov-h">${heading}</h2>`);
|
|
125
|
+
assert.notEqual(i, -1, `${heading} panel heading missing`);
|
|
126
|
+
const note = /<p class="gov-grid-note">([\s\S]*?)<\/p>/.exec(HTML.slice(i));
|
|
127
|
+
assert.ok(note, `${heading} has no framing line`);
|
|
128
|
+
const words = note[1].replace(/<[^>]+>/g, ' ').trim().split(/\s+/).length;
|
|
129
|
+
assert.ok(words <= 30, `${heading}'s intro is ${words} words — a reader meets it before anything actionable`);
|
|
130
|
+
}
|
|
131
|
+
});
|