@bongos/core 1.21.97 → 1.21.99
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 +56 -31
- package/docs/adr/0152-metic-task-abandonment.md +2 -0
- package/docs/adr/0371-auto-pickup-applies-a-submitted-tweak-on-the-runner.md +64 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +2 -2
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +32 -30
- package/docs/copy-registry.json +51 -33
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +10 -10
- package/migrations/core_274_parked_backlog_stays_parked.sql +140 -0
- package/modules/hall-ui/public/studio-lib.js +22 -0
- package/modules/hall-ui/public/studio.css +138 -7
- package/modules/hall-ui/public/studio.html +10 -4
- package/modules/hall-ui/public/studio.js +39 -1
- package/modules/hall-ui/records/studio.md +16 -0
- package/modules/lifecycle/db-ship.js +7 -2
- package/modules/lifecycle/db-tasks.js +25 -0
- package/modules/lifecycle/routes/tasks.js +5 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/government-walk.js +842 -0
- package/src/module-api.js +1 -1
- package/tests/abandon_task.mjs +1 -1
- package/tests/government_walk.mjs +56 -0
- package/tests/hall_studio_home.mjs +140 -5
- package/tests/parked_backlog_stays_parked.mjs +182 -0
- package/tests/promotion_triggers_skip_cascade.mjs +5 -3
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scripts/gds/government-walk.js — THE GOVERNMENT WALK: one repeatable end-to-end
|
|
5
|
+
// pass over a throwaway test project's government, producing a pass/fail table
|
|
6
|
+
// (task 1004658, goal 1000111, criterion gov-proven-and-kept-proven).
|
|
7
|
+
//
|
|
8
|
+
// WHAT IT WALKS, in four sections:
|
|
9
|
+
// 1. Genesis — start founding, decide with a chosen government, close all five
|
|
10
|
+
// stages, come alive; the constitution reads as the choice.
|
|
11
|
+
// 2. Every available form (Monarchy / BDFL, Timocratic Republic, Direct Democracy,
|
|
12
|
+
// Foundation with a council rank) plus the two pass rules no form uses
|
|
13
|
+
// (majority, unanimous): switch to it through a charter amendment, then confirm
|
|
14
|
+
// who can vote and what passes.
|
|
15
|
+
// 3. Every building: set it to decide by vote (a decision-rules amendment), try to
|
|
16
|
+
// act alone on each of its bound routes (refused), restore it (reachable again).
|
|
17
|
+
// 4. Every matter action: open the matter, pass it, confirm the effect happened.
|
|
18
|
+
//
|
|
19
|
+
// HOW REAL IT IS. Every request is real HTTP through the real routers (government,
|
|
20
|
+
// onboarding/genesis, and every building's own router), with the real
|
|
21
|
+
// requirePermission gate resolving the real government port, and every decision is
|
|
22
|
+
// the real code: the board, membership, the tally, the close, the charter apply
|
|
23
|
+
// (written to a throwaway instance folder), the decision-rules apply, act-alone, and
|
|
24
|
+
// each module's matter act. What is NOT real is the storage under them: there is no
|
|
25
|
+
// Postgres on the machine this was built on, so the government module's db layer is
|
|
26
|
+
// an in-memory fake with the same row shapes and the same guards (one open sitting
|
|
27
|
+
// per subject, one vote per member, single-winner close, the decision-rules write
|
|
28
|
+
// only from a passed decision-rules amendment), and the few writes a passed matter
|
|
29
|
+
// makes into other modules are recorded in memory instead of written to a database
|
|
30
|
+
// (or, for a gate approval, sent to GitHub). The table says which mode ran.
|
|
31
|
+
//
|
|
32
|
+
// Usage:
|
|
33
|
+
// node scripts/gds/government-walk.js run it and print the table
|
|
34
|
+
// node scripts/gds/government-walk.js --write also write docs/governance/government-walk.md
|
|
35
|
+
// node scripts/gds/government-walk.js --check exit 1 if the committed table differs
|
|
36
|
+
// tests/government_walk.mjs runs --check, so a change that flips any row reds CI.
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const os = require('os');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
|
|
42
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
43
|
+
const DOC_PATH = path.join(ROOT, 'docs', 'governance', 'government-walk.md');
|
|
44
|
+
|
|
45
|
+
// Bugs this walk found, filed as tasks under goal 1000111. A FAIL row names its task
|
|
46
|
+
// so the table reads as a worklist; a row that starts passing again just passes.
|
|
47
|
+
const KNOWN_BUGS = Object.freeze({});
|
|
48
|
+
|
|
49
|
+
// ── the throwaway project ───────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
// The test project's people. 42 founds it. 47 left the project; it only ever
|
|
52
|
+
// authors the probe ideas, so no probe is decided by its own author.
|
|
53
|
+
const BUILDERS = Object.freeze([
|
|
54
|
+
{ id: '42', rank: 'archon', github_login: 'ada', display_name: 'Ada (founder)', status: 'active' },
|
|
55
|
+
{ id: '43', rank: 'metic', github_login: 'ben', display_name: 'Ben', status: 'active' },
|
|
56
|
+
{ id: '44', rank: 'metic', github_login: 'cy', display_name: 'Cy', status: 'active' },
|
|
57
|
+
{ id: '45', rank: 'thetes', github_login: 'dee', display_name: 'Dee', status: 'active' },
|
|
58
|
+
{ id: '46', rank: 'xenos', github_login: 'eve', display_name: 'Eve', status: 'active' },
|
|
59
|
+
{ id: '47', rank: 'xenos', github_login: 'fay', display_name: 'Fay (left)', status: 'left' },
|
|
60
|
+
]);
|
|
61
|
+
const ACTIVE_IDS = BUILDERS.filter((b) => b.status === 'active').map((b) => b.id);
|
|
62
|
+
const FOUNDER = '42';
|
|
63
|
+
const COUNCIL = ['43', '44'];
|
|
64
|
+
|
|
65
|
+
// Each bound route as a request that, as today, stops in its handler's own first
|
|
66
|
+
// check (a bad id or body) before any database write — the same requests as
|
|
67
|
+
// tests/government_act_alone_buildings.mjs, with People's two carrying a real
|
|
68
|
+
// custom rank and builder of this project.
|
|
69
|
+
const BAD = 'x';
|
|
70
|
+
const REQUESTS = Object.freeze({
|
|
71
|
+
'PATCH /project-settings/rot-days': ['PATCH', '/project-settings/rot-days', { days: 'soon' }],
|
|
72
|
+
'PATCH /project-settings/ship-mode': ['PATCH', '/project-settings/ship-mode', { mode: 'sometimes' }],
|
|
73
|
+
'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
|
|
74
|
+
'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
|
|
75
|
+
'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
|
|
76
|
+
'POST /security/reports/:id/fix': ['POST', `/security/reports/${BAD}/fix`, {}],
|
|
77
|
+
'POST /government/assignments': ['POST', '/government/assignments', { builder_id: 46, rank_key: 'tester' }],
|
|
78
|
+
'DELETE /government/assignments/:builderId/:rankKey': ['DELETE', '/government/assignments/46/tester'],
|
|
79
|
+
'PATCH /access-requests/:id': ['PATCH', `/access-requests/${BAD}`, { status: 'invited' }],
|
|
80
|
+
'POST /access-requests/invite': ['POST', '/access-requests/invite', {}],
|
|
81
|
+
'POST /goals': ['POST', '/goals', {}],
|
|
82
|
+
'POST /goals/reorder': ['POST', '/goals/reorder', {}],
|
|
83
|
+
'POST /work-categories': ['POST', '/work-categories', {}],
|
|
84
|
+
'PUT /versions/focus': ['PUT', '/versions/focus', { version_id: ' ' }],
|
|
85
|
+
'POST /autonomy/fence': ['POST', '/autonomy/fence', { enabled: 'yes' }],
|
|
86
|
+
'POST /autonomy/fence/goals': ['POST', '/autonomy/fence/goals', { goal_id: 0 }],
|
|
87
|
+
'DELETE /autonomy/fence/goals/:goalId': ['DELETE', `/autonomy/fence/goals/${BAD}`],
|
|
88
|
+
'POST /autonomy/fence/priority': ['POST', '/autonomy/fence/priority', {}],
|
|
89
|
+
'POST /autonomy/builders/:builderId/pause': ['POST', `/autonomy/builders/${BAD}/pause`, { paused: false }],
|
|
90
|
+
'POST /done-when/:criterionId/satisfy': ['POST', `/done-when/${BAD}/satisfy`, {}],
|
|
91
|
+
'POST /done-when/:criterionId/unsatisfy': ['POST', `/done-when/${BAD}/unsatisfy`, {}],
|
|
92
|
+
'POST /gate-approvals/:pr/approve': ['POST', `/gate-approvals/${BAD}/approve`, {}],
|
|
93
|
+
'POST /override-requests/:id/decide': ['POST', `/override-requests/${BAD}/decide`, { decision: 'approve' }],
|
|
94
|
+
'POST /provisioning/instances/:id/core-upgrade': ['POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3' }],
|
|
95
|
+
'POST /provisioning/instances/:id/update-channel': ['POST', `/provisioning/instances/${BAD}/update-channel`, { channel: 'nightly' }],
|
|
96
|
+
'POST /render-deploy/act': ['POST', '/render-deploy/act', { action: 'launch' }],
|
|
97
|
+
'POST /npm-release/release': ['POST', '/npm-release/release', { version: 'v-next' }],
|
|
98
|
+
'PATCH /builders/:id/budget': ['PATCH', `/builders/${BAD}/budget`, { monthly_budget_usd: 5 }],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Every matter action, as the matter the walk opens and passes: [area, title, action,
|
|
102
|
+
// the effect in words]. runWalk observes each effect (its HAPPENED table).
|
|
103
|
+
const MATTER_PLAN = Object.freeze([
|
|
104
|
+
['people', 'Give Eve the tester rank', { type: 'assign_rank', builder_id: 46, rank_key: 'tester' }, 'Eve holds tester'],
|
|
105
|
+
['people', 'Take the tester rank from Eve', { type: 'unassign_rank', builder_id: 46, rank_key: 'tester' }, 'Eve no longer holds tester'],
|
|
106
|
+
['automation', 'Switch unattended building on', { type: 'switch_on' }, 'fence on'],
|
|
107
|
+
['automation', 'Switch unattended building off', { type: 'switch_off' }, 'fence off'],
|
|
108
|
+
['automation', 'Allow goal 77 to build unattended', { type: 'allow_goal', goal_id: 77 }, 'goal 77 allowed'],
|
|
109
|
+
['release', 'Approve override request 5', { type: 'approve_override', request_id: 5 }, 'request 5 approved'],
|
|
110
|
+
['release', 'Clear the gate on PR 1500', { type: 'approve_gate', pr: 1500 }, 'PR 1500 cleared'],
|
|
111
|
+
['review', 'Sign off criterion 9', { type: 'satisfy_criterion', criterion_id: 9 }, 'criterion 9 satisfied'],
|
|
112
|
+
['treasury', 'Dee gets $50 a month', { type: 'set_budget', builder_id: 45, monthly_usd: 50 }, 'Dee\'s limit is $50'],
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
// Every router the walk drives, mounted as the server mounts them.
|
|
116
|
+
const ROUTERS = Object.freeze([
|
|
117
|
+
'modules/government/routes/government.js',
|
|
118
|
+
'modules/onboarding/routes/onboarding.js',
|
|
119
|
+
'modules/autonomy/routes/autonomy.js',
|
|
120
|
+
// no HTTP surface; it contributes the Release and Review matter acts, as at boot
|
|
121
|
+
'modules/lifecycle/routes/lifecycle.js',
|
|
122
|
+
'modules/lifecycle/routes/done-when.js',
|
|
123
|
+
'modules/lifecycle/routes/gate-approvals.js',
|
|
124
|
+
'modules/lifecycle/routes/tasks.js',
|
|
125
|
+
'modules/lifecycle/routes/goals.js',
|
|
126
|
+
'modules/lifecycle/routes/versions.js',
|
|
127
|
+
'modules/lifecycle/routes/project-settings.js',
|
|
128
|
+
'modules/render-deploy/routes/act.js',
|
|
129
|
+
'modules/npm-release/routes/release.js',
|
|
130
|
+
'modules/provisioning/routes/core-upgrade.js',
|
|
131
|
+
'src/bongos/routes/builders.js',
|
|
132
|
+
'modules/security/routes/reports.js',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// ── the in-memory government store ──────────────────────────────────────────
|
|
136
|
+
//
|
|
137
|
+
// The government module's db.js, with the same row shapes and the same guards the
|
|
138
|
+
// SQL carries. Any db function the walk did not expect throws, so a code path that
|
|
139
|
+
// reaches storage this fake does not model shows up as a FAIL rather than a quiet
|
|
140
|
+
// undefined.
|
|
141
|
+
function makeStore(catalog) {
|
|
142
|
+
const s = {
|
|
143
|
+
seq: 100,
|
|
144
|
+
clock: 0,
|
|
145
|
+
builders: new Map(BUILDERS.map((b) => [b.id, { ...b }])),
|
|
146
|
+
customRanks: new Map(),
|
|
147
|
+
assignments: [], // { builder_id, rank_key, assigned_by }
|
|
148
|
+
items: [],
|
|
149
|
+
votes: [],
|
|
150
|
+
amendments: new Map(),
|
|
151
|
+
matters: new Map(),
|
|
152
|
+
rules: new Map(),
|
|
153
|
+
ideas: new Map(),
|
|
154
|
+
tasks: new Map(),
|
|
155
|
+
karma: [],
|
|
156
|
+
unexpected: [],
|
|
157
|
+
};
|
|
158
|
+
const next = () => String(++s.seq);
|
|
159
|
+
const tick = () => `T${String(++s.clock).padStart(4, '0')}`;
|
|
160
|
+
const heldRanks = (id) => {
|
|
161
|
+
const b = s.builders.get(String(id));
|
|
162
|
+
if (!b) return [];
|
|
163
|
+
return [b.rank, ...s.assignments.filter((a) => a.builder_id === String(id)).map((a) => a.rank_key)];
|
|
164
|
+
};
|
|
165
|
+
const isLive = (b) => b && b.status === 'active';
|
|
166
|
+
const copy = (x) => (x == null ? x : JSON.parse(JSON.stringify(x)));
|
|
167
|
+
const withExpiry = (i) => ({ ...copy(i), expired: false, remaining_seconds: null });
|
|
168
|
+
const voteRow = (v) => {
|
|
169
|
+
const b = s.builders.get(v.voter_id) || {};
|
|
170
|
+
return { ...copy(v), github_login: b.github_login || null, display_name: b.display_name || null };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const fns = {
|
|
174
|
+
async getPermissionKeysForBuilder(id) {
|
|
175
|
+
const keys = new Set();
|
|
176
|
+
for (const r of heldRanks(id)) for (const k of catalog.RANK_SEED[r] || []) keys.add(k);
|
|
177
|
+
return [...keys];
|
|
178
|
+
},
|
|
179
|
+
async getRankKeysForBuilder(id) { return heldRanks(id).sort(); },
|
|
180
|
+
async getRankByKey(k) {
|
|
181
|
+
if (catalog.RANK_SEED[k]) return { rank_key: k, label: k, is_system: true, seeded_rank: k };
|
|
182
|
+
return copy(s.customRanks.get(k)) || null;
|
|
183
|
+
},
|
|
184
|
+
async createRank({ rankKey, label, description, createdBy }) {
|
|
185
|
+
if (catalog.RANK_SEED[rankKey] || s.customRanks.has(rankKey)) throw Object.assign(new Error('dup'), { code: '23505' });
|
|
186
|
+
const row = { rank_key: rankKey, label, description: description ?? null, is_system: false, seeded_rank: null, created_by: String(createdBy) };
|
|
187
|
+
s.customRanks.set(rankKey, row);
|
|
188
|
+
return copy(row);
|
|
189
|
+
},
|
|
190
|
+
async assignRank({ builderId, rankKey, assignedBy }) {
|
|
191
|
+
const id = String(builderId);
|
|
192
|
+
if (!s.builders.has(id)) throw Object.assign(new Error('fk'), { code: '23503' });
|
|
193
|
+
if (s.assignments.some((a) => a.builder_id === id && a.rank_key === rankKey)) return false;
|
|
194
|
+
s.assignments.push({ builder_id: id, rank_key: rankKey, assigned_by: assignedBy == null ? null : String(assignedBy) });
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
async unassignRank({ builderId, rankKey }) {
|
|
198
|
+
const before = s.assignments.length;
|
|
199
|
+
s.assignments = s.assignments.filter((a) => !(a.builder_id === String(builderId) && a.rank_key === rankKey));
|
|
200
|
+
return s.assignments.length < before;
|
|
201
|
+
},
|
|
202
|
+
async listAssignments() { return copy(s.assignments); },
|
|
203
|
+
async getActiveBuildersWithRank(rankKeys) {
|
|
204
|
+
return [...s.builders.values()]
|
|
205
|
+
.filter((b) => isLive(b) && heldRanks(b.id).some((r) => rankKeys.includes(r)))
|
|
206
|
+
.sort((a, b) => Number(a.id) - Number(b.id))
|
|
207
|
+
.map((b) => ({ builder_id: b.id, github_login: b.github_login, display_name: b.display_name }));
|
|
208
|
+
},
|
|
209
|
+
async builderHoldsRankActive(id, rankKeys) {
|
|
210
|
+
const b = s.builders.get(String(id));
|
|
211
|
+
return isLive(b) && heldRanks(id).some((r) => rankKeys.includes(r));
|
|
212
|
+
},
|
|
213
|
+
async countActiveBuilders() { return [...s.builders.values()].filter(isLive).length; },
|
|
214
|
+
async openBoardItem({ subjectType, subjectId, openedBy = null, constitution, windowMinutes = null }) {
|
|
215
|
+
if (s.items.some((i) => i.subject_type === subjectType && i.subject_id === String(subjectId) && !i.closed_at)) return null;
|
|
216
|
+
const row = {
|
|
217
|
+
id: next(), subject_type: subjectType, subject_id: String(subjectId),
|
|
218
|
+
opened_by: openedBy == null ? null : String(openedBy), opened_at: tick(),
|
|
219
|
+
closes_at: windowMinutes == null ? null : `in ${windowMinutes} minutes`,
|
|
220
|
+
closed_at: null, outcome: null, constitution: copy(constitution || {}),
|
|
221
|
+
};
|
|
222
|
+
s.items.push(row);
|
|
223
|
+
return copy(row);
|
|
224
|
+
},
|
|
225
|
+
async getOpenBoardItem(type, subjectId) {
|
|
226
|
+
return copy(s.items.find((i) => i.subject_type === type && i.subject_id === String(subjectId) && !i.closed_at)) || null;
|
|
227
|
+
},
|
|
228
|
+
async getBoardItemForVote(id) {
|
|
229
|
+
const i = s.items.find((x) => x.id === String(id));
|
|
230
|
+
return i ? withExpiry(i) : null;
|
|
231
|
+
},
|
|
232
|
+
async getBoardItemAuthor(item) {
|
|
233
|
+
if (!item) return null;
|
|
234
|
+
if (item.subject_type === 'full_idea') {
|
|
235
|
+
const idea = s.ideas.get(String(item.subject_id));
|
|
236
|
+
return idea ? idea.captured_by : null;
|
|
237
|
+
}
|
|
238
|
+
return item.opened_by ?? null;
|
|
239
|
+
},
|
|
240
|
+
async insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null }) {
|
|
241
|
+
if (s.votes.some((v) => v.item_id === String(itemId) && v.voter_id === String(voterId))) return null;
|
|
242
|
+
const row = { id: next(), item_id: String(itemId), voter_id: String(voterId), direction, reason_md: reasonMd, section_key: sectionKey, cast_at: tick() };
|
|
243
|
+
s.votes.push(row);
|
|
244
|
+
return copy(row);
|
|
245
|
+
},
|
|
246
|
+
async getBoardVote(itemId, voterId) {
|
|
247
|
+
return copy(s.votes.find((v) => v.item_id === String(itemId) && v.voter_id === String(voterId))) || null;
|
|
248
|
+
},
|
|
249
|
+
async insertVoteKarma({ voterId, itemId, delta }) { s.karma.push({ builder_id: String(voterId), reason: `vote:${itemId}`, delta }); },
|
|
250
|
+
async getVotesForItem(id) { return s.votes.filter((v) => v.item_id === String(id)).map(copy); },
|
|
251
|
+
async getVotesForItems(ids) { const want = ids.map(String); return s.votes.filter((v) => want.includes(v.item_id)).map(voteRow); },
|
|
252
|
+
async closeBoardItem({ itemId, outcome, afterClose = null }) {
|
|
253
|
+
const i = s.items.find((x) => x.id === String(itemId) && !x.closed_at);
|
|
254
|
+
if (!i) return null;
|
|
255
|
+
// A transaction: the close and whatever rides it land together or not at all.
|
|
256
|
+
const rulesBefore = new Map(s.rules);
|
|
257
|
+
i.closed_at = tick();
|
|
258
|
+
i.outcome = outcome;
|
|
259
|
+
if (typeof afterClose === 'function') {
|
|
260
|
+
try {
|
|
261
|
+
await afterClose({ query: async () => { throw new Error('walk store: no raw SQL inside a close'); } }, copy(i));
|
|
262
|
+
} catch (err) {
|
|
263
|
+
i.closed_at = null;
|
|
264
|
+
i.outcome = null;
|
|
265
|
+
s.rules = rulesBefore;
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return copy(i);
|
|
270
|
+
},
|
|
271
|
+
async createAmendment({ proposed, rationaleMd = null, proposedBy, kind = 'constitution' }) {
|
|
272
|
+
const row = { id: next(), proposed: copy(proposed), rationale_md: rationaleMd, proposed_by: String(proposedBy), kind };
|
|
273
|
+
s.amendments.set(row.id, row);
|
|
274
|
+
return copy(row);
|
|
275
|
+
},
|
|
276
|
+
async getAmendment(id) { return copy(s.amendments.get(String(id))) || null; },
|
|
277
|
+
async listAmendmentHistory(limit = 50) {
|
|
278
|
+
return s.items
|
|
279
|
+
.filter((i) => i.subject_type === 'constitutional_amendment' && i.closed_at)
|
|
280
|
+
.sort((a, b) => (a.closed_at < b.closed_at ? 1 : -1))
|
|
281
|
+
.slice(0, limit)
|
|
282
|
+
.map((i) => {
|
|
283
|
+
const a = s.amendments.get(i.subject_id) || {};
|
|
284
|
+
return {
|
|
285
|
+
item_id: i.id, amendment_id: i.subject_id, proposed: copy(a.proposed), rationale_md: a.rationale_md,
|
|
286
|
+
proposed_by: a.proposed_by, opened_at: i.opened_at, closed_at: i.closed_at, outcome: i.outcome,
|
|
287
|
+
decided_under: copy(i.constitution), kind: a.kind || 'constitution',
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
async listDecisionRules() { return [...s.rules.values()].map(copy); },
|
|
292
|
+
// The SQL's guard: written only by a PASSED decision-rules amendment item.
|
|
293
|
+
async applyDecisionRules(_client, { itemId, rules }) {
|
|
294
|
+
const i = s.items.find((x) => x.id === String(itemId));
|
|
295
|
+
const a = i && s.amendments.get(i.subject_id);
|
|
296
|
+
const allowed = i && a && i.subject_type === 'constitutional_amendment' && i.outcome === 'passed' && a.kind === 'decision_rules';
|
|
297
|
+
if (!allowed) throw new Error(`decision rules not written (item ${itemId} is not a passed decision-rules amendment)`);
|
|
298
|
+
for (const r of rules) {
|
|
299
|
+
s.rules.set(r.area, { area: r.area, decided_by: r.decided_by, rule: r.rule, who: r.who, set_by_item_id: String(itemId), updated_at: tick() });
|
|
300
|
+
}
|
|
301
|
+
return rules.length;
|
|
302
|
+
},
|
|
303
|
+
async createMatter({ area, title, detailMd = null, action = null, proposedBy }) {
|
|
304
|
+
const row = { id: next(), area, title, detail_md: detailMd, action: copy(action), proposed_by: String(proposedBy) };
|
|
305
|
+
s.matters.set(row.id, row);
|
|
306
|
+
return copy(row);
|
|
307
|
+
},
|
|
308
|
+
async getMatter(id) { return copy(s.matters.get(String(id))) || null; },
|
|
309
|
+
async getMatterSubjects(ids) { return ids.map((id) => s.matters.get(String(id))).filter(Boolean).map(copy); },
|
|
310
|
+
async getTaskStatuses(ids) {
|
|
311
|
+
return ids.map(String).filter((id) => s.tasks.has(id)).map((id) => ({ id, status: s.tasks.get(id).status }));
|
|
312
|
+
},
|
|
313
|
+
async listLatestGenesisStageItems() {
|
|
314
|
+
const latest = new Map();
|
|
315
|
+
for (const i of s.items.filter((x) => x.subject_type === 'genesis_stage')) latest.set(i.subject_id, i);
|
|
316
|
+
return [...latest.values()].map(withExpiry);
|
|
317
|
+
},
|
|
318
|
+
async insertAuthorRatifyKarma({ authorId, itemId, delta }) {
|
|
319
|
+
const reason = `board.item.ratified:${itemId}`;
|
|
320
|
+
if (s.karma.some((k) => k.reason === reason)) return false;
|
|
321
|
+
s.karma.push({ builder_id: String(authorId), reason, delta });
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
async getIdeaCompleteness(id) { const idea = s.ideas.get(String(id)); return idea ? idea.completeness : null; },
|
|
325
|
+
};
|
|
326
|
+
return { s, fns };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function installStore(govDb, fns, unexpected) {
|
|
330
|
+
for (const k of Object.keys(govDb)) {
|
|
331
|
+
if (typeof govDb[k] !== 'function') continue;
|
|
332
|
+
govDb[k] = fns[k] || (async () => {
|
|
333
|
+
unexpected.push(k);
|
|
334
|
+
throw new Error(`walk store: unexpected db call ${k}`);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── the walk ────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
async function runWalk() {
|
|
342
|
+
// The constitution is host content: point the instance root at a throwaway folder
|
|
343
|
+
// BEFORE anything reads it, so a ratified charter is written there (the real
|
|
344
|
+
// applyBoardAmendment) and never into this checkout.
|
|
345
|
+
const instanceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'government-walk-'));
|
|
346
|
+
process.env.BONGOS_INSTANCE_ROOT = instanceRoot;
|
|
347
|
+
delete process.env.DATABASE_URL; // the walk never touches a real database
|
|
348
|
+
// The modules' info lines (a fence switched, a budget set) would bury the table.
|
|
349
|
+
if (!process.env.LOG_LEVEL) process.env.LOG_LEVEL = 'warn';
|
|
350
|
+
|
|
351
|
+
const req = (rel) => require(path.join(ROOT, rel));
|
|
352
|
+
const api = req('src/module-api.js');
|
|
353
|
+
const seams = req('src/module-seams.js');
|
|
354
|
+
const authMod = req('src/bongos/auth.js');
|
|
355
|
+
const projectSettings = req('src/bongos/project-settings.js');
|
|
356
|
+
const { attachFail } = req('src/bongos/middleware/error-envelope.js');
|
|
357
|
+
const catalog = req('modules/government/catalog.js');
|
|
358
|
+
const govDb = req('modules/government/db.js');
|
|
359
|
+
const config = req('modules/government/config.js');
|
|
360
|
+
const board = req('modules/government/board.js');
|
|
361
|
+
const charter = req('modules/government/charter.js');
|
|
362
|
+
const membership = req('modules/government/board-membership.js');
|
|
363
|
+
const dr = req('modules/government/decision-rules.js');
|
|
364
|
+
const genesis = req('modules/onboarding/genesis-home.js');
|
|
365
|
+
const autoDb = req('modules/autonomy/db.js');
|
|
366
|
+
const lifeDb = req('modules/lifecycle/db.js');
|
|
367
|
+
const doneWhen = req('modules/lifecycle/done-when.js');
|
|
368
|
+
const doneWhenRoutes = req('modules/lifecycle/routes/done-when.js');
|
|
369
|
+
const gateRoutes = req('modules/lifecycle/routes/gate-approvals.js');
|
|
370
|
+
const express = require('express');
|
|
371
|
+
const http = require('http');
|
|
372
|
+
|
|
373
|
+
const { s: store, fns } = makeStore(catalog);
|
|
374
|
+
installStore(govDb, fns, store.unexpected);
|
|
375
|
+
|
|
376
|
+
// Project settings (founding mode + the plan) live in memory.
|
|
377
|
+
const settings = new Map();
|
|
378
|
+
projectSettings.getSetting = async (k) => (settings.has(k) ? { key: k, value: settings.get(k) } : null);
|
|
379
|
+
projectSettings.setSetting = async (k, v) => { settings.set(k, String(v)); return { key: k, value: String(v) }; };
|
|
380
|
+
|
|
381
|
+
// What passed matters write into other modules, recorded.
|
|
382
|
+
const effects = { fence: null, allowedGoals: [], overrides: [], gates: [], criteria: new Map([['9', false]]), budgets: new Map() };
|
|
383
|
+
autoDb.setFenceEnabled = async ({ enabled }) => { effects.fence = enabled; return { enabled }; };
|
|
384
|
+
autoDb.allowGoal = async ({ goalId }) => { effects.allowedGoals.push(String(goalId)); return { goal_id: goalId }; };
|
|
385
|
+
lifeDb.decideOverrideRequest = async ({ requestId, decision }) => { effects.overrides.push([String(requestId), decision]); return {}; };
|
|
386
|
+
gateRoutes.approveGatePr = async ({ number }) => { effects.gates.push(String(number)); return { ok: true, body: {} }; };
|
|
387
|
+
doneWhen.getCriterion = async (id) => (effects.criteria.has(String(id)) ? { id, satisfied: effects.criteria.get(String(id)) } : null);
|
|
388
|
+
doneWhenRoutes.satisfyByOverride = async ({ criterionId }) => { effects.criteria.set(String(criterionId), true); return { updated: {}, goalResult: {} }; };
|
|
389
|
+
|
|
390
|
+
// Ports the routers would otherwise reach a database for: the genesis seeder and
|
|
391
|
+
// the Treasury write. Registered first, so the routers' own registrations step aside.
|
|
392
|
+
const STAGE_TASK_IDS = { scope: '9001', strategize: '9002', charter: '9003', legalize: '9004', legislation: '9005' };
|
|
393
|
+
seams.registerProvider('lifecycle.kickoffSeed', {
|
|
394
|
+
seedGenesis: async () => {
|
|
395
|
+
for (const [stage, id] of Object.entries(STAGE_TASK_IDS)) store.tasks.set(id, { id, stage, title: `${stage} (walk)`, status: 'ready' });
|
|
396
|
+
return { seeded: 5 };
|
|
397
|
+
},
|
|
398
|
+
genesisTasks: async () => {
|
|
399
|
+
const out = {};
|
|
400
|
+
for (const t of store.tasks.values()) (out[t.stage] = out[t.stage] || []).push({ id: t.id, title: t.title, status: t.status });
|
|
401
|
+
return out;
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
seams.registerProvider('reward', {
|
|
405
|
+
setBuilderMonthlyBudget: async (id, usd) => {
|
|
406
|
+
if (!store.builders.has(String(id))) return null;
|
|
407
|
+
effects.budgets.set(String(id), usd);
|
|
408
|
+
return { id, monthly_budget_usd: usd };
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// The session: whoever the request says it is, from this project's people.
|
|
413
|
+
const asHeader = 'x-walk-builder';
|
|
414
|
+
const fakeBuilder = (r, res, nextFn) => {
|
|
415
|
+
const b = store.builders.get(String(r.headers[asHeader] || ''));
|
|
416
|
+
if (!b || b.status !== 'active') return res.status(401).json({ error: { code: 'not_signed_in' } });
|
|
417
|
+
r.builder = { id: Number(b.id), rank: b.rank, github_login: b.github_login, display_name: b.display_name };
|
|
418
|
+
return nextFn();
|
|
419
|
+
};
|
|
420
|
+
api.requireBuilder = fakeBuilder;
|
|
421
|
+
authMod.requireBuilder = fakeBuilder;
|
|
422
|
+
|
|
423
|
+
const app = express();
|
|
424
|
+
app.use(express.json());
|
|
425
|
+
app.use(attachFail);
|
|
426
|
+
for (const rel of ROUTERS) app.use(req(rel)());
|
|
427
|
+
const server = http.createServer(app);
|
|
428
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
429
|
+
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
430
|
+
|
|
431
|
+
async function call(as, method, p, body) {
|
|
432
|
+
const res = await fetch(`${BASE}${p}`, {
|
|
433
|
+
method,
|
|
434
|
+
headers: { [asHeader]: as, ...(body === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
435
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
436
|
+
});
|
|
437
|
+
let json = null;
|
|
438
|
+
try { json = await res.json(); } catch (_) { /* no body */ }
|
|
439
|
+
return { status: res.status, json };
|
|
440
|
+
}
|
|
441
|
+
const code = (r) => (r.json && r.json.error && r.json.error.code) || null;
|
|
442
|
+
const vote = (as, itemId, direction = 'yes') => call(as, 'POST', `/government/board/items/${itemId}/votes`,
|
|
443
|
+
direction === 'yes' ? { direction } : { direction, reason_md: 'Not yet: the walk objects.', section_key: 'purpose' });
|
|
444
|
+
const itemOf = (id) => store.items.find((i) => i.id === String(id)) || null;
|
|
445
|
+
const constitution = async () => (await call(FOUNDER, 'GET', '/government/constitution')).json;
|
|
446
|
+
const live = () => config.loadGovernmentConfig().board;
|
|
447
|
+
const stateOf = (id) => { const i = itemOf(id); return !i ? 'missing' : i.closed_at ? i.outcome : 'open'; };
|
|
448
|
+
|
|
449
|
+
// ── the table ──
|
|
450
|
+
const rows = [];
|
|
451
|
+
let current = '';
|
|
452
|
+
const section = (name) => { current = name; };
|
|
453
|
+
async function check(name, expected, fn) {
|
|
454
|
+
let got;
|
|
455
|
+
let pass = false;
|
|
456
|
+
try {
|
|
457
|
+
const out = await fn();
|
|
458
|
+
got = out.got;
|
|
459
|
+
pass = out.pass === true;
|
|
460
|
+
} catch (err) {
|
|
461
|
+
got = `threw: ${err && err.message}`;
|
|
462
|
+
}
|
|
463
|
+
const n = rows.filter((r) => r.section === current).length + 1;
|
|
464
|
+
rows.push({ section: current, key: `${current.split(' ')[0]}${n}`, name, expected, got: String(got), pass });
|
|
465
|
+
return pass;
|
|
466
|
+
}
|
|
467
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
468
|
+
|
|
469
|
+
// Every voter in `voters` votes; returns the state after each vote.
|
|
470
|
+
async function voteAll(itemId, voters, direction = 'yes') {
|
|
471
|
+
const after = [];
|
|
472
|
+
for (const v of voters) {
|
|
473
|
+
const r = await vote(v, itemId, direction);
|
|
474
|
+
after.push(r.status === 201 ? stateOf(itemId) : `${r.status} ${code(r)}`);
|
|
475
|
+
if (stateOf(itemId) !== 'open') break;
|
|
476
|
+
}
|
|
477
|
+
return after;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── setup: the council and the rank People's acts give ──
|
|
481
|
+
await call(FOUNDER, 'POST', '/government/ranks', { rank_key: 'council', label: 'Council' });
|
|
482
|
+
await call(FOUNDER, 'POST', '/government/ranks', { rank_key: 'tester', label: 'Tester' });
|
|
483
|
+
for (const id of COUNCIL) await call(FOUNDER, 'POST', '/government/assignments', { builder_id: Number(id), rank_key: 'council' });
|
|
484
|
+
|
|
485
|
+
// ══ 1. Genesis ══
|
|
486
|
+
section('G Genesis');
|
|
487
|
+
const CHOSEN = 'timocratic-republic';
|
|
488
|
+
await check('The test project is born a Monarchy (the state every genesis starts from)', 'reads as monarchy', async () => {
|
|
489
|
+
const f = charter.readCharter(live()).form;
|
|
490
|
+
return { got: `reads as ${f}`, pass: f === 'monarchy' };
|
|
491
|
+
});
|
|
492
|
+
await check('Founding starts', '200, stage decide', async () => {
|
|
493
|
+
const r = await call(FOUNDER, 'POST', '/founding/start');
|
|
494
|
+
const stage = r.json && r.json.mode && r.json.mode.stage;
|
|
495
|
+
return { got: `${r.status}, stage ${stage}`, pass: r.status === 200 && stage === 'decide' };
|
|
496
|
+
});
|
|
497
|
+
let charterItem = null;
|
|
498
|
+
await check(`Deciding with ${CHOSEN} saves the plan and files the pick as a charter amendment`, 'plan saved, charter sitting, one vote decides', async () => {
|
|
499
|
+
const r = await call(FOUNDER, 'POST', '/founding/plan', { government: CHOSEN });
|
|
500
|
+
const c = (r.json && r.json.charter) || {};
|
|
501
|
+
charterItem = c.item_id || null;
|
|
502
|
+
return { got: `${r.status}, charter ${c.state}, one vote decides ${c.one_vote_decides}`, pass: r.status === 200 && c.state === 'sitting' && c.one_vote_decides === true };
|
|
503
|
+
});
|
|
504
|
+
await check('Saving the plan moved the project to its first stage', 'stage scope', async () => {
|
|
505
|
+
const g = await genesis.readGenesis();
|
|
506
|
+
return { got: `stage ${g && g.stage}`, pass: !!g && g.stage === 'scope' };
|
|
507
|
+
});
|
|
508
|
+
await check('The founder\'s one yes ratifies the pick under the Monarchy it is born with', 'passed', async () => {
|
|
509
|
+
const r = await vote(FOUNDER, charterItem);
|
|
510
|
+
return { got: `${r.status} ${r.json && r.json.outcome}`, pass: r.status === 201 && r.json.outcome === 'passed' };
|
|
511
|
+
});
|
|
512
|
+
await check('The constitution now reads as the government chosen at decide', `reads as ${CHOSEN}, no divergence`, async () => {
|
|
513
|
+
const c = await constitution();
|
|
514
|
+
const f = c && c.charter && c.charter.reads_as && c.charter.reads_as.form;
|
|
515
|
+
return { got: `reads as ${f}, divergence ${c && c.divergence ? 'yes' : 'none'}`, pass: f === CHOSEN && !c.divergence };
|
|
516
|
+
});
|
|
517
|
+
await check('A stage with unfinished tasks cannot be put to the board', '409 stage_tasks_unfinished', async () => {
|
|
518
|
+
const r = await call(FOUNDER, 'POST', '/founding/stages/scope/close');
|
|
519
|
+
return { got: `${r.status} ${code(r)}`, pass: r.status === 409 && code(r) === 'stage_tasks_unfinished' };
|
|
520
|
+
});
|
|
521
|
+
const timocrats = ['42', '43', '44'];
|
|
522
|
+
for (const stage of genesis.PROPER) {
|
|
523
|
+
store.tasks.get(STAGE_TASK_IDS[stage]).status = 'shipped';
|
|
524
|
+
let itemId = null;
|
|
525
|
+
await check(`${stage}: its finished stage is put to the board`, 'a sitting under the chosen government', async () => {
|
|
526
|
+
const r = await call(FOUNDER, 'POST', `/founding/stages/${stage}/close`);
|
|
527
|
+
itemId = r.json && r.json.item_id;
|
|
528
|
+
const snap = itemId && itemOf(itemId) ? itemOf(itemId).constitution : {};
|
|
529
|
+
const reads = charter.readCharter(snap).form;
|
|
530
|
+
return { got: `${r.status}, sitting under ${reads}`, pass: r.status === 200 && !!itemId && reads === CHOSEN };
|
|
531
|
+
});
|
|
532
|
+
await check(`${stage}: the founder alone cannot close it; every trusted member's yes does`, 'open, open, passed', async () => {
|
|
533
|
+
const after = await voteAll(itemId, timocrats);
|
|
534
|
+
return { got: after.join(', '), pass: same(after, ['open', 'open', 'passed']) };
|
|
535
|
+
});
|
|
536
|
+
const nextStage = genesis.PROPER[genesis.PROPER.indexOf(stage) + 1];
|
|
537
|
+
await check(`${stage}: a pass moves the project on`, nextStage ? `stage ${nextStage}` : 'come-alive door opens', async () => {
|
|
538
|
+
const g = await genesis.readGenesis();
|
|
539
|
+
if (nextStage) return { got: `stage ${g && g.stage}`, pass: !!g && g.stage === nextStage };
|
|
540
|
+
return { got: `can come alive ${g && g.can_come_alive}`, pass: !!g && g.can_come_alive === true };
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
await check('The project comes alive', '200, founding over', async () => {
|
|
544
|
+
const r = await call(FOUNDER, 'POST', '/founding/alive');
|
|
545
|
+
const g = await genesis.readGenesis();
|
|
546
|
+
return { got: `${r.status}, founding ${g ? 'still on' : 'over'}`, pass: r.status === 200 && g === null };
|
|
547
|
+
});
|
|
548
|
+
await check('Alive, it is still governed by the choice', `reads as ${CHOSEN}`, async () => {
|
|
549
|
+
const f = charter.readCharter(live()).form;
|
|
550
|
+
return { got: `reads as ${f}`, pass: f === CHOSEN };
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ══ 2. Every form ══
|
|
554
|
+
section('F Forms');
|
|
555
|
+
let ideaSeq = 500;
|
|
556
|
+
// A probe: a Full Idea (by someone who left, so no author rule bends the count)
|
|
557
|
+
// put to the board in force, voted on to show who may vote and what passes.
|
|
558
|
+
async function probe() {
|
|
559
|
+
const id = String(++ideaSeq);
|
|
560
|
+
store.ideas.set(id, { captured_by: '47', completeness: 80 });
|
|
561
|
+
const out = await board.openWindowForFullIdea({ ideaId: id, completenessScore: 80 });
|
|
562
|
+
return out.item.id;
|
|
563
|
+
}
|
|
564
|
+
const FORMS = [
|
|
565
|
+
// key, how to switch, who is seated, the yes-path states, the objection path (voter order + states)
|
|
566
|
+
{ key: 'timocratic-republic', label: 'Timocratic Republic (in force from genesis)', body: null,
|
|
567
|
+
seated: ['42', '43', '44'], outsider: '45', yes: ['open', 'open', 'passed'], no: { voters: ['43', '42', '44'], dir: ['no', 'yes', 'yes'], states: ['open', 'open', 'returned'] },
|
|
568
|
+
rule: 'consent: a yes does not end it; full turnout passes; one objection returns it' },
|
|
569
|
+
{ key: 'direct-democracy', label: 'Direct Democracy', body: { form: 'direct-democracy' },
|
|
570
|
+
seated: ACTIVE_IDS, outsider: null, yes: ['open', 'open', 'open', 'open', 'passed'], no: { voters: ['46', '42', '43', '44', '45'], dir: ['no', 'yes', 'yes', 'yes', 'yes'], states: ['open', 'open', 'open', 'open', 'returned'] },
|
|
571
|
+
rule: 'consent over every builder' },
|
|
572
|
+
{ key: 'foundation', label: 'Foundation (council rank)', body: { form: 'foundation', council_rank: 'council' }, selfRemoval: true,
|
|
573
|
+
seated: COUNCIL, outsider: '42', yes: ['open', 'passed'], no: { voters: ['44', '43'], dir: ['no', 'yes'], states: ['open', 'returned'] },
|
|
574
|
+
rule: 'consent over the council only' },
|
|
575
|
+
{ key: 'custom', label: 'Every builder, majority (no named form)', body: { franchise: 'everyone', pass_rule: 'majority' }, passRule: 'majority',
|
|
576
|
+
seated: ACTIVE_IDS, outsider: null, yes: ['open', 'open', 'passed'], no: { voters: ['42', '43', '44'], dir: ['no', 'no', 'no'], states: ['open', 'open', 'returned'] },
|
|
577
|
+
rule: 'majority: 3 of 5 yes carries; 3 of 5 no makes it unreachable' },
|
|
578
|
+
{ key: 'custom', label: 'Every builder, unanimous (no named form)', body: { pass_rule: 'unanimous' }, passRule: 'unanimous',
|
|
579
|
+
seated: ACTIVE_IDS, outsider: null, yes: ['open', 'open', 'open', 'open', 'passed'], no: { voters: ['43'], dir: ['no'], states: ['returned'] },
|
|
580
|
+
rule: 'unanimous: one yes does not end it; full turnout passes; one objection returns it at once' },
|
|
581
|
+
{ key: 'monarchy', label: 'Monarchy / BDFL', body: { form: 'monarchy' },
|
|
582
|
+
seated: ['42'], outsider: '43', yes: ['passed'], no: { voters: ['42'], dir: ['no'], states: ['returned'] },
|
|
583
|
+
rule: 'first yes: the founder\'s one vote decides' },
|
|
584
|
+
];
|
|
585
|
+
let seatedNow = ['42', '43', '44']; // who sits under the form in force
|
|
586
|
+
for (const form of FORMS) {
|
|
587
|
+
const tag = form.label;
|
|
588
|
+
if (form.body) {
|
|
589
|
+
let itemId = null;
|
|
590
|
+
await check(`${tag}: the switch is filed as a charter amendment`, form.selfRemoval ? '409 proposer_would_lose_their_seat, then 201 once acknowledged' : '201', async () => {
|
|
591
|
+
let r = await call(FOUNDER, 'POST', '/government/board/charter', form.body);
|
|
592
|
+
let got = `${r.status}`;
|
|
593
|
+
if (form.selfRemoval) {
|
|
594
|
+
const first = `${r.status} ${code(r)}`;
|
|
595
|
+
r = await call(FOUNDER, 'POST', '/government/board/charter', { ...form.body, acknowledge_self_removal: true });
|
|
596
|
+
got = `${first}, then ${r.status}`;
|
|
597
|
+
itemId = r.json && r.json.item && r.json.item.id;
|
|
598
|
+
return { got, pass: first === '409 proposer_would_lose_their_seat' && r.status === 201 && !!itemId };
|
|
599
|
+
}
|
|
600
|
+
itemId = r.json && r.json.item && r.json.item.id;
|
|
601
|
+
return { got, pass: r.status === 201 && !!itemId };
|
|
602
|
+
});
|
|
603
|
+
await check(`${tag}: the government in force decides the switch`, 'passed once every seated member has said yes', async () => {
|
|
604
|
+
const after = await voteAll(itemId, seatedNow);
|
|
605
|
+
return { got: after.join(', '), pass: after[after.length - 1] === 'passed' };
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
await check(`${tag}: the constitution reads as it`, form.key === 'custom' ? `reads as Custom, pass rule ${form.passRule}` : `reads as ${form.key}`, async () => {
|
|
609
|
+
const c = await constitution();
|
|
610
|
+
const ra = (c && c.charter && c.charter.reads_as) || {};
|
|
611
|
+
const ok = ra.form === form.key && (!form.passRule || ra.pass_rule === form.passRule) && !c.divergence;
|
|
612
|
+
return { got: `reads as ${ra.form === 'custom' ? 'Custom' : ra.form}${form.passRule ? `, pass rule ${ra.pass_rule}` : ''}${c.divergence ? ', DIVERGES' : ''}`, pass: ok };
|
|
613
|
+
});
|
|
614
|
+
await check(`${tag}: who is seated`, `builders ${form.seated.join(', ')}`, async () => {
|
|
615
|
+
const members = (await membership.resolveBoardMembers(live())).map((m) => String(m.builder_id));
|
|
616
|
+
return { got: `builders ${members.join(', ') || 'none'}`, pass: same(members, form.seated) };
|
|
617
|
+
});
|
|
618
|
+
if (form.outsider) {
|
|
619
|
+
await check(`${tag}: someone not seated cannot vote`, '403 not_board_member', async () => {
|
|
620
|
+
const id = await probe();
|
|
621
|
+
const r = await vote(form.outsider, id);
|
|
622
|
+
return { got: `${r.status} ${code(r)}`, pass: r.status === 403 && code(r) === 'not_board_member' };
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
await check(`${tag}: what passes (${form.rule})`, form.yes.join(', '), async () => {
|
|
626
|
+
const id = await probe();
|
|
627
|
+
const after = await voteAll(id, form.seated);
|
|
628
|
+
return { got: after.join(', '), pass: same(after, form.yes) };
|
|
629
|
+
});
|
|
630
|
+
await check(`${tag}: what returns`, form.no.states.join(', '), async () => {
|
|
631
|
+
const id = await probe();
|
|
632
|
+
const after = [];
|
|
633
|
+
for (let k = 0; k < form.no.voters.length; k += 1) {
|
|
634
|
+
const r = await vote(form.no.voters[k], id, form.no.dir[k]);
|
|
635
|
+
after.push(r.status === 201 ? stateOf(id) : `${r.status} ${code(r)}`);
|
|
636
|
+
if (stateOf(id) !== 'open') break;
|
|
637
|
+
}
|
|
638
|
+
return { got: after.join(', '), pass: same(after, form.no.states) };
|
|
639
|
+
});
|
|
640
|
+
seatedNow = form.seated;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ══ 3. Every building ══
|
|
644
|
+
section('B Buildings');
|
|
645
|
+
// Who decides at a building is changed by an amendment, decided here under the
|
|
646
|
+
// Monarchy the walk restored: the founder's one yes.
|
|
647
|
+
async function setRules(rules) {
|
|
648
|
+
const r = await call(FOUNDER, 'POST', '/government/board/decision-rules', { rules });
|
|
649
|
+
const itemId = r.json && r.json.item && r.json.item.id;
|
|
650
|
+
if (r.status !== 201 || !itemId) return { ok: false, why: `${r.status} ${code(r)}` };
|
|
651
|
+
const v = await vote(FOUNDER, itemId);
|
|
652
|
+
return { ok: v.status === 201 && v.json.outcome === 'passed', why: `vote ${v.status} ${v.json && v.json.outcome}` };
|
|
653
|
+
}
|
|
654
|
+
const rowOf = async (area) => {
|
|
655
|
+
const c = await constitution();
|
|
656
|
+
return ((c.decision_rules && c.decision_rules.areas) || []).find((a) => a.area === area) || {};
|
|
657
|
+
};
|
|
658
|
+
for (const area of dr.AREAS.filter((a) => a.acts.length)) {
|
|
659
|
+
await check(`${area.name}: set to decide by vote (council, majority) by a passed amendment`, 'sitting, majority vote', async () => {
|
|
660
|
+
const out = await setRules([{ area: area.key, decided_by: 'rank:council', rule: 'majority' }]);
|
|
661
|
+
const row = await rowOf(area.key);
|
|
662
|
+
return { got: `${out.ok ? 'passed' : out.why}; ${row.sitting ? 'sitting' : 'as today'}, ${row.rule_words}`, pass: out.ok && row.sitting === true && row.rule === 'majority' };
|
|
663
|
+
});
|
|
664
|
+
for (const act of area.acts) {
|
|
665
|
+
await check(`${area.name}: acting alone on ${act} is refused`, `409 decided_by_sitting (${area.key})`, async () => {
|
|
666
|
+
const [method, p, body] = REQUESTS[act];
|
|
667
|
+
const r = await call(FOUNDER, method, p, body);
|
|
668
|
+
const d = (r.json && r.json.error && (r.json.error.details || r.json.error)) || {};
|
|
669
|
+
return { got: `${r.status} ${code(r)} (${d.area})`, pass: r.status === 409 && code(r) === 'decided_by_sitting' && d.area === area.key };
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
await check(`${area.name}: restored to as today, every one of its ${area.acts.length} routes reaches its own handler again`, 'none refused', async () => {
|
|
673
|
+
const out = await setRules([{ area: area.key, decided_by: 'gate' }]);
|
|
674
|
+
const bad = [];
|
|
675
|
+
for (const act of area.acts) {
|
|
676
|
+
const [method, p, body] = REQUESTS[act];
|
|
677
|
+
const r = await call(FOUNDER, method, p, body);
|
|
678
|
+
const reached = act.includes('/government/assignments') ? r.status < 300 : (r.status >= 400 && r.status < 500 && r.status !== 403 && code(r) !== 'decided_by_sitting');
|
|
679
|
+
if (!reached) bad.push(`${act} ${r.status} ${code(r)}`);
|
|
680
|
+
}
|
|
681
|
+
return { got: `${out.ok ? 'restored' : out.why}; ${bad.length ? bad.join('; ') : 'none refused'}`, pass: out.ok && bad.length === 0 };
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ══ 4. Every matter action ══
|
|
686
|
+
section('M Matters');
|
|
687
|
+
const SITTINGS = {
|
|
688
|
+
people: 'consent', automation: 'majority', release: 'unanimous', review: 'signoff', treasury: 'first',
|
|
689
|
+
};
|
|
690
|
+
await check('Each acting building is put to the council by one passed amendment', Object.entries(SITTINGS).map(([a, r]) => `${a} ${r}`).join(', '), async () => {
|
|
691
|
+
const out = await setRules(Object.entries(SITTINGS).map(([area, rule]) => ({ area, decided_by: 'rank:council', rule })));
|
|
692
|
+
const got = [];
|
|
693
|
+
for (const area of Object.keys(SITTINGS)) got.push(`${area} ${(await rowOf(area)).rule}`);
|
|
694
|
+
return { got: `${out.ok ? '' : `${out.why}; `}${got.join(', ')}`, pass: out.ok && same(got, Object.entries(SITTINGS).map(([a, r]) => `${a} ${r}`)) };
|
|
695
|
+
});
|
|
696
|
+
// Open a matter as the founder (not on the council), pass it with council votes.
|
|
697
|
+
async function passMatter(area, title, action, voters = COUNCIL, direction = 'yes') {
|
|
698
|
+
const r = await call(FOUNDER, 'POST', '/government/board/matters', { area, title, action });
|
|
699
|
+
const itemId = r.json && r.json.item && r.json.item.id;
|
|
700
|
+
if (r.status !== 201 || !itemId) return { outcome: `not opened: ${r.status} ${code(r)}` };
|
|
701
|
+
const after = await voteAll(itemId, voters, direction);
|
|
702
|
+
return { outcome: stateOf(itemId), after };
|
|
703
|
+
}
|
|
704
|
+
const holds = (id, rank) => store.assignments.some((a) => a.builder_id === id && a.rank_key === rank);
|
|
705
|
+
// Did the act happen? One observation per matter action in MATTER_PLAN.
|
|
706
|
+
const HAPPENED = {
|
|
707
|
+
'people.assign_rank': () => holds('46', 'tester'),
|
|
708
|
+
'people.unassign_rank': () => !holds('46', 'tester'),
|
|
709
|
+
'automation.switch_on': () => effects.fence === true,
|
|
710
|
+
'automation.switch_off': () => effects.fence === false,
|
|
711
|
+
'automation.allow_goal': () => effects.allowedGoals.includes('77'),
|
|
712
|
+
'release.approve_override': () => effects.overrides.some(([id, d]) => id === '5' && d === 'approve'),
|
|
713
|
+
'release.approve_gate': () => effects.gates.includes('1500'),
|
|
714
|
+
'review.satisfy_criterion': () => effects.criteria.get('9') === true,
|
|
715
|
+
'treasury.set_budget': () => effects.budgets.get('45') === 50,
|
|
716
|
+
};
|
|
717
|
+
const MATTERS = MATTER_PLAN.map(([area, title, action, effect]) => [area, title, action, effect, HAPPENED[`${area}.${action.type}`]]);
|
|
718
|
+
// How each building's sitting closes as Ben then Cy say yes, under its rule: the
|
|
719
|
+
// two-member consent, majority and unanimous sittings need both; first yes and
|
|
720
|
+
// sign-off (by someone who did not propose it) close on the first.
|
|
721
|
+
const CLOSES = { people: 'open, passed', automation: 'open, passed', release: 'open, passed', review: 'passed', treasury: 'passed' };
|
|
722
|
+
await check('Someone the building\'s row does not seat cannot vote on its matter', '403 not_board_member', async () => {
|
|
723
|
+
const r = await call(FOUNDER, 'POST', '/government/board/matters', { area: 'automation', title: 'Who may vote here?' });
|
|
724
|
+
const itemId = r.json && r.json.item && r.json.item.id;
|
|
725
|
+
const v = await vote(FOUNDER, itemId);
|
|
726
|
+
await voteAll(itemId, COUNCIL, 'no'); // close the probe so it carries out nothing
|
|
727
|
+
return { got: `${r.status}, then ${v.status} ${code(v)}`, pass: r.status === 201 && v.status === 403 && code(v) === 'not_board_member' };
|
|
728
|
+
});
|
|
729
|
+
for (const [area, title, action, effect, happened] of MATTERS) {
|
|
730
|
+
await check(`${area} ${action.type}: "${title}" passes under ${SITTINGS[area]} and is carried out`, `${CLOSES[area]}; ${effect}`, async () => {
|
|
731
|
+
const out = await passMatter(area, title, action);
|
|
732
|
+
const done = happened();
|
|
733
|
+
const steps = out.after ? out.after.join(', ') : out.outcome;
|
|
734
|
+
return { got: `${steps}; ${done ? effect : `NOT ${effect}`}`, pass: out.outcome === 'passed' && steps === CLOSES[area] && done };
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
await check('A returned matter carries out nothing', 'returned; fence unchanged', async () => {
|
|
738
|
+
const before = effects.fence;
|
|
739
|
+
const out = await passMatter('automation', 'Switch it on again', { type: 'switch_on' }, COUNCIL, 'no');
|
|
740
|
+
return { got: `${out.outcome}; fence ${effects.fence === before ? 'unchanged' : 'CHANGED'}`, pass: out.outcome === 'returned' && effects.fence === before };
|
|
741
|
+
});
|
|
742
|
+
await check('Every building restored to as today', 'all as today', async () => {
|
|
743
|
+
const out = await setRules(Object.keys(SITTINGS).map((area) => ({ area, decided_by: 'gate' })));
|
|
744
|
+
const c = await constitution();
|
|
745
|
+
const sitting = c.decision_rules.areas.filter((a) => a.sitting).map((a) => a.area);
|
|
746
|
+
return { got: `${out.ok ? '' : `${out.why}; `}${sitting.length ? `still sitting: ${sitting.join(', ')}` : 'all as today'}`, pass: out.ok && sitting.length === 0 };
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Any storage the fake did not model is a finding about the walk itself.
|
|
750
|
+
section('S Store');
|
|
751
|
+
await check('No code path reached storage the walk does not model', 'none', async () => {
|
|
752
|
+
const u = [...new Set(store.unexpected)];
|
|
753
|
+
return { got: u.length ? u.join(', ') : 'none', pass: u.length === 0 };
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
await new Promise((r) => server.close(r));
|
|
757
|
+
try { fs.rmSync(instanceRoot, { recursive: true, force: true }); } catch (_) { /* best effort */ }
|
|
758
|
+
return { mode: 'in-memory', rows };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ── the table, as Markdown ──────────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
const SECTION_TITLES = {
|
|
764
|
+
G: '1. Genesis: founded, decided, five stages closed, alive',
|
|
765
|
+
F: '2. Every form: who can vote and what passes',
|
|
766
|
+
B: '3. Every building: decide by vote refuses acting alone',
|
|
767
|
+
M: '4. Every matter action: passed and carried out',
|
|
768
|
+
S: 'The walk itself',
|
|
769
|
+
};
|
|
770
|
+
const cell = (v) => String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
771
|
+
|
|
772
|
+
function renderTable({ mode, rows }) {
|
|
773
|
+
const out = [];
|
|
774
|
+
out.push('<!-- GENERATED by scripts/gds/government-walk.js - do not hand-edit. Regenerate: node scripts/gds/government-walk.js --write -->');
|
|
775
|
+
out.push('');
|
|
776
|
+
out.push('# The government walk');
|
|
777
|
+
out.push('');
|
|
778
|
+
out.push('One repeatable walk over a throwaway test project\'s government (task 1004658, goal 1000111). Re-run it with `node scripts/gds/government-walk.js`; `tests/government_walk.mjs` re-runs it in CI and fails if any row below changes, so this table is kept true, not just written once.');
|
|
779
|
+
out.push('');
|
|
780
|
+
out.push(`**Mode: ${mode}.** Every request is real HTTP through the real routers, and every decision is the real code: the permission gate, board membership, the vote, the tally, the close, the charter apply (written to a throwaway folder), the decision-rules apply, the act-alone gate on every building, and each module's matter act. The storage underneath is an in-memory stand-in for the government tables with the same guards (no Postgres was available where this was built), and the writes a passed matter makes into other modules (the automation fence, override requests, criteria, the GitHub gate approval, budgets) are recorded in memory. A real-database run of this walk is not built yet.`);
|
|
781
|
+
out.push('');
|
|
782
|
+
out.push('The test project: Ada founds it (Archon); Ben and Cy are trusted builders (Metic) and sit on a Council rank; Dee (Thetes) and Eve (Xenos) are newer builders; Fay has left and only authors the probe ideas, so no probe is decided by its own author.');
|
|
783
|
+
out.push('');
|
|
784
|
+
const keys = Object.keys(SECTION_TITLES).filter((k) => rows.some((r) => r.section.startsWith(k)));
|
|
785
|
+
out.push('| Section | Pass | Fail |');
|
|
786
|
+
out.push('|---|---|---|');
|
|
787
|
+
for (const k of keys) {
|
|
788
|
+
const rs = rows.filter((r) => r.section.startsWith(k));
|
|
789
|
+
out.push(`| ${SECTION_TITLES[k]} | ${rs.filter((r) => r.pass).length} | ${rs.filter((r) => !r.pass).length} |`);
|
|
790
|
+
}
|
|
791
|
+
out.push(`| **All** | **${rows.filter((r) => r.pass).length}** | **${rows.filter((r) => !r.pass).length}** |`);
|
|
792
|
+
for (const k of keys) {
|
|
793
|
+
out.push('');
|
|
794
|
+
out.push(`## ${SECTION_TITLES[k]}`);
|
|
795
|
+
out.push('');
|
|
796
|
+
out.push('| # | Check | Expected | Got | Result |');
|
|
797
|
+
out.push('|---|---|---|---|---|');
|
|
798
|
+
for (const r of rows.filter((x) => x.section.startsWith(k))) {
|
|
799
|
+
const bug = KNOWN_BUGS[r.key];
|
|
800
|
+
const result = r.pass ? 'PASS' : `**FAIL**${bug ? ` (${bug})` : ''}`;
|
|
801
|
+
out.push(`| ${r.key} | ${cell(r.name)} | ${cell(r.expected)} | ${cell(r.got)} | ${result} |`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
out.push('');
|
|
805
|
+
return out.join('\n');
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function main(argv) {
|
|
809
|
+
// Logs from the modules go to stderr so the table on stdout stays readable.
|
|
810
|
+
const realLog = console.log;
|
|
811
|
+
console.log = (...a) => console.error(...a);
|
|
812
|
+
const result = await runWalk();
|
|
813
|
+
const md = renderTable(result);
|
|
814
|
+
console.log = realLog;
|
|
815
|
+
const fails = result.rows.filter((r) => !r.pass);
|
|
816
|
+
if (argv.includes('--check')) {
|
|
817
|
+
const committed = fs.existsSync(DOC_PATH) ? fs.readFileSync(DOC_PATH, 'utf8').replace(/\r\n/g, '\n') : '';
|
|
818
|
+
if (committed !== md) {
|
|
819
|
+
process.stdout.write(md);
|
|
820
|
+
process.stdout.write(`\ngovernment-walk: the committed table (${path.relative(ROOT, DOC_PATH)}) differs from this run. A row changed: find it above, then regenerate with node scripts/gds/government-walk.js --write (and file any new FAIL as a bug under goal 1000111).\n`);
|
|
821
|
+
return 1;
|
|
822
|
+
}
|
|
823
|
+
process.stdout.write(`government-walk: ${result.rows.length - fails.length} pass, ${fails.length} fail - matches the committed table.\n`);
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
process.stdout.write(md);
|
|
827
|
+
if (argv.includes('--write')) {
|
|
828
|
+
fs.writeFileSync(DOC_PATH, md);
|
|
829
|
+
process.stdout.write(`\nwrote ${path.relative(ROOT, DOC_PATH)}\n`);
|
|
830
|
+
}
|
|
831
|
+
process.stdout.write(`\ngovernment-walk: ${result.rows.length - fails.length} pass, ${fails.length} fail (${result.mode}).\n`);
|
|
832
|
+
return 0;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (require.main === module) {
|
|
836
|
+
main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (err) => {
|
|
837
|
+
console.error(err);
|
|
838
|
+
process.exit(2);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
module.exports = { runWalk, renderTable, REQUESTS, ROUTERS, MATTER_PLAN, KNOWN_BUGS };
|