@bongos/core 1.19.466 → 1.19.467

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.466",
6
- "core_contract": "1.19.466",
7
- "source_commit": "5b915c14235b08df52728e3d94dc196d548791a3",
5
+ "core_version": "1.19.467",
6
+ "core_contract": "1.19.467",
7
+ "source_commit": "ca27d74624afc9f0a8310e29b2efaa2c5bb993c8",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-02T02:25:23.295Z",
9
+ "built_at": "2026-09-02T13:55:51.607Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 407,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1822,
14
+ "functional_verbatim": 1823,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2253,
20
- "tree_sha256": "4bb527a26dc80efbf88ff9d1a853737b1a657a8f300f8fbf01e76b4146834f58",
19
+ "file_count": 2254,
20
+ "tree_sha256": "8d432d170e40887e5a113bf2531114e2e0b29f4e8dd8aada1b61ae25b764b597",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2122,7 +2122,7 @@
2122
2122
  {
2123
2123
  "path": "docs/module-api-changelog.md",
2124
2124
  "mode": "0000644",
2125
- "sha256": "5e800493043ac5941fa8ed5f9efc66359a0622f79968fd7158a61651556c1cf3"
2125
+ "sha256": "f5622e8a177e977aedade86d47f76e7669f2f417a1d88d5cd63c66949fe9d8c2"
2126
2126
  },
