@bongos/core 1.25.20 → 1.25.22
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 +213 -78
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +18 -0
- package/clients/bongos-client/index.cjs +18 -0
- package/clients/bongos-client/index.d.ts +34 -8
- package/clients/bongos-client/index.mjs +18 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +18 -0
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +3 -1
- package/docs/api/openapi.json +693 -125
- package/docs/api-reference.md +13 -4
- package/docs/copy-inventory.md +432 -427
- package/docs/copy-registry.json +571 -526
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +350 -344
- package/modules/government/board.js +8 -7
- package/modules/government/charter-advice.js +10 -3
- package/modules/government/charter.js +88 -4
- package/modules/government/migrations/government_039_vote_cast_via_filed.sql +22 -0
- package/modules/government/routes/government.js +12 -12
- package/modules/government/sole-member.js +75 -0
- package/modules/hall-ui/public/genesis-home.js +12 -5
- package/modules/lifecycle/kickoff-checklist.js +1 -1
- package/modules/lifecycle/routes/pay-type-gates.js +7 -2
- package/modules/onboarding/genesis-home.js +14 -2
- package/modules/onboarding/project-facts.js +318 -0
- package/modules/onboarding/routes/genesis-work.js +16 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-standing.js +49 -0
- package/modules/platform-identity/routes/my-projects.js +8 -0
- package/modules/provisioning/grants.js +126 -0
- package/modules/provisioning/migrations/provisioning_038_render_promote.sql +43 -0
- package/modules/provisioning/migrations/provisioning_039_project_grants.sql +40 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/project-facts.js +55 -0
- package/modules/provisioning/project-refusal.js +107 -0
- package/modules/provisioning/provisioning.js +6 -3
- package/modules/provisioning/render-lookup.js +36 -1
- package/modules/provisioning/render-promote.js +80 -0
- package/modules/provisioning/render-sandbox.js +6 -0
- package/modules/provisioning/route-helpers.js +70 -16
- package/modules/provisioning/routes/env-manifest.js +21 -5
- package/modules/provisioning/routes/grants.js +141 -0
- package/modules/provisioning/routes/provisioning.js +8 -7
- package/modules/provisioning/routes/render-standup.js +82 -3
- package/modules/public-landing/public/projects.html +290 -32
- package/package-lock.json +2 -2
- package/package.json +2 -1
- package/release-notes.json +32 -0
- package/scripts/bongos/genesis.js +49 -3
- package/scripts/bongos/government-walk-sole.js +41 -0
- package/scripts/bongos/government-walk.js +15 -16
- package/scripts/bongos/init.js +13 -0
- package/scripts/bongos/package-core.js +2 -0
- package/scripts/bongos/provision-render-common.js +48 -0
- package/scripts/bongos/provision-render-promote.js +93 -0
- package/scripts/bongos/provision-render-sandbox.js +19 -3
- package/scripts/bongos/provision-render.js +4 -36
- package/scripts/bongos/provision.js +1 -0
- package/scripts/bongos/render-api.js +17 -2
- package/scripts/bongos/render-payload.js +5 -2
- package/scripts/gds/government-walk-sole.js +6 -0
- package/scripts/gds/provision-render-common.js +6 -0
- package/scripts/gds/provision-render-promote.js +6 -0
- package/src/bongos/folder-upload.js +243 -0
- package/src/bongos/routes/github.js +109 -16
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/src/sandbox/gate.js +164 -0
- package/src/sandbox/index.js +144 -0
- package/tests/fleet_cost_view.mjs +2 -0
- package/tests/folder_upload_project.mjs +396 -0
- package/tests/genesis_government.mjs +5 -1
- package/tests/genesis_home.mjs +17 -0
- package/tests/genesis_work_routes.mjs +25 -1
- package/tests/government_charter_advice.mjs +49 -0
- package/tests/government_charter_forms.mjs +14 -3
- package/tests/government_seed.mjs +1 -0
- package/tests/government_sole_member.mjs +153 -0
- package/tests/project_catalog_routes.mjs +2 -0
- package/tests/project_facts.mjs +158 -0
- package/tests/projects_hub.mjs +1 -1
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_env_manifest.mjs +4 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +2 -0
- package/tests/provisioning_project_grants.mjs +458 -0
- package/tests/provisioning_project_owner_gate.mjs +3 -1
- package/tests/provisioning_project_refusal.mjs +316 -0
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +2 -0
- package/tests/provisioning_settings.mjs +2 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/public_landing_projects.mjs +27 -5
- package/tests/render_promote.mjs +309 -0
- package/tests/render_sandbox.mjs +16 -8
- package/tests/sandbox_gate.mjs +182 -0
|
@@ -148,3 +148,52 @@ test('the port entry reads the head count and the law book', async () => {
|
|
|
148
148
|
assert.equal(expected.headcount, 2, 'the founder\'s expected number wins over the live count');
|
|
149
149
|
assert.equal(expected.charter.key, 'monarchy');
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
// ── task 1005130: every project is already a monarchy, and is told so ────────
|
|
153
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
154
|
+
const config = require(path.join(ROOT, 'modules', 'government', 'config.js'));
|
|
155
|
+
|
|
156
|
+
test('the default constitution reads as Monarchy, and the stage says so on arrival', () => {
|
|
157
|
+
const live = config.loadGovernmentConfig().board;
|
|
158
|
+
const v = charter.charterView(live);
|
|
159
|
+
assert.equal(v.in_force.form, 'monarchy', 'every project boots as a monarchy');
|
|
160
|
+
assert.match(v.in_force.line, /runs as Monarchy \/ BDFL now\. Every project opens as one/);
|
|
161
|
+
assert.match(v.in_force.line, /You decide\. Each decision takes one yes from you\./);
|
|
162
|
+
assert.deepEqual(v.forms.filter((f) => f.current).map((f) => f.key), ['monarchy'], 'exactly the form in force is marked');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('every form says who decides and what closing takes, derived from its own settings', () => {
|
|
166
|
+
const v = charter.charterView(MONARCHY);
|
|
167
|
+
for (const f of v.forms.filter((x) => x.available)) {
|
|
168
|
+
const d = charter.dutyOf(f.franchise, f.pass_rule);
|
|
169
|
+
assert.equal(f.who_decides, d.who_decides, f.key);
|
|
170
|
+
assert.equal(f.to_close, d.to_close, f.key);
|
|
171
|
+
assert.ok(f.who_decides && f.to_close, `${f.key} says both`);
|
|
172
|
+
}
|
|
173
|
+
assert.equal(charter.dutyOf('founder', 'first_ratifier').to_close, 'Each decision takes one yes from you.');
|
|
174
|
+
assert.match(charter.dutyOf('everyone', 'majority').to_close, /more than half/);
|
|
175
|
+
assert.notEqual(charter.dutyOf('trusted', 'consent').to_close, charter.dutyOf('trusted', 'majority').to_close,
|
|
176
|
+
'the sentence follows the pass rule, so a tuned rule is described as it runs');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('choosing what is in force changes nothing and is done; choosing another says what happens next', () => {
|
|
180
|
+
const same = charter.choiceOutcome(MONARCHY, 'monarchy');
|
|
181
|
+
assert.equal(same.changes, false);
|
|
182
|
+
assert.equal(same.complete, true);
|
|
183
|
+
assert.match(same.line, /already a Monarchy \/ BDFL\. Nothing to change/);
|
|
184
|
+
const other = charter.choiceOutcome(MONARCHY, 'direct-democracy');
|
|
185
|
+
assert.equal(other.changes, true);
|
|
186
|
+
assert.match(other.line, /filed as a change to the rules and decided under the government in force \(Monarchy \/ BDFL\)\. Under it, your own one yes applies it\./);
|
|
187
|
+
const underCircle = charter.choiceOutcome({ ...MONARCHY, membership: 'rank:xenos+', pass_rule: 'consent' }, 'monarchy');
|
|
188
|
+
assert.match(underCircle.line, /passes unless one of them objects/, 'under another government the rule in force is named');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the charter stage advice carries what is in force and what choosing the recommendation does', () => {
|
|
192
|
+
const a = advice.buildAdvice({ answers: {}, headcount: 1, board: MONARCHY });
|
|
193
|
+
assert.equal(a.in_force.form, 'monarchy');
|
|
194
|
+
assert.equal(a.charter.key, 'monarchy');
|
|
195
|
+
assert.equal(a.charter.on_choose.changes, false, 'a solo founder is told the recommendation is what they already have');
|
|
196
|
+
const b = advice.buildAdvice({ answers: { final_say: 'everyone' }, headcount: 1, board: MONARCHY });
|
|
197
|
+
assert.equal(b.charter.on_choose.changes, true);
|
|
198
|
+
assert.ok(b.forms.find((f) => f.key === 'monarchy').current);
|
|
199
|
+
});
|
|
@@ -254,8 +254,12 @@ test('a charter that changes nothing files nothing, and a council must be a rank
|
|
|
254
254
|
getRankByKey: async (key) => (key === 'trustees' ? { rank_key: 'trustees' } : null),
|
|
255
255
|
builderHoldsRankActive: async () => true,
|
|
256
256
|
}, async () => {
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
const same = await board.proposeCharter({ form: 'monarchy', proposedBy: '42' });
|
|
258
|
+
assert.equal(same.reason, 'no_change', 'a sitting on the constitution already in force would decide nothing');
|
|
259
|
+
// task 1005130: the refusal says what is true in words and counts the choice done
|
|
260
|
+
assert.equal(same.in_force, 'Monarchy / BDFL');
|
|
261
|
+
assert.equal(same.complete, true);
|
|
262
|
+
assert.match(same.message, /You are already a Monarchy \/ BDFL\. Nothing to change/);
|
|
259
263
|
assert.equal((await board.proposeCharter({ form: 'foundation', councilRank: 'ghosts', proposedBy: '42' })).reason, 'unknown_council_rank');
|
|
260
264
|
assert.equal((await board.proposeCharter({ form: 'direct-democracy', proposedBy: null })).reason, 'missing_proposer');
|
|
261
265
|
});
|
|
@@ -272,7 +276,14 @@ test('the constitution view carries the charter: forms, variables, and what the
|
|
|
272
276
|
});
|
|
273
277
|
const routes = fs.readFileSync(path.join(GOV, 'routes', 'government.js'), 'utf8');
|
|
274
278
|
assert.match(routes, /router\.post\('\/government\/board\/charter', api\.requireBuilder, api\.requirePermission\('board\.item\.open'\)/);
|
|
275
|
-
|
|
279
|
+
// The port's charter filing IS board.proposeCharter, followed by the board-of-one
|
|
280
|
+
// confirm (task 1005130) — called, not read: a refusal from board comes back as is.
|
|
281
|
+
const { governmentPort } = require(path.join(GOV, 'routes', 'government.js'));
|
|
282
|
+
const real = board.proposeCharter;
|
|
283
|
+
board.proposeCharter = async (a) => ({ ok: false, reason: 'from_board', seen: a.form });
|
|
284
|
+
try {
|
|
285
|
+
assert.deepEqual(await governmentPort.proposeCharter({ form: 'monarchy', proposedBy: '42' }), { ok: false, reason: 'from_board', seen: 'monarchy' });
|
|
286
|
+
} finally { board.proposeCharter = real; }
|
|
276
287
|
const pins = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'), 'utf8');
|
|
277
288
|
assert.match(pins, /'POST \/government\/board\/charter': 'perm:board\.item\.open'/);
|
|
278
289
|
});
|
|
@@ -55,6 +55,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
55
55
|
'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
|
|
56
56
|
'government_037_constitution.sql', // the constitution in force as a durable row (task 1005140)
|
|
57
57
|
'government_038_vote_cast_via.sql', // HOW a vote was cast: a ballot, or a stage-close confirm (task 1005121)
|
|
58
|
+
'government_039_vote_cast_via_filed.sql', // a board of one's confirm given by filing (task 1005130)
|
|
58
59
|
]);
|
|
59
60
|
|
|
60
61
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tests/government_sole_member.mjs — a board of ONE decides by filing (task 1005130,
|
|
2
|
+
// part C; the ruling task 1005121 shipped for closing a genesis stage, applied
|
|
3
|
+
// wherever it holds).
|
|
4
|
+
//
|
|
5
|
+
// The rule held here, for a charter change, a laws change and a decision-rules change:
|
|
6
|
+
// • the sole member of a first_ratifier board files → their yes is cast in the same
|
|
7
|
+
// call, recorded as THEIR vote (attributed, cast_via 'filed'), and the sitting
|
|
8
|
+
// closes through the ordinary close path;
|
|
9
|
+
// • a second member on the board, a consent or majority rule, or a caller who is not
|
|
10
|
+
// the sole member → the filing comes back exactly as it was, and nothing is cast;
|
|
11
|
+
// • the yes goes through the real castVote — its membership re-check, and a
|
|
12
|
+
// 'filed' cast is refused on anything but an amendment, or as a 'no';
|
|
13
|
+
// • a filing the filer was refused comes back untouched, and a failing confirm
|
|
14
|
+
// leaves the sitting open rather than failing the filing.
|
|
15
|
+
// The real routes (permission gate first, a non-member's filing left open, a lower
|
|
16
|
+
// rank refused before anything is filed) are walked end to end in
|
|
17
|
+
// scripts/bongos/government-walk.js (rows G3, G5, B*, L2–L4, L11–L15).
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const GOV = path.join(ROOT, 'modules', 'government');
|
|
27
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
28
|
+
const board = require(path.join(GOV, 'board.js'));
|
|
29
|
+
const sole = require(path.join(GOV, 'sole-member.js'));
|
|
30
|
+
|
|
31
|
+
const MONARCHY = { membership: 'rank:archon', pass_rule: 'first_ratifier', window_minutes: null, close_early_on_full_turnout: true };
|
|
32
|
+
const FILERS = ['proposeCharter', 'proposeLaws', 'proposeDecisionRules'];
|
|
33
|
+
|
|
34
|
+
// A world: who holds which rank, the one open item a filing opened, and every vote
|
|
35
|
+
// written. db is stubbed underneath the REAL castVote; the filers and the close are
|
|
36
|
+
// stubbed on board (looked up at call time), so this exercises the decision alone.
|
|
37
|
+
async function world({ members = ['42'], constitution = MONARCHY, subjectType = 'constitutional_amendment', filedOk = true } = {}, fn) {
|
|
38
|
+
const votes = [];
|
|
39
|
+
const closes = [];
|
|
40
|
+
const item = { id: 7, subject_type: subjectType, constitution, closed_at: null, expired: false };
|
|
41
|
+
const stubs = {
|
|
42
|
+
getActiveBuildersWithRank: async () => members.map((id) => ({ builder_id: id, github_login: `b${id}`, display_name: `B${id}` })),
|
|
43
|
+
builderHoldsRankActive: async (id) => members.includes(String(id)),
|
|
44
|
+
getBoardItemForVote: async () => ({ ...item }),
|
|
45
|
+
getBoardItemAuthor: async () => '42',
|
|
46
|
+
insertBoardVote: async (v) => { const row = { item_id: String(v.itemId), voter_id: String(v.voterId), direction: v.direction, cast_via: v.castVia || 'ballot', cast_at: '2026-11-13T12:00:00.000Z' }; votes.push(row); return row; },
|
|
47
|
+
getBoardVote: async () => null,
|
|
48
|
+
insertVoteKarma: async () => { throw new Error('a confirm given by filing earns no vote karma'); },
|
|
49
|
+
};
|
|
50
|
+
const boardStubs = {
|
|
51
|
+
maybeCloseAfterVote: async (id) => { closes.push(id); return { closed: true, outcome: 'passed', item: { id } }; },
|
|
52
|
+
};
|
|
53
|
+
for (const f of FILERS) boardStubs[f] = async () => (filedOk ? { ok: true, amendment: { id: 3 }, item: { ...item } } : { ok: false, reason: 'proposer_would_lose_their_seat' });
|
|
54
|
+
const saved = {};
|
|
55
|
+
for (const [k, v] of Object.entries(stubs)) { saved[`db.${k}`] = db[k]; db[k] = v; }
|
|
56
|
+
for (const [k, v] of Object.entries(boardStubs)) { saved[`board.${k}`] = board[k]; board[k] = v; }
|
|
57
|
+
try {
|
|
58
|
+
return await fn({ votes, closes });
|
|
59
|
+
} finally {
|
|
60
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
61
|
+
const [obj, key] = k.split('.');
|
|
62
|
+
(obj === 'db' ? db : board)[key] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const filer of FILERS) {
|
|
68
|
+
test(`${filer}: the sole member of a one-yes board decides by filing, and the vote is theirs on the record`, async () => {
|
|
69
|
+
await world({}, async ({ votes, closes }) => {
|
|
70
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
71
|
+
assert.equal(out.ok, true);
|
|
72
|
+
assert.deepEqual(out.decided, { closed: true, outcome: 'passed', cast_via: 'filed' });
|
|
73
|
+
assert.equal(votes.length, 1);
|
|
74
|
+
assert.equal(votes[0].voter_id, '42', 'attributed to the member who filed');
|
|
75
|
+
assert.equal(votes[0].direction, 'yes');
|
|
76
|
+
assert.equal(votes[0].cast_via, 'filed', 'the record says how it was cast');
|
|
77
|
+
assert.ok(votes[0].cast_at, 'and when');
|
|
78
|
+
assert.deepEqual(closes, [7], 'closed through the ordinary close path');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test(`${filer}: a second member on the board — nothing is cast, the sitting waits`, async () => {
|
|
83
|
+
await world({ members: ['42', '43'] }, async ({ votes, closes }) => {
|
|
84
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
85
|
+
assert.equal(out.decided, undefined);
|
|
86
|
+
assert.deepEqual(votes, []);
|
|
87
|
+
assert.deepEqual(closes, []);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
for (const rule of ['consent', 'majority', 'unanimous']) {
|
|
92
|
+
test(`${filer}: under ${rule} a sole member's filing still waits for their vote`, async () => {
|
|
93
|
+
await world({ constitution: { ...MONARCHY, pass_rule: rule } }, async ({ votes, closes }) => {
|
|
94
|
+
const out = await sole[filer]({ proposedBy: '42' });
|
|
95
|
+
assert.equal(out.decided, undefined);
|
|
96
|
+
assert.deepEqual(votes, []);
|
|
97
|
+
assert.deepEqual(closes, []);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test(`${filer}: a caller who is not the sole member — the sitting is left open, exactly as today`, async () => {
|
|
103
|
+
await world({ members: ['42'] }, async ({ votes, closes }) => {
|
|
104
|
+
const out = await sole[filer]({ proposedBy: '43' });
|
|
105
|
+
assert.equal(out.ok, true);
|
|
106
|
+
assert.equal(out.decided, undefined);
|
|
107
|
+
assert.deepEqual(votes, []);
|
|
108
|
+
assert.deepEqual(closes, []);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test(`${filer}: a refused filing comes back untouched, and nothing is cast`, async () => {
|
|
113
|
+
await world({ filedOk: false }, async ({ votes }) => {
|
|
114
|
+
assert.deepEqual(await sole[filer]({ proposedBy: '42' }), { ok: false, reason: 'proposer_would_lose_their_seat' });
|
|
115
|
+
assert.deepEqual(votes, []);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('the yes goes through the real castVote: membership is re-checked under the item\'s snapshot', async () => {
|
|
121
|
+
// resolve says 42 is the one member, but the per-request re-check says 42 no longer
|
|
122
|
+
// holds the rank (demoted between the two reads): the cast is refused, nothing closes.
|
|
123
|
+
await world({}, async ({ votes, closes }) => {
|
|
124
|
+
db.builderHoldsRankActive = async () => false;
|
|
125
|
+
const out = await sole.proposeLaws({ proposedBy: '42' });
|
|
126
|
+
assert.equal(out.decided, undefined);
|
|
127
|
+
assert.deepEqual(votes, []);
|
|
128
|
+
assert.deepEqual(closes, []);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('a cast marked "filed" is refused on anything but an amendment, and as a no', async () => {
|
|
133
|
+
await world({ subjectType: 'full_idea' }, async ({ votes }) => {
|
|
134
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
135
|
+
assert.deepEqual(votes, []);
|
|
136
|
+
});
|
|
137
|
+
await world({ subjectType: 'genesis_stage' }, async () => {
|
|
138
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
139
|
+
});
|
|
140
|
+
await world({}, async () => {
|
|
141
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'no', reasonMd: 'no', sectionKey: 'purpose', castVia: 'filed' })).reason, 'bad_cast_via');
|
|
142
|
+
assert.equal((await board.castVote({ itemId: 7, voterId: '42', direction: 'yes', castVia: 'by_magic' })).reason, 'bad_cast_via');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('a confirm that fails leaves the filing standing and the sitting open', async () => {
|
|
147
|
+
await world({}, async () => {
|
|
148
|
+
board.maybeCloseAfterVote = async () => { throw new Error('db down'); };
|
|
149
|
+
const out = await sole.proposeCharter({ proposedBy: '42' });
|
|
150
|
+
assert.equal(out.ok, true);
|
|
151
|
+
assert.equal(out.decided, undefined);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -50,6 +50,8 @@ let published = [];
|
|
|
50
50
|
let healed = [];
|
|
51
51
|
|
|
52
52
|
provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
|
|
53
|
+
// The gate also admits an owner-made grant (task 1004161, ADR 0407); nobody in this file holds one.
|
|
54
|
+
require('../modules/provisioning/grants.js').liveGrant = async () => null;
|
|
53
55
|
provisioning.getInstanceBySlug = async (_db, slug) => (instance && instance.slug === slug ? { ...instance } : null);
|
|
54
56
|
provisioning.getInstanceByDomain = async () => null;
|
|
55
57
|
provisioning.listInstancesForOwner = async () => []; // the shape gate's count (task 1004412): no other hosted project
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// tests/project_facts.mjs — the project's own facts, carried from the hub, and each reward
|
|
2
|
+
// option's real implications at the point of choice (task 1005129).
|
|
3
|
+
//
|
|
4
|
+
// Everything RUNS: the hub's real companion line (provisioning.js SETTINGS_COMPANION_ENV →
|
|
5
|
+
// project-facts.js) decoded by the project's real reader, the real soft-limit table carried
|
|
6
|
+
// as data, and the project's facts + options over an in-memory project_settings store and
|
|
7
|
+
// a fake `lifecycle.payTypeGates` port built from the REAL gate table.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/project_facts.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
17
|
+
const hub = require('../modules/provisioning/project-facts.js');
|
|
18
|
+
const pf = require('../modules/onboarding/project-facts.js');
|
|
19
|
+
const gatesLib = require('../modules/lifecycle/pay-type-gates.js');
|
|
20
|
+
const { PAY_TYPE_LABELS } = require('../modules/lifecycle/pay-terms.js');
|
|
21
|
+
|
|
22
|
+
// The real gate verdicts with nothing on file (or with the given pay types cleared).
|
|
23
|
+
function payGates(cleared = []) {
|
|
24
|
+
return {
|
|
25
|
+
labels: PAY_TYPE_LABELS,
|
|
26
|
+
readGates: async () => {
|
|
27
|
+
const types = [...Object.keys(gatesLib.REQUIREMENTS), 'eu_labour'];
|
|
28
|
+
return types.map((p) => gatesLib.withoutEvidence(gatesLib.gateVerdict(p, cleared.includes(p)
|
|
29
|
+
? gatesLib.requirementIds(p).map((requirement) => ({ pay_type: p, requirement, state: 'cleared' })) : [])));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function project({ detail, gates = payGates() } = {}) {
|
|
35
|
+
const store = new Map();
|
|
36
|
+
const line = detail ? provisioning.SETTINGS_COMPANION_ENV.PROJECT_FACTS({ detail }) : '';
|
|
37
|
+
return {
|
|
38
|
+
store,
|
|
39
|
+
deps: () => ({
|
|
40
|
+
getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
|
|
41
|
+
setSetting: async (k, v) => { store.set(k, String(v)); },
|
|
42
|
+
factsMarker: pf.factsMarker({ projectFacts: line }),
|
|
43
|
+
payGates: gates,
|
|
44
|
+
now: () => new Date('2026-11-13T12:00:00Z'),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('the hub carries only the facts a stage reads, and the reward rules as data', () => {
|
|
50
|
+
const line = provisioning.SETTINGS_COMPANION_ENV.PROJECT_FACTS({ detail: {
|
|
51
|
+
home_country: 'DE', builder_geography: 'global', entity: 'nonprofit', minors_on_board: false,
|
|
52
|
+
reward_intents: ['cash', 'equity'], framing: 'managed', project_state: 'new',
|
|
53
|
+
} });
|
|
54
|
+
const m = JSON.parse(Buffer.from(line, 'base64url').toString('utf8'));
|
|
55
|
+
assert.deepEqual(Object.keys(m.facts).sort(), ['builder_geography', 'entity', 'home_country', 'minors_on_board', 'reward_intents']);
|
|
56
|
+
assert.ok(!('framing' in m.facts), 'nothing a stage does not read crosses');
|
|
57
|
+
assert.ok(m.rules.length > 0 && m.rules.every((r) => r.options.some((o) => o.startsWith('reward_intents'))), 'only reward rules ride along');
|
|
58
|
+
assert.deepEqual(m.rules.map((r) => r.id), hub.REWARD_RULES.map((r) => r.id), 'read from the soft-limit table, never rewritten');
|
|
59
|
+
assert.equal(provisioning.SETTINGS_COMPANION_ENV.PROJECT_FACTS({ detail: {} }), '', 'nothing answered: an empty line, not a guess');
|
|
60
|
+
assert.equal(provisioning.SETTINGS_COMPANION_ENV.PROJECT_FACTS({}), '');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the project seeds its own copy once, as carried over; the founder\'s edit wins after', async () => {
|
|
64
|
+
const p = project({ detail: { home_country: 'US', home_region: 'US-CA', builder_geography: 'us-only' } });
|
|
65
|
+
let r = await pf.readFacts(p.deps());
|
|
66
|
+
const geo = r.facts.find((f) => f.key === 'builder_geography');
|
|
67
|
+
assert.equal(geo.value, 'us-only');
|
|
68
|
+
assert.equal(geo.origin, 'carried_over');
|
|
69
|
+
assert.deepEqual(r.unanswered, ['minors_on_board', 'entity', 'reward_intents'], 'never answered reads as not answered');
|
|
70
|
+
const w = await pf.setFacts({ builder_geography: 'global' }, 1, p.deps());
|
|
71
|
+
assert.equal(w.ok, true);
|
|
72
|
+
r = await pf.readFacts(p.deps());
|
|
73
|
+
assert.equal(r.facts.find((f) => f.key === 'builder_geography').value, 'global', 'the copy is editable, not a frozen snapshot');
|
|
74
|
+
assert.equal(r.facts.find((f) => f.key === 'builder_geography').origin, 'founder');
|
|
75
|
+
assert.equal(r.facts.find((f) => f.key === 'home_country').origin, 'carried_over');
|
|
76
|
+
// Un-answering goes back to "not answered".
|
|
77
|
+
await pf.setFacts({ home_region: null }, 1, p.deps());
|
|
78
|
+
assert.ok((await pf.readFacts(p.deps())).unanswered.includes('home_region'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a project not made through the hub has every fact "not answered" and is not refused', async () => {
|
|
82
|
+
const p = project({});
|
|
83
|
+
const r = await pf.readOptions(p.deps());
|
|
84
|
+
assert.equal(r.unanswered.length, 6);
|
|
85
|
+
assert.equal(r.facts.every((f) => f.value === null), true);
|
|
86
|
+
assert.ok(r.options.length > 0, 'the options are still stated, from the enforced table');
|
|
87
|
+
assert.ok(r.options.every((o) => o.facts.length === 0), 'no fact note fires on an answer nobody gave');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('every option states what the server enforces and the facts beside it, kept apart', async () => {
|
|
91
|
+
const p = project({ detail: { home_country: 'US', builder_geography: 'us-eu', entity: 'nonprofit', minors_on_board: true } });
|
|
92
|
+
const r = await pf.readOptions(p.deps());
|
|
93
|
+
const cash = r.options.find((o) => o.pay_type === 'cash');
|
|
94
|
+
assert.equal(cash.status, 'needs_setup');
|
|
95
|
+
assert.equal(cash.enforced.cleared, false);
|
|
96
|
+
assert.deepEqual(cash.enforced.missing, ['Processor onboarding done', 'Tax details collected', 'Age check passed', 'Sanctions screening done', 'Signed pay rule on file'],
|
|
97
|
+
'the five things cash needs, from the gate table');
|
|
98
|
+
assert.ok(cash.facts.some((n) => /EU aren't available yet/.test(n.reason)), 'builders in the EU: a fact about cash');
|
|
99
|
+
assert.ok(cash.facts.some((n) => /13 to 15/.test(n.reason)), 'minors on board: a fact about cash');
|
|
100
|
+
assert.ok(cash.facts.every((n) => n.enforced === false), 'a soft limit is never shown as enforced');
|
|
101
|
+
const equity = r.options.find((o) => o.pay_type === 'equity');
|
|
102
|
+
assert.ok(equity.facts.some((n) => n.effect === 'grey' && /nonprofit/i.test(n.reason)));
|
|
103
|
+
assert.equal(equity.advised_against, true);
|
|
104
|
+
// A blocked option is SHOWN, with its reason.
|
|
105
|
+
const eu = r.options.find((o) => o.pay_type === 'eu_labour');
|
|
106
|
+
assert.ok(eu, 'EU labour is listed, not hidden');
|
|
107
|
+
assert.equal(eu.status, 'closed');
|
|
108
|
+
assert.equal(eu.enforced.never, true);
|
|
109
|
+
assert.match(eu.enforced.reason, /refuses/);
|
|
110
|
+
assert.equal(eu.label, 'Paying builders in the EU');
|
|
111
|
+
// A cleared gate reads as allowed.
|
|
112
|
+
const q = project({ gates: payGates(['self_managed']) });
|
|
113
|
+
assert.equal((await pf.readOptions(q.deps())).options.find((o) => o.pay_type === 'self_managed').status, 'open');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('changing a fact re-checks every option and says what it changed', async () => {
|
|
117
|
+
const p = project({ detail: { home_country: 'US', builder_geography: 'us-only' } });
|
|
118
|
+
let r = await pf.readOptions(p.deps());
|
|
119
|
+
assert.equal(r.options.find((o) => o.pay_type === 'cash').facts.length, 0);
|
|
120
|
+
const w = await pf.setFacts({ builder_geography: 'global' }, 1, p.deps());
|
|
121
|
+
const c = w.changed.find((x) => x.pay_type === 'cash');
|
|
122
|
+
assert.ok(c, 'cash is named as changed');
|
|
123
|
+
assert.ok(c.new_notes.some((n) => /EU/.test(n)));
|
|
124
|
+
r = await pf.readOptions(p.deps());
|
|
125
|
+
assert.ok(r.options.find((o) => o.pay_type === 'cash').facts.length > 0, 'the read reflects the new fact at once');
|
|
126
|
+
const back = await pf.setFacts({ builder_geography: 'us-only' }, 1, p.deps());
|
|
127
|
+
assert.ok(back.changed.find((x) => x.pay_type === 'cash').gone_notes.length > 0, 'and says when a note no longer applies');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a bad fact is refused with its reason; nothing is written', async () => {
|
|
131
|
+
const p = project({ detail: { home_country: 'US' } });
|
|
132
|
+
await pf.readFacts(p.deps());
|
|
133
|
+
const before = p.store.get(pf.FACTS_KEY);
|
|
134
|
+
assert.equal((await pf.setFacts({ nope: 1 }, 1, p.deps())).reason, 'unknown_fact');
|
|
135
|
+
assert.equal((await pf.setFacts({ builder_geography: 'mars' }, 1, p.deps())).reason, 'bad_fact');
|
|
136
|
+
assert.equal((await pf.setFacts({ home_region: 'CA-ON' }, 1, p.deps())).reason, 'region_not_in_country');
|
|
137
|
+
assert.equal((await pf.setFacts({ reward_intents: ['gold'] }, 1, p.deps())).reason, 'bad_fact');
|
|
138
|
+
assert.equal(p.store.get(pf.FACTS_KEY), before);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('with no pay-rules module nothing is claimed as enforced', async () => {
|
|
142
|
+
const p = project({ detail: { entity: 'nonprofit' }, gates: null });
|
|
143
|
+
const r = await pf.readOptions(p.deps());
|
|
144
|
+
assert.equal(r.rules_module, false);
|
|
145
|
+
assert.ok(r.options.every((o) => o.enforced === null && o.status === 'no_rules_module'));
|
|
146
|
+
assert.ok(r.options.find((o) => o.pay_type === 'equity').facts.length > 0, 'the facts still speak');
|
|
147
|
+
assert.match(pf.optionsMarkdown(r), /nothing below is enforced by the server/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('the session read explains the rules and tells it never to add one', async () => {
|
|
151
|
+
const p = project({ detail: { home_country: 'US', builder_geography: 'global' } });
|
|
152
|
+
const md = pf.optionsMarkdown(await pf.readOptions(p.deps()));
|
|
153
|
+
assert.match(md, /never add a rule of your own/);
|
|
154
|
+
assert.match(md, /Closed \(enforced\)/);
|
|
155
|
+
assert.match(md, /Fact, not a rule: /);
|
|
156
|
+
assert.match(md, /Where its builders are: global \(from project creation, not yet confirmed\)/);
|
|
157
|
+
assert.match(md, /not answered/);
|
|
158
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -2240,7 +2240,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2240
2240
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
2241
2241
|
assert.match(SCRIPT, /hosting_shape: 'cloud-host',/, 'the shape is unchanged');
|
|
2242
2242
|
assert.match(SCRIPT, /target_ref: state\.owner \+ '\/' \+ state\.repo,/, 'the repo ref is unchanged');
|
|
2243
|
-
assert.match(SCRIPT, /onboard_mode: state\.mode === 'adopt' \? 'adopt' : 'greenfield'/, 'the onboard mode
|
|
2243
|
+
assert.match(SCRIPT, /onboard_mode: state\.mode === 'adopt' \|\| state\.folderUploaded \? 'adopt' : 'greenfield'/, 'the onboard mode: adopt, or a repo made from an uploaded folder (task 1005282)');
|
|
2244
2244
|
// the kit walks every step and files against the stub
|
|
2245
2245
|
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
2246
2246
|
for (const s of ['named', 'ready', 'demo', 'checks-filled', 'row-code', 'row-extras', 'look', 'create-blocked', 'done']) assert.ok(states['in-new-' + s], `the ${s} state has a kit state`);
|
|
@@ -82,6 +82,7 @@ ${NEUTRAL_PREFIX}_DEMO_HOURS=
|
|
|
82
82
|
${NEUTRAL_PREFIX}_SHIP_MODE=
|
|
83
83
|
${NEUTRAL_PREFIX}_TEST_PROJECT=
|
|
84
84
|
${NEUTRAL_PREFIX}_GUIDE_HANDOFF=
|
|
85
|
+
${NEUTRAL_PREFIX}_PROJECT_FACTS=
|
|
85
86
|
${NEUTRAL_PREFIX}_HUB_UPGRADE_URL=
|
|
86
87
|
`;
|
|
87
88
|
|
|
@@ -153,7 +154,7 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
153
154
|
// the birth marker and a demo's box (task 1004506), empty on an un-stamped row
|
|
154
155
|
[`${NEUTRAL_PREFIX}_FOUNDING_BIRTH`]: '', [`${NEUTRAL_PREFIX}_FOUNDING_GENESIS`]: '', [`${NEUTRAL_PREFIX}_DEMO_PEOPLE`]: '', [`${NEUTRAL_PREFIX}_DEMO_HOURS`]: '',
|
|
155
156
|
// the wizard's ship mode (task 1004629), empty when none was picked
|
|
156
|
-
[`${NEUTRAL_PREFIX}_SHIP_MODE`]: '', [`${NEUTRAL_PREFIX}_TEST_PROJECT`]: '', [`${NEUTRAL_PREFIX}_GUIDE_HANDOFF`]: '',
|
|
157
|
+
[`${NEUTRAL_PREFIX}_SHIP_MODE`]: '', [`${NEUTRAL_PREFIX}_TEST_PROJECT`]: '', [`${NEUTRAL_PREFIX}_GUIDE_HANDOFF`]: '', [`${NEUTRAL_PREFIX}_PROJECT_FACTS`]: '',
|
|
157
158
|
// where the hall's Update button goes (task 1004608), empty when no hub origin resolves
|
|
158
159
|
[`${NEUTRAL_PREFIX}_HUB_UPGRADE_URL`]: '',
|
|
159
160
|
});
|
|
@@ -235,6 +235,10 @@ console.log('\nthe routes:');
|
|
|
235
235
|
|
|
236
236
|
let instance = null;
|
|
237
237
|
provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
|
|
238
|
+
// The routes also admit an owner-made grant (task 1004161) and audit each change: nobody here
|
|
239
|
+
// holds a grant, and the audit rows are not this file's subject.
|
|
240
|
+
require('../modules/provisioning/grants.js').liveGrant = async () => null;
|
|
241
|
+
provisioning.recordEvent = async () => {};
|
|
238
242
|
|
|
239
243
|
const api = require('../src/module-api.js');
|
|
240
244
|
let builder = { id: 7, rank: 'xenos', github_login: 'owner' };
|
|
@@ -32,6 +32,8 @@ const express = require('express');
|
|
|
32
32
|
|
|
33
33
|
const api = require('../src/module-api.js');
|
|
34
34
|
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
35
|
+
// The gate also admits an owner-made grant (task 1004161, ADR 0407); nobody in this file holds one.
|
|
36
|
+
require('../modules/provisioning/grants.js').liveGrant = async () => null;
|
|
35
37
|
const envManifest = require('../modules/provisioning/env-manifest.js');
|
|
36
38
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
37
39
|
|