@bongos/core 1.19.250 → 1.19.252
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 +100 -30
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0179-board-membership-predicate-grammar.md +113 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +1432 -0
- package/docs/copy-registry.json +13311 -0
- package/modules/copy-desk/CLAUDE.md +15 -0
- package/modules/copy-desk/flags.js +245 -0
- package/modules/copy-desk/migrations/copy_desk_001_flags.sql +128 -0
- package/modules/copy-desk/module.json +11 -0
- package/modules/copy-desk/registry.js +317 -0
- package/modules/copy-desk/routes/copy-desk.js +272 -0
- package/modules/copy-desk/tests/copy_flags.mjs +246 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +104 -0
- package/modules/copy-desk/tests/copy_queue.mjs +257 -0
- package/modules/government/board-membership.js +20 -20
- package/modules/government/board.js +57 -3
- package/modules/government/config.js +14 -8
- package/modules/government/db.js +18 -7
- package/modules/government/membership-predicate.js +115 -0
- package/modules/government/routes/government.js +19 -0
- package/modules/hall-ui/public/government.js +54 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/publish-manifest.js +9 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +5 -1
- package/tests/government_board_amendment.mjs +87 -1
- package/tests/government_board_membership.mjs +101 -10
- package/tests/government_board_membership_db.mjs +150 -0
- package/tests/government_board_vote.mjs +5 -3
- package/tests/government_constitution_view.mjs +53 -0
- package/tests/publish_manifest.mjs +82 -0
- package/tests/rank_tier_single_source.mjs +9 -0
|
@@ -79,6 +79,10 @@ test('a proposal files the amendment and opens its window under the constitution
|
|
|
79
79
|
await withDb({
|
|
80
80
|
createAmendment: async (args) => ({ id: '5', proposed: args.proposed, rationale_md: args.rationaleMd, proposed_by: args.proposedBy }),
|
|
81
81
|
openBoardItem: async (args) => { opened = args; return { id: '21', ...args }; },
|
|
82
|
+
// Task 1003094's disenfranchisement guard asks, before any write, whether the
|
|
83
|
+
// proposer would still sit under the PROPOSED block. Here they would (the
|
|
84
|
+
// proposal keeps rank:archon), so the propose proceeds.
|
|
85
|
+
builderHoldsRankActive: async () => true,
|
|
82
86
|
}, async () => {
|
|
83
87
|
const out = await proposeAmendment({ proposal: CONSENT_90, rationaleMd: 'time to open up', proposedBy: '42' });
|
|
84
88
|
assert.equal(out.ok, true);
|
|
@@ -95,12 +99,94 @@ test('an invalid proposal files NOTHING — refused before any write', async ()
|
|
|
95
99
|
await withDb({
|
|
96
100
|
createAmendment: async () => { throw new Error('must not be reached'); },
|
|
97
101
|
openBoardItem: async () => { throw new Error('must not be reached'); },
|
|
102
|
+
builderHoldsRankActive: async () => true,
|
|
98
103
|
}, async () => {
|
|
99
104
|
assert.equal((await proposeAmendment({ proposal: { ...CONSENT_90, pass_rule: 'majority' }, proposedBy: '42' })).ok, false);
|
|
100
105
|
assert.equal((await proposeAmendment({ proposal: CONSENT_90, proposedBy: null })).reason, 'missing_proposer');
|
|
101
106
|
});
|
|
102
107
|
});
|
|
103
108
|
|
|
109
|
+
// ── the disenfranchisement guard (task 1003094) ──────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// The shape that cannot self-correct: an amendment that removes the person
|
|
112
|
+
// making it. Once it lands they can no longer vote — including to undo it. This
|
|
113
|
+
// is not hypothetical. On the R25 rehearsal an Archon ratified `rank:metic`
|
|
114
|
+
// under monarchy, the apply worked perfectly, and the next vote came back 403
|
|
115
|
+
// not_board_member. The board would have deadlocked at one member.
|
|
116
|
+
|
|
117
|
+
test('a proposal that would unseat its own proposer is REFUSED before any write', async () => {
|
|
118
|
+
await withDb({
|
|
119
|
+
createAmendment: async () => { throw new Error('must not be reached — nothing may be written'); },
|
|
120
|
+
openBoardItem: async () => { throw new Error('must not be reached'); },
|
|
121
|
+
// The proposer is an Archon; the proposed block seats metics ONLY.
|
|
122
|
+
builderHoldsRankActive: async (_id, rankKeys) => rankKeys.includes('archon'),
|
|
123
|
+
}, async () => {
|
|
124
|
+
const out = await proposeAmendment({
|
|
125
|
+
proposal: { ...CONSENT_90, membership: 'rank:metic' },
|
|
126
|
+
proposedBy: '42',
|
|
127
|
+
});
|
|
128
|
+
assert.equal(out.ok, false);
|
|
129
|
+
assert.equal(out.reason, 'proposer_would_lose_their_seat');
|
|
130
|
+
assert.match(out.what_this_means, /would remove YOU from the board/);
|
|
131
|
+
assert.match(out.hint, /rank:<key>\+/, 'the refusal names the one-character fix');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('the guard is satisfied by the LADDER form — the fix the hint points at', async () => {
|
|
136
|
+
let wrote = false;
|
|
137
|
+
await withDb({
|
|
138
|
+
createAmendment: async (args) => { wrote = true; return { id: '9', proposed: args.proposed }; },
|
|
139
|
+
openBoardItem: async (args) => ({ id: '31', ...args }),
|
|
140
|
+
builderHoldsRankActive: async (_id, rankKeys) => rankKeys.includes('archon'),
|
|
141
|
+
}, async () => {
|
|
142
|
+
const out = await proposeAmendment({
|
|
143
|
+
proposal: { ...CONSENT_90, membership: 'rank:metic+' },
|
|
144
|
+
proposedBy: '42',
|
|
145
|
+
});
|
|
146
|
+
assert.equal(out.ok, true, 'metic-and-above keeps the Archon seated, so the proposal proceeds');
|
|
147
|
+
assert.equal(wrote, true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('stepping back is still expressible — with an explicit acknowledgement', async () => {
|
|
152
|
+
// Deliberately not a hard ban: handing the franchise to a wider body and
|
|
153
|
+
// stepping back is a legitimate constitutional act, and a system that cannot
|
|
154
|
+
// express it is telling owners what they may decide.
|
|
155
|
+
let wrote = false;
|
|
156
|
+
await withDb({
|
|
157
|
+
createAmendment: async (args) => { wrote = true; return { id: '10', proposed: args.proposed }; },
|
|
158
|
+
openBoardItem: async (args) => ({ id: '32', ...args }),
|
|
159
|
+
builderHoldsRankActive: async () => { throw new Error('the guard must not even ask when acknowledged'); },
|
|
160
|
+
}, async () => {
|
|
161
|
+
const out = await proposeAmendment({
|
|
162
|
+
proposal: { ...CONSENT_90, membership: 'rank:metic' },
|
|
163
|
+
proposedBy: '42',
|
|
164
|
+
acknowledgeSelfRemoval: true,
|
|
165
|
+
});
|
|
166
|
+
assert.equal(out.ok, true);
|
|
167
|
+
assert.equal(wrote, true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the acknowledgement never becomes part of the ratified text', async () => {
|
|
172
|
+
// It is a separate parameter, not a proposal field — and the strict validator
|
|
173
|
+
// refuses unknown fields, which is what keeps that honest.
|
|
174
|
+
const v = validateProposedConstitution({ ...CONSENT_90, acknowledge_self_removal: true });
|
|
175
|
+
assert.equal(v.ok, false);
|
|
176
|
+
assert.equal(v.reason, 'unknown_field');
|
|
177
|
+
assert.equal(v.field, 'acknowledge_self_removal');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('the bad-membership refusal TEACHES the grammar', () => {
|
|
181
|
+
// Whoever lands here is trying to write a constitution, and since task 1003094
|
|
182
|
+
// the commonest reason is reaching for "and above".
|
|
183
|
+
const v = validateProposedConstitution({ ...CONSENT_90, membership: 'rank:metic++' });
|
|
184
|
+
assert.equal(v.ok, false);
|
|
185
|
+
assert.equal(v.reason, 'bad_membership');
|
|
186
|
+
assert.match(v.expected, /rank:<key>\+/);
|
|
187
|
+
assert.match(v.expected, /comma list/);
|
|
188
|
+
});
|
|
189
|
+
|
|
104
190
|
// ── invariants 2+3: the monarchy act, and the subject-typed anti-self ────────
|
|
105
191
|
|
|
106
192
|
const MONARCHY = Object.freeze({ membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true });
|
|
@@ -121,7 +207,7 @@ test('the monarchy act end to end: the sole Archon ratifies their OWN amendment
|
|
|
121
207
|
let authorAsked = 0;
|
|
122
208
|
await withDb({
|
|
123
209
|
getBoardItemForVote: async () => amendmentItem(),
|
|
124
|
-
builderHoldsRankActive: async (builderId,
|
|
210
|
+
builderHoldsRankActive: async (builderId, rankKeys) => builderId === '42' && rankKeys.includes('archon'),
|
|
125
211
|
insertBoardVote: async (args) => { const v = { id: '1', voter_id: args.voterId, direction: args.direction }; votes.push(v); return v; },
|
|
126
212
|
insertVoteKarma: async () => {},
|
|
127
213
|
getVotesForItem: async () => votes,
|
|
@@ -31,12 +31,61 @@ const { parseMembershipPredicate, resolveBoardMembers, isBoardMember } = members
|
|
|
31
31
|
|
|
32
32
|
// ── the pure parser ──────────────────────────────────────────────────────────
|
|
33
33
|
|
|
34
|
-
test('
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
assert.deepEqual(parseMembershipPredicate('rank:
|
|
34
|
+
test('a bare rank:<key> still means EXACTLY that key — the meaning-preserving half', () => {
|
|
35
|
+
// The owner's decision (2026-08-18) was EXPLICIT grammar, not a redefinition:
|
|
36
|
+
// every constitution already ratified — including the shipped monarchy default
|
|
37
|
+
// `rank:archon` — must resolve to the same board after an upgrade. Redefining
|
|
38
|
+
// the bare form as "and above" would have silently rewritten the whole fleet.
|
|
39
|
+
assert.deepEqual(parseMembershipPredicate('rank:archon'), { rankKeys: ['archon'] });
|
|
40
|
+
assert.deepEqual(parseMembershipPredicate('rank:metic'), { rankKeys: ['metic'] });
|
|
41
|
+
assert.deepEqual(parseMembershipPredicate('rank:triage-helper'), { rankKeys: ['triage-helper'] },
|
|
42
|
+
'a CUSTOM rank is legal by shape — whether it seats anyone is the DB answer');
|
|
43
|
+
assert.deepEqual(parseMembershipPredicate('rank:board_member'), { rankKeys: ['board_member'] });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('rank:<key>+ means that rank AND ABOVE — the form whose absence unseated an Archon', () => {
|
|
47
|
+
// THE BUG, in one assertion. Under v1 `rank:metic` was an exact SQL match, so
|
|
48
|
+
// an Archon who ratified it was 403'd off their own board on the next vote,
|
|
49
|
+
// and "Metic and above" could not be said at all.
|
|
50
|
+
assert.deepEqual(parseMembershipPredicate('rank:metic+'), { rankKeys: ['metic', 'archon'] });
|
|
51
|
+
assert.deepEqual(parseMembershipPredicate('rank:xenos+'), { rankKeys: ['xenos', 'thetes', 'metic', 'archon'] },
|
|
52
|
+
'rank:xenos+ is how you actually say everyone — bare rank:xenos seats NEWCOMERS ONLY');
|
|
53
|
+
assert.deepEqual(parseMembershipPredicate('rank:archon+'), { rankKeys: ['archon'] },
|
|
54
|
+
'the top of the ladder and above is just the top — no special case needed');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a comma list is an explicit union', () => {
|
|
58
|
+
assert.deepEqual(parseMembershipPredicate('rank:metic,archon'), { rankKeys: ['metic', 'archon'] });
|
|
59
|
+
assert.deepEqual(parseMembershipPredicate('rank:xenos+,ops'),
|
|
60
|
+
{ rankKeys: ['xenos', 'thetes', 'metic', 'archon', 'ops'] },
|
|
61
|
+
'the standard ladder and a custom rank compose');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a union dedupes and keeps a deterministic order', () => {
|
|
65
|
+
// The tally denominator is built from this list; a duplicate would raise the
|
|
66
|
+
// bar a consent vote must clear without anyone amending the constitution.
|
|
67
|
+
assert.deepEqual(parseMembershipPredicate('rank:metic+,archon'), { rankKeys: ['metic', 'archon'] });
|
|
68
|
+
assert.deepEqual(parseMembershipPredicate('rank:archon,archon'), { rankKeys: ['archon'] });
|
|
69
|
+
assert.deepEqual(parseMembershipPredicate('rank:metic+,xenos+'),
|
|
70
|
+
{ rankKeys: ['metic', 'archon', 'xenos', 'thetes'] },
|
|
71
|
+
'first occurrence wins its position — deterministic, which is what a denominator needs');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('+ on a rank with NO ladder position is REFUSED, not guessed', () => {
|
|
75
|
+
// "Metic and above" is a real question. "and above" about a rank sitting on no
|
|
76
|
+
// ladder is not, so it fails closed rather than resolving to something
|
|
77
|
+
// plausible. A BARE custom key is unaffected.
|
|
78
|
+
assert.equal(parseMembershipPredicate('rank:triage-helper+'), null);
|
|
79
|
+
assert.equal(parseMembershipPredicate('rank:ops+'), null);
|
|
80
|
+
assert.equal(parseMembershipPredicate('rank:archon,ops+'), null,
|
|
81
|
+
'one bad element poisons the whole predicate — a partial franchise is not a safe fallback');
|
|
82
|
+
assert.deepEqual(parseMembershipPredicate('rank:triage-helper'), { rankKeys: ['triage-helper'] });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the parse result is frozen all the way down', () => {
|
|
86
|
+
const parsed = parseMembershipPredicate('rank:metic+');
|
|
87
|
+
assert.ok(Object.isFrozen(parsed));
|
|
88
|
+
assert.ok(Object.isFrozen(parsed.rankKeys), 'a caller must not be able to widen the franchise in place');
|
|
40
89
|
});
|
|
41
90
|
|
|
42
91
|
test('everything else parses to null — the fail-closed value', () => {
|
|
@@ -47,9 +96,15 @@ test('everything else parses to null — the fail-closed value', () => {
|
|
|
47
96
|
'RANK:ARCHON', // grammar is lowercase
|
|
48
97
|
'rank:Archon',
|
|
49
98
|
'role:builder', // the rejected role-chamber model — must never parse
|
|
50
|
-
'rank:archon;metic', //
|
|
99
|
+
'rank:archon;metic', // ';' is not the list separator
|
|
51
100
|
'rank: archon', // no whitespace
|
|
52
101
|
'builder:3', // naming a PERSON is config-as-authority — refused by shape
|
|
102
|
+
'rank:archon,', // trailing separator
|
|
103
|
+
'rank:,archon', // leading separator
|
|
104
|
+
'rank:archon,,metic', // empty element
|
|
105
|
+
'rank:metic++', // one suffix, not a pile
|
|
106
|
+
'rank:+metic', // the suffix is a suffix
|
|
107
|
+
'rank:metic+archon', // '+' modifies, it does not join
|
|
53
108
|
];
|
|
54
109
|
for (const p of junk) {
|
|
55
110
|
assert.equal(parseMembershipPredicate(p), null, `${JSON.stringify(p)} must not parse`);
|
|
@@ -89,8 +144,12 @@ const MONARCHY = Object.freeze({ ...BOARD_DEFAULTS });
|
|
|
89
144
|
|
|
90
145
|
test('resolveBoardMembers: the happy path — rows come back frozen, ids as strings', async () => {
|
|
91
146
|
await withDb({
|
|
92
|
-
getActiveBuildersWithRank: async (
|
|
93
|
-
|
|
147
|
+
getActiveBuildersWithRank: async (rankKeys) => {
|
|
148
|
+
// The db read takes a SET since task 1003094 — every predicate form,
|
|
149
|
+
// including the bare one, resolves to one. A one-element set queries
|
|
150
|
+
// byte-identically to the old exact match, which is what keeps every
|
|
151
|
+
// already-ratified constitution meaning what it meant.
|
|
152
|
+
assert.deepEqual(rankKeys, ['archon'], 'the predicate rank set reaches the query verbatim');
|
|
94
153
|
return [
|
|
95
154
|
{ builder_id: 42n, github_login: 'first-member', display_name: 'First Member' },
|
|
96
155
|
{ builder_id: '1000099', github_login: 'second-member', display_name: 'Second Member' },
|
|
@@ -117,6 +176,38 @@ test('an unparseable predicate resolves to NOBODY without touching the db', asyn
|
|
|
117
176
|
});
|
|
118
177
|
});
|
|
119
178
|
|
|
179
|
+
test('a ladder predicate reaches the query as the whole expanded set', async () => {
|
|
180
|
+
// The end-to-end shape of the fix: `rank:metic+` must arrive at the SQL as
|
|
181
|
+
// BOTH metic and archon, or the Archon is off the board exactly as before.
|
|
182
|
+
let seen = null;
|
|
183
|
+
await withDb({
|
|
184
|
+
getActiveBuildersWithRank: async (rankKeys) => {
|
|
185
|
+
seen = rankKeys;
|
|
186
|
+
return [{ builder_id: '7', github_login: 'a-metic', display_name: 'A Metic' },
|
|
187
|
+
{ builder_id: '8', github_login: 'an-archon', display_name: 'An Archon' }];
|
|
188
|
+
},
|
|
189
|
+
}, async () => {
|
|
190
|
+
const members = await resolveBoardMembers({ ...MONARCHY, membership: 'rank:metic+' });
|
|
191
|
+
assert.deepEqual(seen, ['metic', 'archon']);
|
|
192
|
+
assert.equal(members.length, 2);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('THE REGRESSION: an Archon stays on the board under rank:metic+', async () => {
|
|
197
|
+
// The literal scenario from the R25 rehearsal. Under v1 this returned false
|
|
198
|
+
// and the owner lost their seat on their own constitution.
|
|
199
|
+
await withDb({
|
|
200
|
+
// A faithful stand-in for the real SQL: the builder holds exactly 'archon',
|
|
201
|
+
// and the query now asks whether that is IN the predicate's set.
|
|
202
|
+
builderHoldsRankActive: async (builderId, rankKeys) => rankKeys.includes('archon'),
|
|
203
|
+
}, async () => {
|
|
204
|
+
assert.equal(await isBoardMember('42', { ...MONARCHY, membership: 'rank:metic+' }), true,
|
|
205
|
+
'metic-and-above must seat the Archon');
|
|
206
|
+
assert.equal(await isBoardMember('42', { ...MONARCHY, membership: 'rank:metic' }), false,
|
|
207
|
+
'and the BARE form must still mean exactly metic — that is the compatibility half');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
120
211
|
test('a predicate naming a rank with no assignments seats an EMPTY board — loud, not widened', async () => {
|
|
121
212
|
await withDb({ getActiveBuildersWithRank: async () => [] }, async () => {
|
|
122
213
|
const members = await resolveBoardMembers({ ...MONARCHY, membership: 'rank:board-of-directors' });
|
|
@@ -126,7 +217,7 @@ test('a predicate naming a rank with no assignments seats an EMPTY board — lou
|
|
|
126
217
|
|
|
127
218
|
test('isBoardMember: fail-closed on a missing builder id, true only on a live row', async () => {
|
|
128
219
|
await withDb({
|
|
129
|
-
builderHoldsRankActive: async (builderId,
|
|
220
|
+
builderHoldsRankActive: async (builderId, rankKeys) => builderId === '42' && rankKeys.includes('archon'),
|
|
130
221
|
}, async () => {
|
|
131
222
|
assert.equal(await isBoardMember('42', MONARCHY), true);
|
|
132
223
|
assert.equal(await isBoardMember('999', MONARCHY), false);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/government_board_membership_db.mjs — the board franchise against a REAL
|
|
2
|
+
// Postgres (task 1003094).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS BESIDE THE DB-FREE government_board_membership.mjs. That file
|
|
5
|
+
// covers the pure parser and the resolvers against a stubbed db, which is where
|
|
6
|
+
// the grammar lives. It cannot cover the half this task actually changed: the
|
|
7
|
+
// SQL. `WHERE rank_key = $1` became `WHERE rank_key = ANY($1)` plus a DISTINCT,
|
|
8
|
+
// and a FRANCHISE bug is precisely the kind a stub agrees with and the database
|
|
9
|
+
// does not — the stub returns whatever the test author imagined the query means.
|
|
10
|
+
//
|
|
11
|
+
// What only real SQL can answer:
|
|
12
|
+
// * does `rank:metic+` genuinely seat the Archon (the regression that cost an
|
|
13
|
+
// owner their seat on their own constitution);
|
|
14
|
+
// * do the two STRUCTURAL filters survive the rewrite — a deactivated builder
|
|
15
|
+
// and a machine principal must still never govern, because a widened
|
|
16
|
+
// franchise that also admits a bot is a worse bug than the one being fixed;
|
|
17
|
+
// * does DISTINCT actually hold when a builder matches through two keys — a
|
|
18
|
+
// double-counted member silently raises the bar a consent vote must clear.
|
|
19
|
+
//
|
|
20
|
+
// It SELF-SKIPS (exit 0) with no reachable Postgres, and is listed in
|
|
21
|
+
// run-unit-tests.js's INTEGRATION set so the DB-free lane never runs it.
|
|
22
|
+
//
|
|
23
|
+
// Run: DATABASE_URL=postgres://... node tests/government_board_membership_db.mjs
|
|
24
|
+
//
|
|
25
|
+
// It creates its own throwaway `builders` / `government_builder_ranks` rows at
|
|
26
|
+
// ids far above any real row, and deletes them at the end.
|
|
27
|
+
|
|
28
|
+
import pg from 'pg';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await pool.query('SELECT 1');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`government_board_membership_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
|
|
38
|
+
await pool.end().catch(() => {});
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
let passed = 0, failed = 0;
|
|
42
|
+
async function t(name, fn) {
|
|
43
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
44
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
45
|
+
}
|
|
46
|
+
const assert = (await import('node:assert')).strict;
|
|
47
|
+
|
|
48
|
+
// Minimal stand-ins ONLY when the real tables are absent (throwaway container);
|
|
49
|
+
// against a migrated database these are no-ops and the real tables are used.
|
|
50
|
+
await pool.query(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS builders (
|
|
52
|
+
id bigserial PRIMARY KEY, github_login text, display_name text,
|
|
53
|
+
status text DEFAULT 'active', system_role text);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS government_builder_ranks (
|
|
55
|
+
builder_id bigint, rank_key text);
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
// Ids far above any real row, and every login prefixed, so cleanup is exact and
|
|
59
|
+
// this can never touch a real builder or a real rank assignment.
|
|
60
|
+
const B = 990000000;
|
|
61
|
+
const ID = { xenos: B + 1, thetes: B + 2, metic: B + 3, archon: B + 4, gone: B + 5, bot: B + 6, ops: B + 7 };
|
|
62
|
+
const cleanup = async () => {
|
|
63
|
+
await pool.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
|
|
64
|
+
await pool.query('DELETE FROM builders WHERE id >= $1', [B]);
|
|
65
|
+
};
|
|
66
|
+
await cleanup();
|
|
67
|
+
await pool.query(`INSERT INTO builders (id, github_login, display_name, status, system_role) VALUES
|
|
68
|
+
($1,'gbmdb-x','Xenos','active',NULL),($2,'gbmdb-t','Thetes','active',NULL),
|
|
69
|
+
($3,'gbmdb-m','Metic','active',NULL),($4,'gbmdb-a','Archon','active',NULL),
|
|
70
|
+
($5,'gbmdb-gone','Deactivated Archon','inactive',NULL),
|
|
71
|
+
($6,'gbmdb-bot','A Machine','active','bfg'),
|
|
72
|
+
($7,'gbmdb-ops','Custom Ops','active',NULL)`,
|
|
73
|
+
[ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
|
|
74
|
+
await pool.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
|
|
75
|
+
($1,'xenos'),($2,'thetes'),($3,'metic'),($4,'archon'),($5,'archon'),($6,'archon'),($7,'gbmdb-ops-rank')`,
|
|
76
|
+
[ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
|
|
77
|
+
|
|
78
|
+
const api = require(process.cwd() + '/src/module-api.js');
|
|
79
|
+
api.pool.query = (...a) => pool.query(...a);
|
|
80
|
+
const { resolveBoardMembers, isBoardMember } = require(process.cwd() + '/modules/government/board-membership.js');
|
|
81
|
+
|
|
82
|
+
const board = (membership) => ({ membership, pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
|
|
83
|
+
const logins = async (m) => (await resolveBoardMembers(board(m)))
|
|
84
|
+
.filter((r) => Number(r.builder_id) >= B) // ignore any real rows on a migrated db
|
|
85
|
+
.map((r) => r.github_login.replace('gbmdb-', ''));
|
|
86
|
+
|
|
87
|
+
console.log('the predicate against real SQL');
|
|
88
|
+
|
|
89
|
+
await t('bare rank:archon is unchanged — exactly archons', async () => {
|
|
90
|
+
assert.deepEqual(await logins('rank:archon'), ['a']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await t('THE BUG: bare rank:metic seats metics ONLY (still true, deliberately)', async () => {
|
|
94
|
+
assert.deepEqual(await logins('rank:metic'), ['m']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await t('THE FIX: rank:metic+ seats metic AND archon', async () => {
|
|
98
|
+
assert.deepEqual(await logins('rank:metic+'), ['m', 'a']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await t('rank:xenos+ is how you say everyone', async () => {
|
|
102
|
+
assert.deepEqual(await logins('rank:xenos+'), ['x', 't', 'm', 'a']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t('bare rank:xenos really does seat NEWCOMERS ONLY — the library\'s lie', async () => {
|
|
106
|
+
assert.deepEqual(await logins('rank:xenos'), ['x']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await t('a comma union works, and composes with the ladder', async () => {
|
|
110
|
+
assert.deepEqual(await logins('rank:metic,archon'), ['m', 'a']);
|
|
111
|
+
assert.deepEqual(await logins('rank:metic+,gbmdb-ops-rank'), ['m', 'a', 'ops']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await t('the deactivated and the machine are STILL excluded under the widened form', async () => {
|
|
115
|
+
// The two structural filters must survive the ANY() rewrite — a widened
|
|
116
|
+
// franchise that also admits a bot or a departed builder is a worse bug.
|
|
117
|
+
const l = await logins('rank:xenos+');
|
|
118
|
+
assert.ok(!l.includes('gone'), 'a deactivated builder never governs');
|
|
119
|
+
assert.ok(!l.includes('bot'), 'a machine principal never governs');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await t('no duplicate rows when a builder matches through two keys', async () => {
|
|
123
|
+
await pool.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
|
|
124
|
+
try {
|
|
125
|
+
const l = await logins('rank:archon,gbmdb-ops-rank');
|
|
126
|
+
assert.deepEqual(l.filter((x) => x === 'a').length, 1,
|
|
127
|
+
'DISTINCT holds — a double-counted member silently raises the consent bar');
|
|
128
|
+
} finally {
|
|
129
|
+
await pool.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await t('isBoardMember agrees with resolveBoardMembers on every form', async () => {
|
|
134
|
+
assert.equal(await isBoardMember(ID.archon, board('rank:metic+')), true, 'THE REGRESSION: the Archon keeps their seat');
|
|
135
|
+
assert.equal(await isBoardMember(ID.archon, board('rank:metic')), false, 'and the bare form still excludes them');
|
|
136
|
+
assert.equal(await isBoardMember(ID.metic, board('rank:metic+')), true);
|
|
137
|
+
assert.equal(await isBoardMember(ID.xenos, board('rank:xenos+')), true);
|
|
138
|
+
assert.equal(await isBoardMember(ID.gone, board('rank:xenos+')), false, 'deactivated');
|
|
139
|
+
assert.equal(await isBoardMember(ID.bot, board('rank:xenos+')), false, 'machine');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await t('an unparseable predicate still seats nobody against a real db', async () => {
|
|
143
|
+
assert.deepEqual(await logins('rank:ops+'), [], '+ on a non-ladder rank fails closed');
|
|
144
|
+
assert.deepEqual(await logins('nonsense'), []);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await cleanup();
|
|
148
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
149
|
+
await pool.end();
|
|
150
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -84,8 +84,10 @@ test('the happy path: a member yes is recorded fresh, with its +1 karma', async
|
|
|
84
84
|
let insertArgs = null;
|
|
85
85
|
await withDb({
|
|
86
86
|
getBoardItemForVote: async () => openItem(),
|
|
87
|
-
builderHoldsRankActive: async (builderId,
|
|
88
|
-
|
|
87
|
+
builderHoldsRankActive: async (builderId, rankKeys) =>
|
|
88
|
+
// The db read takes a SET since task 1003094; the REAL membership parse
|
|
89
|
+
// resolved 'rank:archon' from the snapshot to the one-element set ['archon'].
|
|
90
|
+
builderId === '42' && rankKeys.includes('archon'),
|
|
89
91
|
getBoardItemAuthor: async () => '7', // someone else authored the idea
|
|
90
92
|
insertBoardVote: async (args) => { insertArgs = args; return { id: '1', ...args }; },
|
|
91
93
|
insertVoteKarma: async (args) => { karma.push(args); },
|
|
@@ -105,7 +107,7 @@ test('membership is the SNAPSHOT\'s rule: a rank the snapshot does not name is r
|
|
|
105
107
|
// they hold A rank — just not the snapshotted one.
|
|
106
108
|
await withDb({
|
|
107
109
|
getBoardItemForVote: async () => openItem(),
|
|
108
|
-
builderHoldsRankActive: async (builderId,
|
|
110
|
+
builderHoldsRankActive: async (builderId, rankKeys) => rankKeys.includes('metic'), // holds metic, not archon
|
|
109
111
|
getBoardItemAuthor: async () => { throw new Error('must not be reached'); },
|
|
110
112
|
insertBoardVote: async () => { throw new Error('must not be reached'); },
|
|
111
113
|
}, async () => {
|
|
@@ -86,3 +86,56 @@ test('the read rides the vote atom, and the client renders the SERVER value (sou
|
|
|
86
86
|
assert.ok(!/'first_ratifier'[^]*hardcod/i.test(client), 'sanity');
|
|
87
87
|
assert.match(client, /loadConstitution\(\)/, 'the panel loads for everyone the page admits');
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// ── the hall's membership prose mirrors the server parser (task 1003094) ─────
|
|
91
|
+
//
|
|
92
|
+
// Browser code cannot import the server module (the established pattern here —
|
|
93
|
+
// the five Full Idea section labels are mirrored the same way), so the hall
|
|
94
|
+
// re-implements the predicate grammar to render it in plain language. Two copies
|
|
95
|
+
// of a grammar is exactly the drift that produced the bug this task fixes, so
|
|
96
|
+
// they are held equal here over every form.
|
|
97
|
+
//
|
|
98
|
+
// This matters more than a normal mirror: the Constitution panel is where an
|
|
99
|
+
// owner READS who governs before deciding whether to change it. A panel that
|
|
100
|
+
// misdescribes the franchise is the same failure as a resolver that mis-seats
|
|
101
|
+
// it, one layer up.
|
|
102
|
+
test('the hall renders every predicate form the server actually resolves', () => {
|
|
103
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
|
|
104
|
+
|
|
105
|
+
// Lift the browser mirror out of the IIFE and run it.
|
|
106
|
+
const ladder = /const STANDARD_LADDER = (\[[^\]]*\]);/.exec(src);
|
|
107
|
+
assert.ok(ladder, 'the hall must declare the standard ladder it expands `+` against');
|
|
108
|
+
const fn = /function membershipRankKeys\(predicate\) \{[\s\S]*?\n \}/.exec(src);
|
|
109
|
+
assert.ok(fn, 'membershipRankKeys must be findable — this test lifts it');
|
|
110
|
+
// eslint-disable-next-line no-new-func
|
|
111
|
+
const browserKeys = new Function(`const STANDARD_LADDER = ${ladder[1]}; ${fn[0]}; return membershipRankKeys;`)();
|
|
112
|
+
|
|
113
|
+
const { parseMembershipPredicate } = require(path.join(ROOT, 'modules/government/membership-predicate.js'));
|
|
114
|
+
|
|
115
|
+
const forms = [
|
|
116
|
+
'rank:archon', 'rank:metic', 'rank:xenos', 'rank:custom-rank',
|
|
117
|
+
'rank:metic+', 'rank:xenos+', 'rank:archon+',
|
|
118
|
+
'rank:metic,archon', 'rank:metic+,ops', 'rank:archon,archon',
|
|
119
|
+
'rank:custom+', 'rank:', 'rank:metic++', 'rank:+metic', 'nonsense', '',
|
|
120
|
+
'rank:archon,', 'rank:,archon', 'rank:archon,,metic',
|
|
121
|
+
];
|
|
122
|
+
for (const f of forms) {
|
|
123
|
+
const server = parseMembershipPredicate(f);
|
|
124
|
+
const browser = browserKeys(f);
|
|
125
|
+
assert.deepEqual(
|
|
126
|
+
browser === null ? null : browser,
|
|
127
|
+
server === null ? null : [...server.rankKeys],
|
|
128
|
+
`the hall and the server disagree about ${JSON.stringify(f)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('the ladder the hall expands against IS the government ladder', () => {
|
|
134
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
|
|
135
|
+
const { RANK_ORDER } = require(path.join(ROOT, 'modules/government/catalog.js'));
|
|
136
|
+
const m = /const STANDARD_LADDER = \[([^\]]*)\]/.exec(src);
|
|
137
|
+
assert.ok(m);
|
|
138
|
+
const hall = m[1].split(',').map((x) => x.trim().replace(/['"]/g, '')).filter(Boolean);
|
|
139
|
+
assert.deepEqual(hall, [...RANK_ORDER],
|
|
140
|
+
'a hall ladder that drifts from catalog.RANK_ORDER renders a franchise nobody has');
|
|
141
|
+
});
|
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
import { strict as assert } from 'node:assert';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
+
// fs/path/ROOT serve the disk-DERIVED guards at the bottom of this file (task
|
|
19
|
+
// 1003132). They must read the tree rather than a list: an enumerated assertion
|
|
20
|
+
// would miss the next unclassified module exactly as the enumerated allowlist
|
|
21
|
+
// missed the one that prompted them.
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import url from 'node:url';
|
|
18
25
|
|
|
26
|
+
const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
|
|
19
27
|
const require = createRequire(import.meta.url);
|
|
20
28
|
const m = require('../scripts/gds/publish-manifest.js');
|
|
21
29
|
const {
|
|
@@ -179,6 +187,80 @@ t('every PROTECTED_GLOB is either topology-excluded or publishable (the methodol
|
|
|
179
187
|
assert.ok(isPublishable(g), `protected surface "${g}" is neither denied nor publishable — re-decide the carve-out`);
|
|
180
188
|
}
|
|
181
189
|
});
|
|
190
|
+
// ── the publish question, asked of the tree itself (task 1003132) ───────────────────
|
|
191
|
+
//
|
|
192
|
+
// THE GAP THESE CLOSE. modules/copy-desk/ was designed, built, tested against a real
|
|
193
|
+
// Postgres, graded, merged and very nearly RELEASED while publishing nothing. Not one
|
|
194
|
+
// of the 429 unit tests, 20+ fitness checks, or the adversarial grader asks the publish
|
|
195
|
+
// question, and the assertions above enumerate representative paths — so a brand-new
|
|
196
|
+
// module key appears in no list and is simply invisible.
|
|
197
|
+
//
|
|
198
|
+
// The failure is worse than "the feature does not ship", because it is ASYMMETRIC: the
|
|
199
|
+
// module's hall page ships anyway (modules/hall-ui/ is one allowlisted subtree), so a
|
|
200
|
+
// release delivers a nav tab and a page whose every API call 404s.
|
|
201
|
+
//
|
|
202
|
+
// Both guards DERIVE from disk. A list here would rot the same way the allowlist did.
|
|
203
|
+
//
|
|
204
|
+
// Modules deliberately not published, on the record. An entry here is a decision
|
|
205
|
+
// someone made, not a gap someone forgot — and every entry needs the task or idea that
|
|
206
|
+
// records the decision, so this can never become a quiet parking lot.
|
|
207
|
+
const KNOWN_UNPUBLISHED_MODULES = new Map([
|
|
208
|
+
// Same gap as copy-desk, found by the guard below the moment it was written. LATENT
|
|
209
|
+
// rather than live only because agents is default:false — the day a released instance
|
|
210
|
+
// enables it, the loader MODULE_NOT_FOUNDs, which is exactly what the provisioning
|
|
211
|
+
// (task 2135) and platform-identity (task 1002235) allowlist entries exist to prevent.
|
|
212
|
+
// Left for goal 1000038 rather than fixed here: deciding what the published core
|
|
213
|
+
// contains for the agents module is that goal's call, not a drive-by from a copy-desk
|
|
214
|
+
// task. Filed as idea 1000797 with the one-line fix.
|
|
215
|
+
['agents', 'idea 1000797'],
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
t('every module on disk is either publishable or deliberately unpublished (a new module cannot be silently invisible)', () => {
|
|
219
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
220
|
+
const keys = fs.readdirSync(modulesDir, { withFileTypes: true })
|
|
221
|
+
.filter((d) => d.isDirectory() && fs.existsSync(path.join(modulesDir, d.name, 'module.json')))
|
|
222
|
+
.map((d) => d.name);
|
|
223
|
+
assert.ok(keys.length > 5, `sanity: only ${keys.length} modules found on disk — the scan is broken, not the tree`);
|
|
224
|
+
|
|
225
|
+
const unclassified = keys.filter((k) => {
|
|
226
|
+
const manifest = `modules/${k}/module.json`;
|
|
227
|
+
return !isPublishable(manifest) && !matchesDeny(manifest) && !KNOWN_UNPUBLISHED_MODULES.has(k);
|
|
228
|
+
});
|
|
229
|
+
assert.deepEqual(unclassified, [],
|
|
230
|
+
'these modules are in neither the allowlist nor a deny list, so they publish NOTHING. Add each to PUBLISH_ALLOWLIST, deny it on the record, or register it in KNOWN_UNPUBLISHED_MODULES with the task that decided it.');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
t('every KNOWN_UNPUBLISHED_MODULES entry names a real module and a real decision', () => {
|
|
234
|
+
// Two ways this exception list rots: an entry outlives the module (and silently
|
|
235
|
+
// exempts nothing), or an entry outlives the fix (and silently exempts something
|
|
236
|
+
// already fixed). Both make the guard above weaker than it looks.
|
|
237
|
+
for (const [key, ref] of KNOWN_UNPUBLISHED_MODULES) {
|
|
238
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules', key, 'module.json')),
|
|
239
|
+
`KNOWN_UNPUBLISHED_MODULES names "${key}", which is not a module on disk — drop the entry`);
|
|
240
|
+
assert.ok(/\b(task|idea)\s+\d+/.test(ref),
|
|
241
|
+
`KNOWN_UNPUBLISHED_MODULES["${key}"] must cite the task or idea that decided it, got "${ref}"`);
|
|
242
|
+
assert.equal(isPublishable(`modules/${key}/module.json`), false,
|
|
243
|
+
`"${key}" is now publishable — delete its KNOWN_UNPUBLISHED_MODULES entry so the guard covers it again`);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
t('a hall page named for a module ships only if that module ships (never a tab whose API 404s)', () => {
|
|
248
|
+
const hallDir = path.join(ROOT, 'modules/hall-ui/public');
|
|
249
|
+
const modulesDir = path.join(ROOT, 'modules');
|
|
250
|
+
if (!fs.existsSync(hallDir)) return;
|
|
251
|
+
const orphans = [];
|
|
252
|
+
for (const f of fs.readdirSync(hallDir)) {
|
|
253
|
+
if (!f.endsWith('.html')) continue;
|
|
254
|
+
const key = f.replace(/\.html$/, '');
|
|
255
|
+
// Only pages that NAME a module are paired — the hall's own pages (work, goals,
|
|
256
|
+
// settings…) have no module and are not the subject of this rule.
|
|
257
|
+
if (!fs.existsSync(path.join(modulesDir, key, 'module.json'))) continue;
|
|
258
|
+
if (!isPublishable(`modules/${key}/module.json`)) orphans.push(f);
|
|
259
|
+
}
|
|
260
|
+
assert.deepEqual(orphans, [],
|
|
261
|
+
'these hall pages would ship to instances whose backing module does not — a nav tab and a page whose every call 404s. Publish the module, or stop publishing the page.');
|
|
262
|
+
});
|
|
263
|
+
|
|
182
264
|
t('the topology subset of the protected surfaces is never publishable', () => {
|
|
183
265
|
for (const g of PROTECTED_TOPOLOGY_EXCLUDE) {
|
|
184
266
|
assert.equal(isPublishable(g), false, `topology surface "${g}" must not publish`);
|
|
@@ -107,6 +107,15 @@ const STATIC_LADDER_FILES = [
|
|
|
107
107
|
'modules/hall-ui/public/ranks.js',
|
|
108
108
|
'modules/hall-ui/public/profile.js',
|
|
109
109
|
'modules/hall-ui/public/watch.js',
|
|
110
|
+
// task 1003094: the Constitution panel expands the `rank:<key>+` membership
|
|
111
|
+
// form ("that rank and above") to render who governs in plain language, so it
|
|
112
|
+
// needs the ladder to expand against. Browser code cannot require the server
|
|
113
|
+
// module — the established mirror pattern here — and listing it in this array
|
|
114
|
+
// is what makes the copy provable rather than merely allowed. A second,
|
|
115
|
+
// government-specific pin lives in tests/government_constitution_view.mjs,
|
|
116
|
+
// which holds it against catalog.RANK_ORDER and holds the whole browser parser
|
|
117
|
+
// equal to the server one.
|
|
118
|
+
'modules/hall-ui/public/government.js',
|
|
110
119
|
];
|
|
111
120
|
test('static ladder copies carry the exact canonical ladder', () => {
|
|
112
121
|
for (const rel of STATIC_LADDER_FILES) {
|