2127
2127
  {
2128
2128
  "path": "docs/modules-contract.md",
@@ -3612,7 +3612,7 @@
3612
3612
  {
3613
3613
  "path": "modules/economy/credits.js",
3614
3614
  "mode": "0000644",
3615
- "sha256": "2269a87709349cd410bad2714189788d24c1edff3e654af5b47aacb93c415cdb"
3615
+ "sha256": "06f1cc7080509864ea4f474627a7a35de8348fcf4f935a13fc62da38e2a67fa8"
3616
3616
  },
3617
3617
  {
3618
3618
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -3632,7 +3632,7 @@
3632
3632
  {
3633
3633
  "path": "modules/economy/reward.js",
3634
3634
  "mode": "0000644",
3635
- "sha256": "59952daf3cb8cadc33f5249fb07552949d390ba37c71299117a2d503f83e422a"
3635
+ "sha256": "13cc932402c3cf0d2154507c6e6a462aeb98520a2214b8561ec01a6bb9245fd6"
3636
3636
  },
3637
3637
  {
3638
3638
  "path": "modules/economy/routes/cost.js",
@@ -6622,12 +6622,12 @@
6622
6622
  {
6623
6623
  "path": "package-lock.json",
6624
6624
  "mode": "0000644",
6625
- "sha256": "098654f260139d3b7b4d45ef87d7a15c3ca8a0ebb04ce36ba538e212c3dce351"
6625
+ "sha256": "fecffff8fe1b80ab540c5212c13d5056d722e897b824afdc2ebeaae24738935b"
6626
6626
  },
6627
6627
  {
6628
6628
  "path": "package.json",
6629
6629
  "mode": "0000644",
6630
- "sha256": "9670afc099badbbc922edd16e4cb6a1a99d370267cc15b2232af678b6c65f7b0"
6630
+ "sha256": "f4a08b3e124eba41cb2e39cea9a79ede0858c5e1496f2ce1531030814df04d12"
6631
6631
  },
6632
6632
  {
6633
6633
  "path": "public-docs/index.html",
@@ -8277,7 +8277,7 @@
8277
8277
  {
8278
8278
  "path": "src/module-api.js",
8279
8279
  "mode": "0000644",
8280
- "sha256": "400ed351948806a44d6558bbc10d35158237d80c3101415acebb6d075747c3ae"
8280
+ "sha256": "30e8f24ef0a0f45e8c4172c98daee61756ba74acf0d55f100ae8cc6c7b7540e2"
8281
8281
  },
8282
8282
  {
8283
8283
  "path": "src/module-loader/catalog.js",
@@ -9794,6 +9794,11 @@
9794
9794
  "mode": "0000644",
9795
9795
  "sha256": "63afe2afd59ff64e290234a0cc8d33a0f77b784d8c10c2d7147fe2ab3c114cd1"
9796
9796
  },
9797
+ {
9798
+ "path": "tests/idea_governance_pass_credit.mjs",
9799
+ "mode": "0000644",
9800
+ "sha256": "75af0cfce88da50c74a1c499e7308774b70eb05e31a3274246dd6b4c8fdb1b34"
9801
+ },
9797
9802
  {
9798
9803
  "path": "tests/idea_grades.mjs",
9799
9804
  "mode": "0000644",
@@ -1369,5 +1369,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1369
1369
  landed since 1.19.464 with no explicit bump. run 33581328823. (task 1002620)
1370
1370
  1.19.466 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1371
1371
  landed since 1.19.465 with no explicit bump. run 33583122775. (task 1002620)
1372
+ 1.19.467 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1373
+ landed since 1.19.466 with no explicit bump. run 33638703282. (task 1002620)
1372
1374
  ---------------------------------------------------------------------------
1373
1375
  ```
@@ -119,6 +119,11 @@ const IDEA_FULL_PASS_CREDIT = 5; // owner: "gives 5 credits"
119
119
  const IDEA_FULL_PASS_BAR = 60; // completeness 0-100; ADR 0172 proposed default
120
120
  const IDEA_TASK_SHIP_BASE = 5; // stream 1: base credit per shipped task under an idea
121
121
  const QUICK_IDEA_FACTOR = 0.6; // a quick/spark idea's grade factor (ADR 0172 default)
122
+ // Stream 3 (IC5): a board-ratified Full Idea pays a share of a 100-credit
123
+ // ceiling, in proportion to how complete the idea was. Owner: "30-100 credits
124
+ // ... paid as a percent on the 100 credits for its fullness."
125
+ const IDEA_GOVERNANCE_PASS_MAX = 100; // owner: "up to 100 credits"
126
+ const IDEA_GOVERNANCE_PASS_FLOOR = 30; // owner: floor ~30 — see the note on the award fn
122
127
 
123
128
  // =========================================================================
124
129
  // Session token reward (flat cost-plus) — task 1005 (ADR 0054).
@@ -577,6 +582,114 @@ async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig()
577
582
  : { credited: 0, reason: 'already_paid', ideaId: Number(ideaId), authorId: Number(authorId) };
578
583
  }
579
584
 
585
+ // Stream 3 (ADR 0172 / goal 1000067 IC5): a Full Idea that PASSES the Board
586
+ // Room's ratification vote pays a share of a 100-credit ceiling, scaled by the
587
+ // idea's R142 completeness — "70%-complete + passes = 70". The ideator lane, so
588
+ // it gates on config.ideatorCredit, never perTaskCredit.
589
+ //
590
+ // THE BOARD CALLS THIS THROUGH THE `reward` PORT, INSIDE ITS CLOSE TRANSACTION
591
+ // (docs/specs/board-room-pass-event-contract.md, Decision 1) — not off the
592
+ // `board.item.passed` seam event, because emitAsync swallows listener errors,
593
+ // which is right for a Discord post and disqualifying for a payment. If this
594
+ // write fails the close rolls back and the item stays open, so the vote is
595
+ // retried rather than the author silently unpaid.
596
+ //
597
+ // RE-DRIVABLE BY DESIGN (contract Decision 5): the durable fact is the
598
+ // `board_items` row with outcome='passed', not a delivered event. Nothing here
599
+ // reads board state or requires a live listener, and the reason key makes a
600
+ // blanket re-run safe, so a missed award can be reconciled by sweeping the
601
+ // passed items and calling this again. No such sweep is scheduled in v1 — the
602
+ // requirement is only that the shape permits one.
603
+ //
604
+ // client — pg client / pool (transaction participant)
605
+ // ideaId, authorId — the ratified idea and its captured_by builder
606
+ // completenessScore — the R142 score (0-100)
607
+ // subjectType — the board subject, when the caller knows it; only
608
+ // 'full_idea' pays (contract Decision 2). Omitted means
609
+ // "already filtered", which is how the board calls it.
610
+ // config — resolved reward policy; gates on config.ideatorCredit
611
+ //
612
+ // Returns { credited, reason }:
613
+ // { credited: N, reason: 'paid' } — first pass for this idea
614
+ // { credited: 0, reason: 'already_paid' } — a prior row for this idea
615
+ // { credited: 0, reason: 'not_a_full_idea' } — an amendment pays nothing
616
+ // { credited: 0, reason: 'ideator_lane_off' } — instance opted the lane out
617
+ // { credited: 0, reason: 'missing_author_or_idea' }
618
+ // { credited: 0, reason: 'no_completeness_score' }
619
+ //
620
+ // NO `below_bar` RETURN, deliberately (contract Decision 4): ADR 0175 §5 makes
621
+ // the completeness bar the gate on OPENING a vote window at all, so an idea
622
+ // below the bar can never reach a pass. Re-checking it here would imply the
623
+ // board might send something it cannot.
624
+ async function awardIdeaGovernancePassCredit(client, {
625
+ ideaId, authorId, completenessScore, subjectType, config = rewardConfig(),
626
+ } = {}) {
627
+ // SUBJECT TYPE FIRST, ahead of the lane check that opens both sibling streams:
628
+ // a constitutional amendment traverses the same close path and is not an
629
+ // ideator payment at all, so whether the ideator lane is on is irrelevant to
630
+ // it. A re-drive sweep reading these reasons gets the truthful one.
631
+ if (subjectType != null && subjectType !== 'full_idea') {
632
+ return { credited: 0, reason: 'not_a_full_idea' };
633
+ }
634
+ if (config && config.ideatorCredit === false) {
635
+ return { credited: 0, reason: 'ideator_lane_off' };
636
+ }
637
+ if (ideaId == null) {
638
+ return { credited: 0, reason: 'missing_author_or_idea' };
639
+ }
640
+ if (completenessScore == null || !Number.isFinite(Number(completenessScore))) {
641
+ return { credited: 0, reason: 'no_completeness_score' };
642
+ }
643
+ // The parties come from the DB, not the call site (the R147 rule): there is
644
+ // exactly one place that decides who earns from an idea. `authorId` is a
645
+ // fallback for a caller holding an idea this lookup cannot see.
646
+ const parties = await ideaCreditParties(client, ideaId);
647
+ const resolvedAuthor = (parties && parties.authorId != null) ? parties.authorId : authorId;
648
+ if (resolvedAuthor == null) {
649
+ return { credited: 0, reason: 'missing_author_or_idea' };
650
+ }
651
+ // CLAMPED to [0,1] before scaling, so a malformed score can never pay out
652
+ // more than the ceiling (the stream-1 precedent). The floor then applies:
653
+ // it is UNREACHABLE under default config — ADR 0175 §5 gates window-opening
654
+ // on IDEA_FULL_PASS_BAR (60), so a real payout lands in [60,100] and max()
655
+ // never binds — but it ships because it is the owner's stated intent and goes
656
+ // live the moment either constant moves.
657
+ const pct = Math.max(0, Math.min(1, Number(completenessScore) / 100));
658
+ const delta = Math.max(
659
+ IDEA_GOVERNANCE_PASS_FLOOR,
660
+ Math.round(pct * IDEA_GOVERNANCE_PASS_MAX)
661
+ );
662
+ // Split with whoever DEVELOPED the spark, the same 70/30 as streams 1 and 2
663
+ // (ADR 0185) — the contract predates that split and names the author alone;
664
+ // mirroring the sibling streams, as it asks, means splitting here too.
665
+ const payees = splitIdeaCredit(delta, {
666
+ authorId: resolvedAuthor,
667
+ developedBy: parties ? parties.developedBy : null,
668
+ });
669
+ if (payees.length === 0) {
670
+ return { credited: 0, reason: 'zero_after_scaling' };
671
+ }
672
+ // ONCE PER IDEA, FOREVER. A resubmission that passes again, a re-close, a
673
+ // replay, or a concurrent request all resolve to this same key and book
674
+ // nothing further. Builder-scoped so the NOT EXISTS hits
675
+ // idx_credit_log_builder (credit_log is a CORE table — a module migration may
676
+ // not add an index on `reason`), and because a split books one row per payee
677
+ // under ONE reason, a reason-only predicate would refuse the second payee.
678
+ // task_id is NULL: an idea credit has no task.
679
+ const reason = `idea.credit.governance_pass:${ideaId}`;
680
+ const scorePct = Math.round(Number(completenessScore));
681
+ const { credited, payees: booked } = await bookIdeaSplit(client, {
682
+ payees,
683
+ reason,
684
+ describe: (p) => `Full Idea #${ideaId} ratified by the board (completeness ${scorePct}/100) → ${p.delta}c to its ${p.role.replace(/_/g, ' ')} (ideator lane stream 3, ADR 0172${p.role === 'developer' || p.role === 'spark_author' ? '; split ADR 0185' : ''})`,
685
+ });
686
+ return {
687
+ credited,
688
+ reason: credited > 0 ? 'paid' : 'already_paid',
689
+ payees: booked,
690
+ };
691
+ }
692
+
580
693
  // Award the flat cost-plus session token reward, idempotent per session_id
581
694
  // (task 1005, ADR 0054). Called inside the same transaction as the session
582
695
  // upsert (see upsertSessionRecordWithReward).
@@ -824,6 +937,7 @@ module.exports = {
824
937
  maybeAwardIdeaPromotionBonus,
825
938
  awardIdeaFullPassCredit,
826
939
  awardIdeaTaskShipCredit,
940
+ awardIdeaGovernancePassCredit,
827
941
  // the spark-handoff split (task 1002923 / BV1.R147, ADR 0185)
828
942
  splitIdeaCredit,
829
943
  IDEA_DEVELOPER_SHARE,
@@ -831,6 +945,8 @@ module.exports = {
831
945
  IDEA_FULL_PASS_BAR,
832
946
  IDEA_TASK_SHIP_BASE,
833
947
  QUICK_IDEA_FACTOR,
948
+ IDEA_GOVERNANCE_PASS_MAX,
949
+ IDEA_GOVERNANCE_PASS_FLOOR,
834
950
  awardSessionTokenReward,
835
951
  NET_NEGATIVE_BONUS,
836
952
  SUBTRACTOR_THRESHOLD,
@@ -28,6 +28,11 @@ module.exports = {
28
28
  awardIdeaFullPassCredit: (...a) => credits.awardIdeaFullPassCredit(...a),
29
29
  // ADR 0172 IC4 — stream 1: pay the idea's author per shipped task, grade-scaled (replaces the 25% slice at ship).
30
30
  awardIdeaTaskShipCredit: (...a) => credits.awardIdeaTaskShipCredit(...a),
31
+ // ADR 0172 IC5 — stream 3: a board-ratified Full Idea pays up to 100, scaled by completeness.
32
+ // The board's close transaction resolves this port and calls this with its own
33
+ // `client` (docs/specs/board-room-pass-event-contract.md) — the port is how
34
+ // money crosses the wall; the `board.item.passed` event never carries a payment.
35
+ awardIdeaGovernancePassCredit: (...a) => credits.awardIdeaGovernancePassCredit(...a),
31
36
  awardNetNegativeBonus: (...a) => credits.awardNetNegativeBonus(...a),
32
37
  awardSessionTokenReward: (...a) => credits.awardSessionTokenReward(...a),
33
38
  // gamification hooks the lifecycle fires at claim/ship
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.466",
3
+ "version": "1.19.467",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.466",
9
+ "version": "1.19.467",
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.466",
3
+ "version": "1.19.467",
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/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.466'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.467'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,243 @@
1
+ // tests/idea_governance_pass_credit.mjs
2
+ //
3
+ // task 1002999 (BV1.R175 / IC5, goal 1000067) — stream 3 of the ideator credit
4
+ // lane: a Full Idea that PASSES the Board Room's ratification vote pays a share
5
+ // of a 100-credit ceiling, scaled by its R142 completeness. ADR 0172 §3, and
6
+ // docs/specs/board-room-pass-event-contract.md (the agreement with the board).
7
+ //
8
+ // The done-when as assertions: a ratified Full Idea books round(completeness%)
9
+ // once; a constitutional amendment books nothing; re-running never double-pays;
10
+ // cost-plus-only does NOT suppress it (it gates on ideatorCredit, never
11
+ // perTaskCredit); an instance that opts the ideator lane out pays nothing.
12
+ //
13
+ // The credit fn is exercised over a stub client that records statements and
14
+ // simulates the WHERE-NOT-EXISTS idempotency, so the idempotency logic is proven
15
+ // without a DB — the tests/idea_full_pass_credit.mjs harness, unchanged.
16
+ //
17
+ // Run: node tests/idea_governance_pass_credit.mjs
18
+
19
+ import { strict as assert } from 'node:assert';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const credits = require('../modules/economy/credits.js');
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+ async function test(name, fn) {
28
+ try { await fn(); passed++; console.log(` ok ${name}`); }
29
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
30
+ }
31
+
32
+ // A stub client whose credit_log "table" is an in-memory set of (builder, reason)
33
+ // pairs, so INSERT ... WHERE NOT EXISTS behaves exactly like Postgres: a second
34
+ // insert for the same pair is a no-op (rowCount 0). `developedBy` defaults to
35
+ // null — an idea nobody else developed — and is set explicitly by the split test.
36
+ function stubClient({ authorId = null, developedBy = null } = {}) {
37
+ const rows = [];
38
+ return {
39
+ rows,
40
+ async query(sql, params) {
41
+ // The parties lookup — who authored this idea, and who developed it.
42
+ if (/FROM idea_inbox i/i.test(sql) && /developed_by/i.test(sql)) {
43
+ return { rows: authorId == null ? [] : [{ captured_by: authorId, developed_by: developedBy }] };
44
+ }
45
+ // The idempotency predicate must scope by builder_id AND reason: a split
46
+ // books two rows under ONE reason, so a reason-only predicate would
47
+ // silently refuse the second payee (and credit_log is a CORE table, so a
48
+ // module migration cannot add an index on `reason` anyway).
49
+ const builderId = params[0];
50
+ const reason = params[3];
51
+ assert.match(sql, /builder_id = \$1 AND reason = \$4/, 'idempotency check must scope by builder_id AND reason');
52
+ const exists = rows.some((r) => r.builder_id === builderId && r.reason === reason);
53
+ if (exists) return { rowCount: 0, rows: [] };
54
+ rows.push({ builder_id: builderId, task_id: params[1], delta: params[2], reason, description: params[4] });
55
+ return { rowCount: 1, rows: [] };
56
+ },
57
+ };
58
+ }
59
+
60
+ const ON = { ideatorCredit: true };
61
+
62
+ await test('a ratified Full Idea pays its completeness percent of the 100 ceiling', async () => {
63
+ const c = stubClient({ authorId: 3 });
64
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 9, authorId: 3, completenessScore: 70, config: ON });
65
+ assert.equal(r.credited, 70, 'the owner\'s worked example: 70%-complete + passes = 70');
66
+ assert.equal(r.reason, 'paid');
67
+ assert.equal(c.rows.length, 1);
68
+ assert.equal(c.rows[0].delta, 70);
69
+ assert.equal(c.rows[0].builder_id, 3);
70
+ assert.equal(c.rows[0].reason, 'idea.credit.governance_pass:9');
71
+ assert.equal(c.rows[0].task_id, null, 'an idea credit has no task');
72
+ });
73
+
74
+ await test('a fully-complete idea pays the whole 100-credit ceiling, never more', async () => {
75
+ const c = stubClient({ authorId: 3 });
76
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 1, authorId: 3, completenessScore: 100, config: ON });
77
+ assert.equal(r.credited, credits.IDEA_GOVERNANCE_PASS_MAX);
78
+ assert.equal(r.credited, 100);
79
+ });
80
+
81
+ await test('a malformed over-100 score is clamped to the ceiling, not paid out', async () => {
82
+ const c = stubClient({ authorId: 3 });
83
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 2, authorId: 3, completenessScore: 420, config: ON });
84
+ assert.equal(r.credited, 100, 'the ceiling holds even when the input is nonsense');
85
+ });
86
+
87
+ await test('re-running pays nothing — once per idea, forever', async () => {
88
+ const c = stubClient({ authorId: 3 });
89
+ const opts = { ideaId: 9, authorId: 3, completenessScore: 80, config: ON };
90
+ const first = await credits.awardIdeaGovernancePassCredit(c, opts);
91
+ const second = await credits.awardIdeaGovernancePassCredit(c, opts);
92
+ const third = await credits.awardIdeaGovernancePassCredit(c, opts);
93
+ assert.equal(first.credited, 80);
94
+ assert.equal(second.credited, 0);
95
+ assert.equal(second.reason, 'already_paid');
96
+ assert.equal(third.credited, 0);
97
+ assert.equal(c.rows.length, 1, 'exactly one credit_log row ever, across three calls');
98
+ });
99
+
100
+ await test('a constitutional amendment books nothing — subject_type is load-bearing', async () => {
101
+ const c = stubClient({ authorId: 3 });
102
+ const r = await credits.awardIdeaGovernancePassCredit(c, {
103
+ ideaId: null, authorId: 3, completenessScore: null,
104
+ subjectType: 'constitutional_amendment', config: ON,
105
+ });
106
+ assert.equal(r.credited, 0);
107
+ assert.equal(r.reason, 'not_a_full_idea');
108
+ assert.equal(c.rows.length, 0, 'an amendment traverses the same close path and must pay nobody');
109
+ });
110
+
111
+ await test('subject_type is checked BEFORE the lane flag, so a sweep gets the truthful reason', async () => {
112
+ const c = stubClient({ authorId: 3 });
113
+ const r = await credits.awardIdeaGovernancePassCredit(c, {
114
+ ideaId: 5, authorId: 3, completenessScore: 90,
115
+ subjectType: 'constitutional_amendment', config: { ideatorCredit: false },
116
+ });
117
+ assert.equal(r.reason, 'not_a_full_idea', 'an amendment is not an ideator payment at all');
118
+ });
119
+
120
+ await test('an explicit full_idea subject pays exactly as an omitted one does', async () => {
121
+ const c = stubClient({ authorId: 3 });
122
+ const r = await credits.awardIdeaGovernancePassCredit(c, {
123
+ ideaId: 7, authorId: 3, completenessScore: 65, subjectType: 'full_idea', config: ON,
124
+ });
125
+ assert.equal(r.credited, 65, 'the board omits subjectType because it has already filtered; a sweep passes it');
126
+ });
127
+
128
+ await test('COST-PLUS-ONLY does NOT suppress it: the ideator lane pays regardless of perTaskCredit', async () => {
129
+ // the live cloudbongos.com policy: builder estimate off, ideator lane on
130
+ const policy = credits.rewardPolicy({ mode: 'cost-plus-only' });
131
+ assert.equal(policy.perTaskCredit, false);
132
+ assert.equal(policy.ideatorCredit, true);
133
+ const c = stubClient({ authorId: 3 });
134
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 5, authorId: 3, completenessScore: 75, config: policy });
135
+ assert.equal(r.credited, 75, 'paid despite cost-plus-only — this is the whole point of ADR 0172');
136
+ });
137
+
138
+ await test('an instance that opts the ideator lane out pays nothing', async () => {
139
+ const c = stubClient({ authorId: 3 });
140
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 6, authorId: 3, completenessScore: 90, config: { ideatorCredit: false } });
141
+ assert.equal(r.credited, 0);
142
+ assert.equal(r.reason, 'ideator_lane_off');
143
+ assert.equal(c.rows.length, 0);
144
+ });
145
+
146
+ await test('an unscorable idea books 0 — and it is NOT reported as below a bar', async () => {
147
+ // There is deliberately no `below_bar` return: ADR 0175 §5 makes the
148
+ // completeness bar the gate on OPENING a window, so a sub-bar idea can never
149
+ // reach a pass. A null score means the score is MISSING, not low.
150
+ const c = stubClient({ authorId: 3 });
151
+ assert.equal((await credits.awardIdeaGovernancePassCredit(c, { ideaId: 1, authorId: 3, completenessScore: null, config: ON })).reason, 'no_completeness_score');
152
+ assert.equal((await credits.awardIdeaGovernancePassCredit(c, { ideaId: 2, authorId: 3, completenessScore: undefined, config: ON })).reason, 'no_completeness_score');
153
+ assert.equal((await credits.awardIdeaGovernancePassCredit(c, { ideaId: 3, authorId: 3, completenessScore: 'x', config: ON })).reason, 'no_completeness_score');
154
+ assert.equal(c.rows.length, 0, 'nothing written when there is no score to scale by');
155
+ });
156
+
157
+ await test('a missing author or idea is a safe 0, never a throw', async () => {
158
+ const noIdea = stubClient({ authorId: 3 });
159
+ assert.equal((await credits.awardIdeaGovernancePassCredit(noIdea, { ideaId: null, authorId: 3, completenessScore: 90, config: ON })).reason, 'missing_author_or_idea');
160
+ // An idea the lookup cannot see AND no caller-supplied author → still a safe 0.
161
+ const unknown = stubClient(); // lookup returns no row
162
+ assert.equal((await credits.awardIdeaGovernancePassCredit(unknown, { ideaId: 6, authorId: null, completenessScore: 90, config: ON })).reason, 'missing_author_or_idea');
163
+ assert.equal(unknown.rows.length, 0);
164
+ });
165
+
166
+ await test('the DB is authoritative for the author; the argument is only a fallback', async () => {
167
+ const known = stubClient({ authorId: 3 });
168
+ const r = await credits.awardIdeaGovernancePassCredit(known, { ideaId: 6, authorId: null, completenessScore: 90, config: ON });
169
+ assert.equal(r.credited, 90);
170
+ assert.equal(known.rows[0].builder_id, 3, 'paid the author the DB names');
171
+
172
+ // ...and when the lookup finds nothing, the passed author is still honoured,
173
+ // so a caller holding an idea this module cannot see is not silently unpaid.
174
+ const invisible = stubClient();
175
+ const f = await credits.awardIdeaGovernancePassCredit(invisible, { ideaId: 6, authorId: 7, completenessScore: 90, config: ON });
176
+ assert.equal(f.credited, 90);
177
+ assert.equal(invisible.rows[0].builder_id, 7);
178
+ });
179
+
180
+ await test('a developed spark splits 70/30 with its developer, conserving the total', async () => {
181
+ // ADR 0185, the same rule streams 1 and 2 apply. The contract for this stream
182
+ // predates the split and names the author alone; mirroring the sibling
183
+ // streams, as it asks, means splitting here too.
184
+ const c = stubClient({ authorId: 3, developedBy: 8 });
185
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 4, authorId: 3, completenessScore: 70, config: ON });
186
+ assert.equal(r.credited, 70, 'the total is conserved across the two payees');
187
+ assert.equal(c.rows.length, 2);
188
+ const dev = c.rows.find((x) => x.builder_id === 8);
189
+ const author = c.rows.find((x) => x.builder_id === 3);
190
+ assert.equal(dev.delta, 49, '70% of 70, rounded');
191
+ assert.equal(author.delta, 21, 'the remainder — never independently rounded');
192
+ assert.equal(dev.reason, author.reason, 'one reason key, two rows — hence the builder-scoped predicate');
193
+ });
194
+
195
+ await test('a self-developed spark collapses to one payee, so nobody is under-paid', async () => {
196
+ const c = stubClient({ authorId: 3, developedBy: 3 });
197
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 11, authorId: 3, completenessScore: 70, config: ON });
198
+ assert.equal(r.credited, 70);
199
+ assert.equal(c.rows.length, 1, 'two rows for one builder would hit the idempotency predicate and UNDER-pay');
200
+ });
201
+
202
+ await test('the floor pays 30 on a sub-30 score — a branch unreachable under default config', async () => {
203
+ // NOT A REAL SCENARIO, and deliberately not faked into one: ADR 0175 §5 gates
204
+ // window-opening on IDEA_FULL_PASS_BAR, so a real payout lands in [60,100]
205
+ // and max() never binds. The branch is exercised by handing the function an
206
+ // input the board cannot currently send — never by lowering the bar in a
207
+ // fixture, which would look like a scenario it is not. It ships because it is
208
+ // the owner's stated intent and goes live the moment either constant moves.
209
+ const c = stubClient({ authorId: 3 });
210
+ const r = await credits.awardIdeaGovernancePassCredit(c, { ideaId: 12, authorId: 3, completenessScore: 10, config: ON });
211
+ assert.equal(r.credited, credits.IDEA_GOVERNANCE_PASS_FLOOR);
212
+ assert.equal(r.credited, 30);
213
+ });
214
+
215
+ await test('the floor really is unreachable at the shipped constants — the finding, as an assertion', async () => {
216
+ const lowestPassablePayout = Math.round(credits.IDEA_FULL_PASS_BAR / 100 * credits.IDEA_GOVERNANCE_PASS_MAX);
217
+ assert.ok(
218
+ lowestPassablePayout > credits.IDEA_GOVERNANCE_PASS_FLOOR,
219
+ `bar ${credits.IDEA_FULL_PASS_BAR} pays ${lowestPassablePayout}, above the ${credits.IDEA_GOVERNANCE_PASS_FLOOR} floor — if this ever fails, the floor has become live and the branch above is a real scenario`
220
+ );
221
+ });
222
+
223
+ await test('all three ideator streams use distinct reason keys, so one idea can earn on each', async () => {
224
+ const c = stubClient({ authorId: 3 });
225
+ await credits.awardIdeaFullPassCredit(c, { ideaId: 20, authorId: 3, completenessScore: 90, config: ON });
226
+ await credits.awardIdeaGovernancePassCredit(c, { ideaId: 20, authorId: 3, completenessScore: 90, config: ON });
227
+ assert.equal(c.rows.length, 2, 'stream 2 and stream 3 both paid the same idea');
228
+ assert.equal(c.rows[0].reason, 'idea.credit.full_pass:20');
229
+ assert.equal(c.rows[1].reason, 'idea.credit.governance_pass:20');
230
+ assert.equal(c.rows[0].delta + c.rows[1].delta, 95, '5 for the grade, 90 for the ratification');
231
+ });
232
+
233
+ await test('the award rides the reward PORT, so the board can resolve it', async () => {
234
+ const reward = require('../modules/economy/reward.js');
235
+ assert.equal(typeof reward.awardIdeaGovernancePassCredit, 'function',
236
+ 'board.js duck-types on exactly this name; without it the close is a silent no-op');
237
+ const c = stubClient({ authorId: 3 });
238
+ const r = await reward.awardIdeaGovernancePassCredit(c, { ideaId: 30, authorId: 3, completenessScore: 60, config: ON });
239
+ assert.equal(r.credited, 60, 'delegated, not spread — the port reaches the live function');
240
+ });
241
+
242
+ console.log(`\nidea_governance_pass_credit: ${passed} passed, ${failed} failed`);
243
+ if (failed > 0) process.exit(1);