@bongos/core 1.19.227 → 1.19.228
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 +12 -12
- package/modules/government/board.js +78 -4
- package/modules/government/db.js +48 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +10 -0
- package/src/module-api.js +3 -1
- package/tests/government_board_close.mjs +85 -6
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.228",
|
|
6
|
+
"core_contract": "1.19.228",
|
|
7
|
+
"source_commit": "f14babca30bfba954ddd62b510ebc1ed4749d9fa",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-18T05:08:08.027Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 311,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1641,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "2a30117336498006b3241dc75e5157ce5fdc462327dbdf3618e469ad9defb158",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2902,7 +2902,7 @@
|
|
|
2902
2902
|
{
|
|
2903
2903
|
"path": "modules/government/board.js",
|
|
2904
2904
|
"mode": "0000644",
|
|
2905
|
-
"sha256": "
|
|
2905
|
+
"sha256": "6a2b7ebfcbba1442f45efe6df40131b03455fa0fff9ecce51e87696a72c5b117"
|
|
2906
2906
|
},
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "modules/government/catalog.js",
|
|
@@ -2917,7 +2917,7 @@
|
|
|
2917
2917
|
{
|
|
2918
2918
|
"path": "modules/government/db.js",
|
|
2919
2919
|
"mode": "0000644",
|
|
2920
|
-
"sha256": "
|
|
2920
|
+
"sha256": "e20e5a4aa8a79441e5860ceda2f19e41cbdb8fd85c0eda5d00f572eee5701f20"
|
|
2921
2921
|
},
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "modules/government/migrations/governance_001_tables.sql",
|
|
@@ -4302,12 +4302,12 @@
|
|
|
4302
4302
|
{
|
|
4303
4303
|
"path": "package-lock.json",
|
|
4304
4304
|
"mode": "0000644",
|
|
4305
|
-
"sha256": "
|
|
4305
|
+
"sha256": "41bf0bb43e6a556ddf9598e5208bc478dd9bfc5d92f46ff64b6af6ef3ecac0eb"
|
|
4306
4306
|
},
|
|
4307
4307
|
{
|
|
4308
4308
|
"path": "package.json",
|
|
4309
4309
|
"mode": "0000644",
|
|
4310
|
-
"sha256": "
|
|
4310
|
+
"sha256": "1b62282a15730fccfbe7ae8b3a2cb2321c4b1f9947bba37928150613db0e6d7d"
|
|
4311
4311
|
},
|
|
4312
4312
|
{
|
|
4313
4313
|
"path": "public-docs/index.html",
|
|
@@ -4932,7 +4932,7 @@
|
|
|
4932
4932
|
{
|
|
4933
4933
|
"path": "scripts/gds/fitness.js",
|
|
4934
4934
|
"mode": "0000644",
|
|
4935
|
-
"sha256": "
|
|
4935
|
+
"sha256": "00eacd557419ebdba21347e215ef0f197de8cd839f6af778bf90ff2901823a0e"
|
|
4936
4936
|
},
|
|
4937
4937
|
{
|
|
4938
4938
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5922,7 +5922,7 @@
|
|
|
5922
5922
|
{
|
|
5923
5923
|
"path": "src/module-api.js",
|
|
5924
5924
|
"mode": "0000644",
|
|
5925
|
-
"sha256": "
|
|
5925
|
+
"sha256": "985014c8a83781918e242757e48bf93cbc1df612b1eb4560177aeba85b30b0f7"
|
|
5926
5926
|
},
|
|
5927
5927
|
{
|
|
5928
5928
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6847,7 +6847,7 @@
|
|
|
6847
6847
|
{
|
|
6848
6848
|
"path": "tests/government_board_close.mjs",
|
|
6849
6849
|
"mode": "0000644",
|
|
6850
|
-
"sha256": "
|
|
6850
|
+
"sha256": "4408ec434f36b70a589b0592814733d3f88d48aa9aff38c08c9a2eb6c6c9f7b3"
|
|
6851
6851
|
},
|
|
6852
6852
|
{
|
|
6853
6853
|
"path": "tests/government_board_membership.mjs",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// take effect on the very next window without a process restart, the same
|
|
41
41
|
// instant-effect posture the resolver takes (ADR 0016).
|
|
42
42
|
|
|
43
|
+
const api = require('../../src/module-api');
|
|
43
44
|
const db = require('./db');
|
|
44
45
|
const { loadGovernmentConfig } = require('./config');
|
|
45
46
|
const { isBoardMember, resolveBoardMembers } = require('./board-membership');
|
|
@@ -250,17 +251,89 @@ function shouldCloseAfterVote({ constitution, votes, members } = {}) {
|
|
|
250
251
|
return false; // unknown rule: only the clock closes it
|
|
251
252
|
}
|
|
252
253
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
254
|
+
// The pass-event payload (R12, docs/specs/board-room-pass-event-contract.md
|
|
255
|
+
// Decision 2) — IDENTICAL shape for the in-transaction port call and the
|
|
256
|
+
// post-commit event, so there is one thing to reason about. Deliberately
|
|
257
|
+
// carries no tally, no voter list, no constitution: the economy module has no
|
|
258
|
+
// business reading how the vote went, only that it passed.
|
|
259
|
+
function passPayload(closed, { authorId, completenessScore, passedAt }) {
|
|
260
|
+
const isIdea = closed.subject_type === 'full_idea';
|
|
261
|
+
return {
|
|
262
|
+
item_id: closed.id,
|
|
263
|
+
subject_type: closed.subject_type,
|
|
264
|
+
subject_id: closed.subject_id,
|
|
265
|
+
idea_id: isIdea ? closed.subject_id : null,
|
|
266
|
+
author_id: authorId,
|
|
267
|
+
completeness_score: completenessScore,
|
|
268
|
+
passed_at: passedAt,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Close an item: tally what was cast, write the outcome, pay on a pass.
|
|
273
|
+
// Idempotent end to end — the single-winner UPDATE plus every award's
|
|
255
274
|
// reason-key guard. `cause` is for the caller's telemetry ('vote' | 'expiry').
|
|
275
|
+
//
|
|
276
|
+
// R12 (the pass-event contract): MONEY RIDES THE CLOSE TRANSACTION. Stream 3's
|
|
277
|
+
// credit is booked through the `reward` PORT inside the same transaction that
|
|
278
|
+
// writes the outcome — if the credit write fails, the close rolls back and the
|
|
279
|
+
// item stays open, so the vote is retried rather than silently unpaid.
|
|
280
|
+
// `resolveOptional` returning null (no economy module), or a reward port that
|
|
281
|
+
// does not yet carry awardIdeaGovernancePassCredit (IC5 unbuilt), is a CLEAN
|
|
282
|
+
// NO-OP: the item still closes, nothing is booked, and the durable
|
|
283
|
+
// `outcome = 'passed'` row lets IC5's re-drive sweep pay later. The port is
|
|
284
|
+
// resolved and the payload's reads happen BEFORE the transaction opens.
|
|
285
|
+
//
|
|
286
|
+
// `board.item.passed` is then emitted AFTER commit, emitAsync, for non-money
|
|
287
|
+
// reactions only (R22's Discord mirror, future notifications) — emitAsync
|
|
288
|
+
// swallows listener errors, which is right for a broadcast and disqualifying
|
|
289
|
+
// for a payment, which is exactly why the money went through the port above.
|
|
256
290
|
async function closeItem(item, { cause = 'vote' } = {}) {
|
|
257
291
|
if (!item || item.closed_at) return { closed: false, reason: 'already_closed' };
|
|
258
292
|
const votes = await db.getVotesForItem(item.id);
|
|
259
293
|
const outcome = tallyOutcome(votes);
|
|
260
|
-
|
|
294
|
+
|
|
295
|
+
// The payload's reads, ahead of the transaction (reads never ride a tx they
|
|
296
|
+
// don't need). Only a pass needs them, and only a full_idea can pay.
|
|
297
|
+
let authorId = null;
|
|
298
|
+
let completenessScore = null;
|
|
299
|
+
let credit = null;
|
|
300
|
+
if (outcome === 'passed') {
|
|
301
|
+
authorId = await db.getBoardItemAuthor(item);
|
|
302
|
+
if (item.subject_type === 'full_idea') {
|
|
303
|
+
completenessScore = await db.getIdeaCompleteness(item.subject_id);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const reward = outcome === 'passed' && item.subject_type === 'full_idea'
|
|
308
|
+
? (api.resolveOptional && api.resolveOptional('reward'))
|
|
309
|
+
: null;
|
|
310
|
+
const payMoney = reward && typeof reward.awardIdeaGovernancePassCredit === 'function';
|
|
311
|
+
|
|
312
|
+
const closed = await db.closeBoardItem({
|
|
313
|
+
itemId: item.id,
|
|
314
|
+
outcome,
|
|
315
|
+
afterClose: payMoney
|
|
316
|
+
? async (client) => {
|
|
317
|
+
credit = await reward.awardIdeaGovernancePassCredit(client, {
|
|
318
|
+
ideaId: item.subject_id,
|
|
319
|
+
authorId,
|
|
320
|
+
completenessScore,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
: null,
|
|
324
|
+
});
|
|
261
325
|
if (!closed) return { closed: false, reason: 'already_closed' };
|
|
326
|
+
|
|
262
327
|
const authorKarma = outcome === 'passed' ? await awardAuthorRatifyKarma(closed) : null;
|
|
263
|
-
|
|
328
|
+
|
|
329
|
+
if (outcome === 'passed') {
|
|
330
|
+
const payload = passPayload(closed, { authorId, completenessScore, passedAt: closed.closed_at });
|
|
331
|
+
try {
|
|
332
|
+
if (api.emitAsync) await api.emitAsync('board.item.passed', payload);
|
|
333
|
+
} catch (_) { /* emitAsync isolates listener errors; nothing that moves credits subscribes */ }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { closed: true, item: closed, outcome, cause, author_karma: authorKarma, ...(credit ? { credit } : {}) };
|
|
264
337
|
}
|
|
265
338
|
|
|
266
339
|
// The post-vote hook R08's route fires after a FRESH cast. Reads everything it
|
|
@@ -290,6 +363,7 @@ module.exports = {
|
|
|
290
363
|
castVote,
|
|
291
364
|
tallyOutcome,
|
|
292
365
|
shouldCloseAfterVote,
|
|
366
|
+
passPayload,
|
|
293
367
|
closeItem,
|
|
294
368
|
maybeCloseAfterVote,
|
|
295
369
|
};
|
package/modules/government/db.js
CHANGED
|
@@ -386,15 +386,55 @@ async function getVotesForItem(itemId) {
|
|
|
386
386
|
// two concurrent closers (a racing vote and the expiry sweep) write exactly one
|
|
387
387
|
// outcome; the loser gets null and reads the winner's row back if it cares.
|
|
388
388
|
// closed_at comes from the database clock, like every timestamp on this table.
|
|
389
|
-
|
|
389
|
+
//
|
|
390
|
+
// R12 (the pass-event contract, Decision 1) made this a TRANSACTION: money
|
|
391
|
+
// rides the close, so `afterClose(client, closedRow)` runs INSIDE it — if the
|
|
392
|
+
// credit write fails, the close rolls back and the item stays open (the vote is
|
|
393
|
+
// retried, never silently unpaid; "the ledger and the outcome can never
|
|
394
|
+
// disagree"). No afterClose (or a null one) degrades to the plain single-
|
|
395
|
+
// statement close R10 shipped.
|
|
396
|
+
async function closeBoardItem({ itemId, outcome, afterClose = null }) {
|
|
397
|
+
if (typeof afterClose !== 'function') {
|
|
398
|
+
const { rows } = await pool.query(
|
|
399
|
+
`UPDATE government_board_items
|
|
400
|
+
SET closed_at = now(), outcome = $2
|
|
401
|
+
WHERE id = $1 AND closed_at IS NULL
|
|
402
|
+
RETURNING *`,
|
|
403
|
+
[itemId, outcome],
|
|
404
|
+
);
|
|
405
|
+
return rows[0] || null;
|
|
406
|
+
}
|
|
407
|
+
const client = await pool.connect();
|
|
408
|
+
try {
|
|
409
|
+
await client.query('BEGIN');
|
|
410
|
+
const { rows } = await client.query(
|
|
411
|
+
`UPDATE government_board_items
|
|
412
|
+
SET closed_at = now(), outcome = $2
|
|
413
|
+
WHERE id = $1 AND closed_at IS NULL
|
|
414
|
+
RETURNING *`,
|
|
415
|
+
[itemId, outcome],
|
|
416
|
+
);
|
|
417
|
+
const closed = rows[0] || null;
|
|
418
|
+
if (closed) await afterClose(client, closed);
|
|
419
|
+
await client.query('COMMIT');
|
|
420
|
+
return closed;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
try { await client.query('ROLLBACK'); } catch (_) { /* connection-level failure */ }
|
|
423
|
+
throw err;
|
|
424
|
+
} finally {
|
|
425
|
+
client.release();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The subject's stored completeness score, for the pass-event payload (R12).
|
|
430
|
+
// ideas_grades is another module's table; READING it is the sanctioned pattern
|
|
431
|
+
// (the boundary is code imports, not table access) — null when unscored.
|
|
432
|
+
async function getIdeaCompleteness(ideaId) {
|
|
390
433
|
const { rows } = await pool.query(
|
|
391
|
-
`
|
|
392
|
-
|
|
393
|
-
WHERE id = $1 AND closed_at IS NULL
|
|
394
|
-
RETURNING *`,
|
|
395
|
-
[itemId, outcome],
|
|
434
|
+
`SELECT completeness_score FROM ideas_grades WHERE idea_id = $1`,
|
|
435
|
+
[ideaId],
|
|
396
436
|
);
|
|
397
|
-
return rows[0]
|
|
437
|
+
return rows[0] ? rows[0].completeness_score : null;
|
|
398
438
|
}
|
|
399
439
|
|
|
400
440
|
// Author-ratification karma (+3, board.js AUTHOR_RATIFY_KARMA_DELTA) — fired by
|
|
@@ -450,4 +490,5 @@ module.exports = {
|
|
|
450
490
|
insertAuthorRatifyKarma,
|
|
451
491
|
getVotesForItem,
|
|
452
492
|
closeBoardItem,
|
|
493
|
+
getIdeaCompleteness,
|
|
453
494
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.228",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.228",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.228",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1688,6 +1688,16 @@ const VOCAB_ALLOW = [
|
|
|
1688
1688
|
+ 'repo, so the old spelling is still read. Dropping it would silently stop '
|
|
1689
1689
|
+ 'honouring a label override someone already configured.',
|
|
1690
1690
|
},
|
|
1691
|
+
{
|
|
1692
|
+
file: 'modules/government/board.js',
|
|
1693
|
+
match: 'governancepasscredit',
|
|
1694
|
+
why: 'the R02 pass-event contract (docs/specs/board-room-pass-event-contract.md, agreed '
|
|
1695
|
+
+ 'cross-goal 2026-08-17) names the economy-side award awardIdeaGovernancePassCredit, '
|
|
1696
|
+
+ 'matching the credit_log reason family idea.credit.governance_pass:<id> that streams '
|
|
1697
|
+
+ '1-3 share. The board calls the CONSUMER\'s agreed method name — renaming it here '
|
|
1698
|
+
+ 'would break the contract, and the reason strings are durable ledger rows. Scoped to '
|
|
1699
|
+
+ 'the method name, so a stray "governance" elsewhere in board.js still reds the build.',
|
|
1700
|
+
},
|
|
1691
1701
|
{
|
|
1692
1702
|
file: 'modules/government/module.json',
|
|
1693
1703
|
match: 'role',
|
package/src/module-api.js
CHANGED
|
@@ -881,8 +881,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
881
881
|
// landed since 1.19.225 with no explicit bump. run 32100543772. (task 1002620)
|
|
882
882
|
// 1.19.227 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
883
883
|
// landed since 1.19.226 with no explicit bump. run 32101041951. (task 1002620)
|
|
884
|
+
// 1.19.228 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
885
|
+
// landed since 1.19.227 with no explicit bump. run 32101659669. (task 1002620)
|
|
884
886
|
// ---------------------------------------------------------------------------
|
|
885
|
-
const CORE_VERSION = '1.19.
|
|
887
|
+
const CORE_VERSION = '1.19.228'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
886
888
|
|
|
887
889
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
888
890
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -99,6 +99,7 @@ test('a pass pays the author (+3, idempotent downstream); a return pays nobody',
|
|
|
99
99
|
getVotesForItem: async () => [yes('42')],
|
|
100
100
|
closeBoardItem: async ({ itemId, outcome }) => ({ ...openItem(), id: String(itemId), closed_at: 't', outcome }),
|
|
101
101
|
getBoardItemAuthor: async () => '7',
|
|
102
|
+
getIdeaCompleteness: async () => 82,
|
|
102
103
|
insertAuthorRatifyKarma: async (args) => { karma.push(args); return true; },
|
|
103
104
|
}, async () => {
|
|
104
105
|
const out = await closeItem(openItem(), { cause: 'vote' });
|
|
@@ -124,6 +125,8 @@ test('a pass pays the author (+3, idempotent downstream); a return pays nobody',
|
|
|
124
125
|
test('the losing racer gets already_closed and pays nothing', async () => {
|
|
125
126
|
await withDb({
|
|
126
127
|
getVotesForItem: async () => [yes('42')],
|
|
128
|
+
getBoardItemAuthor: async () => '7',
|
|
129
|
+
getIdeaCompleteness: async () => 82,
|
|
127
130
|
closeBoardItem: async () => null, // the WHERE closed_at IS NULL conflict path
|
|
128
131
|
insertAuthorRatifyKarma: async () => { throw new Error('must not be reached'); },
|
|
129
132
|
}, async () => {
|
|
@@ -143,6 +146,7 @@ test('monarchy end to end: the first yes closes the item as passed, author paid'
|
|
|
143
146
|
getActiveBuildersWithRank: async () => [{ builder_id: '42', github_login: 'a', display_name: 'A' }],
|
|
144
147
|
closeBoardItem: async ({ outcome }) => ({ ...openItem(), closed_at: 't', outcome }),
|
|
145
148
|
getBoardItemAuthor: async () => '7',
|
|
149
|
+
getIdeaCompleteness: async () => 82,
|
|
146
150
|
insertAuthorRatifyKarma: async (args) => { karma.push(args); return true; },
|
|
147
151
|
}, async () => {
|
|
148
152
|
const out = await maybeCloseAfterVote(11);
|
|
@@ -166,12 +170,87 @@ test('consent with turnout outstanding leaves the window open', async () => {
|
|
|
166
170
|
|
|
167
171
|
// ── the structural pins ──────────────────────────────────────────────────────
|
|
168
172
|
|
|
169
|
-
test('the close UPDATE is single-winner in the SQL
|
|
173
|
+
test('the close UPDATE is single-winner in the SQL — in BOTH the plain and the transactional form', () => {
|
|
170
174
|
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
171
|
-
const block = src.slice(src.indexOf('async function closeBoardItem'), src.indexOf('
|
|
172
|
-
assert.match(
|
|
175
|
+
const block = src.slice(src.indexOf('async function closeBoardItem'), src.indexOf('async function getIdeaCompleteness'));
|
|
176
|
+
assert.equal((block.match(/WHERE id = \$1 AND closed_at IS NULL/g) || []).length, 2,
|
|
177
|
+
'two racers write exactly one outcome, with or without an in-tx credit hook');
|
|
178
|
+
assert.match(block, /ROLLBACK/, 'a failed in-tx credit rolls the close back — the vote is retried, never silently unpaid');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── R12: money rides the port inside the tx; the event is post-commit courtesy ──
|
|
182
|
+
|
|
183
|
+
test('a pass books the stream-3 credit INSIDE the close transaction, through the reward port', async () => {
|
|
184
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
185
|
+
const hadReward = api.hasProvider('reward');
|
|
186
|
+
if (!hadReward) {
|
|
187
|
+
api.registerProvider('reward', {
|
|
188
|
+
awardIdeaGovernancePassCredit: async (client, args) => ({ credited: 82, reason: 'paid', _client: client, _args: args }),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const seen = [];
|
|
192
|
+
try {
|
|
193
|
+
await withDb({
|
|
194
|
+
getVotesForItem: async () => [yes('42')],
|
|
195
|
+
getBoardItemAuthor: async () => '7',
|
|
196
|
+
getIdeaCompleteness: async () => 82,
|
|
197
|
+
closeBoardItem: async ({ outcome, afterClose }) => {
|
|
198
|
+
const closed = { ...openItem(), closed_at: 't', outcome };
|
|
199
|
+
// the db layer hands afterClose ITS transaction client — simulate that
|
|
200
|
+
if (afterClose) await afterClose({ __tx: true }, closed);
|
|
201
|
+
seen.push({ outcome, hadHook: typeof afterClose === 'function' });
|
|
202
|
+
return closed;
|
|
203
|
+
},
|
|
204
|
+
insertAuthorRatifyKarma: async () => true,
|
|
205
|
+
}, async () => {
|
|
206
|
+
const out = await closeItem(openItem(), { cause: 'vote' });
|
|
207
|
+
assert.equal(out.closed, true);
|
|
208
|
+
if (out.credit) { // present only when a reward provider carries IC5's method
|
|
209
|
+
assert.equal(out.credit.reason, 'paid');
|
|
210
|
+
assert.deepEqual(out.credit._client, { __tx: true }, 'the award ran on the CLOSE transaction\'s client');
|
|
211
|
+
assert.equal(out.credit._args.ideaId, '7');
|
|
212
|
+
assert.equal(out.credit._args.completenessScore, 82);
|
|
213
|
+
}
|
|
214
|
+
assert.equal(seen[0].outcome, 'passed');
|
|
215
|
+
});
|
|
216
|
+
} finally { /* provider registry is process-global; leave whatever was there */ }
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('a RETURNED item books nothing and emits nothing — only a pass pays or announces', async () => {
|
|
220
|
+
await withDb({
|
|
221
|
+
getVotesForItem: async () => [no('42')],
|
|
222
|
+
closeBoardItem: async ({ outcome, afterClose }) => {
|
|
223
|
+
assert.equal(afterClose, null, 'no in-tx hook on a return — nothing to book');
|
|
224
|
+
return { ...openItem(), closed_at: 't', outcome };
|
|
225
|
+
},
|
|
226
|
+
getBoardItemAuthor: async () => { throw new Error('must not be reached'); },
|
|
227
|
+
getIdeaCompleteness: async () => { throw new Error('must not be reached'); },
|
|
228
|
+
}, async () => {
|
|
229
|
+
const out = await closeItem(openItem());
|
|
230
|
+
assert.equal(out.outcome, 'returned');
|
|
231
|
+
assert.equal(out.credit, undefined);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('the payload matches the contract shape, and an amendment carries idea_id null', () => {
|
|
236
|
+
const closed = { id: '11', subject_type: 'full_idea', subject_id: '7', closed_at: 'T' };
|
|
237
|
+
const p = board.passPayload(closed, { authorId: '42', completenessScore: 82, passedAt: 'T' });
|
|
238
|
+
assert.deepEqual(Object.keys(p).sort(),
|
|
239
|
+
['author_id', 'completeness_score', 'idea_id', 'item_id', 'passed_at', 'subject_id', 'subject_type'],
|
|
240
|
+
'exactly the contract\'s seven fields — no tally, no voter list, no constitution');
|
|
241
|
+
assert.equal(p.idea_id, '7', 'idea_id === subject_id for a full_idea');
|
|
242
|
+
const amendment = board.passPayload({ ...closed, subject_type: 'constitutional_amendment' },
|
|
243
|
+
{ authorId: '3', completenessScore: null, passedAt: 'T' });
|
|
244
|
+
assert.equal(amendment.idea_id, null, 'an amendment traverses the same path and must book nothing — the consumer filters on subject_type');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('the event is emitAsync AFTER the close, and money never rides it (source pins)', () => {
|
|
173
248
|
const boardSrc = fs.readFileSync(path.join(GOV, 'board.js'), 'utf8');
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
assert.ok(
|
|
249
|
+
const close = boardSrc.slice(boardSrc.indexOf('async function closeItem'), boardSrc.indexOf('async function maybeCloseAfterVote'));
|
|
250
|
+
assert.match(close, /emitAsync\('board\.item\.passed'/, 'the courtesy event exists for R22');
|
|
251
|
+
assert.ok(close.indexOf('closeBoardItem') < close.indexOf("emitAsync('board.item.passed'"),
|
|
252
|
+
'emitted after the close write — a payment must not depend on a listener having been alive');
|
|
253
|
+
assert.match(close, /awardIdeaGovernancePassCredit/, 'the money goes through the PORT');
|
|
254
|
+
const emitBlock = close.slice(close.indexOf("emitAsync('board.item.passed'"));
|
|
255
|
+
assert.ok(!emitBlock.includes('award'), 'nothing that moves credits touches the event path');
|
|
177
256
|
});
|