@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 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.227",
6
- "core_contract": "1.19.227",
7
- "source_commit": "f6137a0375f70937656a4026edfe79fb32897774",
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-18T04:58:06.044Z",
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": "eede258e0c6a9ae77dd442500e39fdb81956608f764730d9eccbf7caa11a4fce",
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": "43a284c7c69e3181268d1cb1e177deecbafe3c2693881211a74e72442aabf854"
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": "8bd28cbedd0dcb829147ad341c5a0bd3cf30225d6b2665c66218172a1930d1b0"
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": "9f7314822fed28bb1cc244c3a440c0139fa6c383f58019565647078c267738d8"
4305
+ "sha256": "41bf0bb43e6a556ddf9598e5208bc478dd9bfc5d92f46ff64b6af6ef3ecac0eb"
4306
4306
  },
4307
4307
  {
4308
4308
  "path": "package.json",
4309
4309
  "mode": "0000644",
4310
- "sha256": "2b3299463571782b859abbccdb9a2fb7d0130dc6390da6de5484ab6c9800bde3"
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": "a255afac6eb4a2b8269e896032e8eabf10f104aed5a0f1e8d0a7348695625424"
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": "206a9ffcb92d028f8f954dad04df2ec172bfacbd6be04d5204176acd4a94788a"
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": "59b35a20d28c2d689fac976f02eb0e10e95ef887c63702b9e52e3361e71eb6b4"
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
- // Close an item: tally what was cast, write the outcome, pay the author on a
254
- // pass. Idempotent end to end — the single-winner UPDATE plus the karma's
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
- const closed = await db.closeBoardItem({ itemId: item.id, outcome });
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
- return { closed: true, item: closed, outcome, cause, author_karma: authorKarma };
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
  };
@@ -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
- async function closeBoardItem({ itemId, outcome }) {
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
- `UPDATE government_board_items
392
- SET closed_at = now(), outcome = $2
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] || null;
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.227",
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.227",
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.227",
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",
@@ -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.227'; // CI auto-patch carrier (ADR 0161); see changelog above
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, and the pass event is NOT emitted here', () => {
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('// Author-ratification karma'));
172
- assert.match(block, /WHERE id = \$1 AND closed_at IS NULL/, 'two racers write exactly one outcome');
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
- assert.ok(!/emit|event/i.test(boardSrc.slice(boardSrc.indexOf('async function closeItem'), boardSrc.indexOf('async function maybeCloseAfterVote'))) || true,
175
- 'sanity only');
176
- assert.ok(!boardSrc.includes('pass_event'), 'R12 owns the pass event — closing and emitting are separate acts');
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
  });