@bongos/core 1.19.177 → 1.19.178

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.177",
6
- "core_contract": "1.19.177",
7
- "source_commit": "ec902233a41d6187d97c1b0761ca59b57ae3b180",
5
+ "core_version": "1.19.178",
6
+ "core_contract": "1.19.178",
7
+ "source_commit": "039017134d5f7aee3cc1df34e7238847f878d51c",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-16T20:47:16.225Z",
9
+ "built_at": "2026-08-16T20:52:11.709Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 306,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1273,
14
+ "functional_verbatim": 1274,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1600,
20
- "tree_sha256": "07b51c6ba546cfe6dc3f252283ffdbf5fa06230b8a86baeb82b82311d5e11dee",
19
+ "file_count": 1601,
20
+ "tree_sha256": "c2eb9f930075434353ce4f1fccd3189b5a30229ff5083e7fd9b4f7ee57508e45",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2832,7 +2832,7 @@
2832
2832
  {
2833
2833
  "path": "modules/economy/credits.js",
2834
2834
  "mode": "0000644",
2835
- "sha256": "38526b1d7a095fc48d062ddd645df074f381d2ef0e0aac6593ff306601659904"
2835
+ "sha256": "b6f8c6fcabe6cb9f5eb83e936d129509004c86969d5e0d16a9653ff06b88f7ac"
2836
2836
  },
2837
2837
  {
2838
2838
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -2847,7 +2847,7 @@
2847
2847
  {
2848
2848
  "path": "modules/economy/reward.js",
2849
2849
  "mode": "0000644",
2850
- "sha256": "2c0e17de0497d642dd28be0d810dd24bfcfc263fd805612f2f0cc05bc330f1d8"
2850
+ "sha256": "2a354daf06e344a613c8ad7df12ed6e534a9920f49b9f62b54e29b64ead59546"
2851
2851
  },
2852
2852
  {
2853
2853
  "path": "modules/economy/routes/cost.js",
@@ -3477,7 +3477,7 @@
3477
3477
  {
3478
3478
  "path": "modules/ideas/module.json",
3479
3479
  "mode": "0000644",
3480
- "sha256": "7aa38c4dc690735cea2c1ab1ea61b9fa34281ec6f3505ed091990949f741b77d"
3480
+ "sha256": "64116b6abe3b2e13e3e6697b5c53e12534de698376410e2a14e319d4836481be"
3481
3481
  },
3482
3482
  {
3483
3483
  "path": "modules/ideas/routes/blockers.js",
@@ -3487,7 +3487,7 @@
3487
3487
  {
3488
3488
  "path": "modules/ideas/routes/inbox.js",
3489
3489
  "mode": "0000644",
3490
- "sha256": "8604c627617cc90fa3197dcf3776ce708546d754b9acc2d45c16dfea97a478cb"
3490
+ "sha256": "d6307869ae1b4388aa33244a1807f74ca2442122dea597e2f1ea5b7fe4533495"
3491
3491
  },
3492
3492
  {
3493
3493
  "path": "modules/ideas/templates.js",
@@ -4227,12 +4227,12 @@
4227
4227
  {
4228
4228
  "path": "package-lock.json",
4229
4229
  "mode": "0000644",
4230
- "sha256": "bd31fbe4175062649acbd10eba6caca643e13941a4d8a366bd5ba2625e7ac92d"
4230
+ "sha256": "1048f3d5207f880d23989332e8eebcaf55edcbd117046b2e72e6ce65f9fc5f42"
4231
4231
  },
4232
4232
  {
4233
4233
  "path": "package.json",
4234
4234
  "mode": "0000644",
4235
- "sha256": "f5fe7936f9d6739c7268697daab41d5520a0da60bc2b63099d8bd7145e977091"
4235
+ "sha256": "8792a5baa25590aacff1a8ff0ea6690fce9d800d1ac1ce74fc64f998e2038c3c"
4236
4236
  },
4237
4237
  {
4238
4238
  "path": "public-docs/index.html",
@@ -5837,7 +5837,7 @@
5837
5837
  {
5838
5838
  "path": "src/module-api.js",
5839
5839
  "mode": "0000644",
5840
- "sha256": "5904881edeeaeedb873a2968c36a01b3c2faf6e79112bd56653f28692aa5267b"
5840
+ "sha256": "56077ebaf7d23e7e524497ae087473677b8a7ca34e5695ccbf0a54544884be83"
5841
5841
  },
5842
5842
  {
5843
5843
  "path": "src/module-loader/catalog.js",
@@ -6964,6 +6964,11 @@
6964
6964
  "mode": "0000644",
6965
6965
  "sha256": "95b53f1acfa61f723741b4d33b5e882525903ccc549ae01f3094a788251a60ec"
6966
6966
  },
6967
+ {
6968
+ "path": "tests/idea_full_pass_credit.mjs",
6969
+ "mode": "0000644",
6970
+ "sha256": "1ae98ae22202e6a6dbf42a48e0133654eeda32eec36b697c59f6b9d630936817"
6971
+ },
6967
6972
  {
6968
6973
  "path": "tests/idea_grades.mjs",
6969
6974
  "mode": "0000644",
@@ -107,6 +107,17 @@ const KIND_MULTIPLIERS = Object.freeze({
107
107
  });
108
108
  const IDEA_BONUS_FRACTION = 0.25; // 25% slice to the idea's original capturer
109
109
 
110
+ // =========================================================================
111
+ // The IDEATOR credit lane — stream 2 (ADR 0172 / goal 1000067 IC2).
112
+ // =========================================================================
113
+ // A Full Idea that clears the completeness bar pays its author a flat credit,
114
+ // ONCE. This is the ideator lane, NOT the builder per-task estimate — it gates
115
+ // on config.ideatorCredit, never perTaskCredit, so cost-plus-only does not
116
+ // suppress it (that is the whole point of ADR 0172). The other two streams
117
+ // (per-task-ship IC4, governance-pass IC5) will be siblings of this function.
118
+ const IDEA_FULL_PASS_CREDIT = 5; // owner: "gives 5 credits"
119
+ const IDEA_FULL_PASS_BAR = 60; // completeness 0-100; ADR 0172 proposed default
120
+
110
121
  // =========================================================================
111
122
  // Session token reward (flat cost-plus) — task 1005 (ADR 0054).
112
123
  // =========================================================================
@@ -321,6 +332,59 @@ async function maybeAwardIdeaPromotionBonus(client, taskId, shipperBuilderId, mu
321
332
  };
322
333
  }
323
334
 
335
+ // Stream 2 (ADR 0172 / IC2): a Full Idea with a passing completeness score pays
336
+ // its author IDEA_FULL_PASS_CREDIT, ONCE. Transaction-participant like the rest
337
+ // of the reward port — the caller passes its `client` (a pool works too, since
338
+ // the write is a single idempotent statement).
339
+ //
340
+ // client — pg client or pool
341
+ // ideaId, authorId — the idea and its captured_by builder
342
+ // completenessScore — the R142 score (0-100) or null (unscorable)
343
+ // config — resolved reward policy; gates on config.ideatorCredit
344
+ //
345
+ // Returns { credited, reason }:
346
+ // { credited: 5, reason: 'paid' } — first pass, booked
347
+ // { credited: 0, reason: 'already_paid' } — a prior row for this idea exists
348
+ // { credited: 0, reason: 'below_bar' } — null or < IDEA_FULL_PASS_BAR
349
+ // { credited: 0, reason: 'ideator_lane_off' } — instance opted the lane out
350
+ //
351
+ // IDEMPOTENCY is the reason string: `idea.credit.full_pass:<ideaId>` is unique
352
+ // per idea, and the INSERT ... WHERE NOT EXISTS is a single atomic statement, so
353
+ // a re-grade / re-capture / concurrent request can never double-pay — no unique
354
+ // constraint or extra column on the core credit_log table needed (which a module
355
+ // migration may not add anyway). task_id is NULL: an idea credit has no task.
356
+ async function awardIdeaFullPassCredit(client, { ideaId, authorId, completenessScore, config = rewardConfig() } = {}) {
357
+ if (config && config.ideatorCredit === false) {
358
+ return { credited: 0, reason: 'ideator_lane_off' };
359
+ }
360
+ if (completenessScore == null || !Number.isFinite(Number(completenessScore)) || Number(completenessScore) < IDEA_FULL_PASS_BAR) {
361
+ return { credited: 0, reason: 'below_bar' };
362
+ }
363
+ if (authorId == null || ideaId == null) {
364
+ return { credited: 0, reason: 'missing_author_or_idea' };
365
+ }
366
+ const reason = `idea.credit.full_pass:${ideaId}`;
367
+ const description = `Full Idea #${ideaId} passed (completeness ${Math.round(Number(completenessScore))}/100) → ${IDEA_FULL_PASS_CREDIT}c to its author (ideator lane, ADR 0172)`;
368
+ // Idempotency predicate is `builder_id = author AND reason = <per-idea key>`.
369
+ // The `builder_id` term is NOT for correctness — the reason string is already
370
+ // globally unique per idea — it exists to hit the existing
371
+ // `idx_credit_log_builder (builder_id, recorded_at)` index: the NOT EXISTS
372
+ // narrows to the author's handful of credit rows (index scan) instead of a
373
+ // sequential scan of a credit_log that grows with total platform activity.
374
+ // credit_log is a CORE table, so a module migration cannot add an index on
375
+ // `reason` (the additive-namespace rule) — reusing the builder index is the
376
+ // in-scope fix. (R143/IC2 grader finding.)
377
+ const { rowCount } = await client.query(
378
+ `INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
379
+ SELECT $1, NULL, $2, $3, $4
380
+ WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $3)`,
381
+ [authorId, IDEA_FULL_PASS_CREDIT, reason, description]
382
+ );
383
+ return rowCount > 0
384
+ ? { credited: IDEA_FULL_PASS_CREDIT, reason: 'paid' }
385
+ : { credited: 0, reason: 'already_paid' };
386
+ }
387
+
324
388
  // Award the flat cost-plus session token reward, idempotent per session_id
325
389
  // (task 1005, ADR 0054). Called inside the same transaction as the session
326
390
  // upsert (see upsertSessionRecordWithReward).
@@ -531,6 +595,9 @@ module.exports = {
531
595
  applyKindMultiplier,
532
596
  insertMultipliedConfirmCredit,
533
597
  maybeAwardIdeaPromotionBonus,
598
+ awardIdeaFullPassCredit,
599
+ IDEA_FULL_PASS_CREDIT,
600
+ IDEA_FULL_PASS_BAR,
534
601
  awardSessionTokenReward,
535
602
  NET_NEGATIVE_BONUS,
536
603
  SUBTRACTOR_THRESHOLD,
@@ -24,6 +24,8 @@ module.exports = {
24
24
  // credit allocation — transaction-participant: caller passes its tx `client`
25
25
  insertMultipliedConfirmCredit: (...a) => credits.insertMultipliedConfirmCredit(...a),
26
26
  maybeAwardIdeaPromotionBonus: (...a) => credits.maybeAwardIdeaPromotionBonus(...a),
27
+ // ADR 0172 IC2 — the ideator credit lane (stream 2): a passing Full Idea pays its author, once.
28
+ awardIdeaFullPassCredit: (...a) => credits.awardIdeaFullPassCredit(...a),
27
29
  awardNetNegativeBonus: (...a) => credits.awardNetNegativeBonus(...a),
28
30
  awardSessionTokenReward: (...a) => credits.awardSessionTokenReward(...a),
29
31
  // gamification hooks the lifecycle fires at claim/ship
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "key": "ideas",
3
3
  "title": "Ideas",
4
- "description": "The idea inbox (capture, triage, criterion-proposal ratification) and the blockers queue — the methodology surfaces that feed the backlog. A core-domain module carved out of the GDS core in BV1.R78 (ADR 0093 §1); default-on because every instance captures ideas and tracks what's stuck on its owner. Registers the `ideas.capture` kernel PORT the discord #ideas inbound resolves (ADR 0093 §5 — retiring the temporary `captureIdea` doorway leak).",
4
+ "description": "The idea inbox (capture, triage, criterion-proposal ratification) and the blockers queue \u2014 the methodology surfaces that feed the backlog. A core-domain module carved out of the GDS core in BV1.R78 (ADR 0093 \u00a71); default-on because every instance captures ideas and tracks what's stuck on its owner. Registers the `ideas.capture` kernel PORT the discord #ideas inbound resolves (ADR 0093 \u00a75 \u2014 retiring the temporary `captureIdea` doorway leak).",
5
5
  "coreVersion": "^1.8.0",
6
6
  "default": true,
7
- "maintenance": { "status": "core-maintained" },
8
- "contributes": { "routes": ["inbox", "blockers"] },
9
- "provides": ["ideas.capture"],
10
- "consumes": []
7
+ "maintenance": {
8
+ "status": "core-maintained"
9
+ },
10
+ "contributes": {
11
+ "routes": [
12
+ "inbox",
13
+ "blockers"
14
+ ]
15
+ },
16
+ "provides": [
17
+ "ideas.capture"
18
+ ],
19
+ "consumes": [
20
+ "reward"
21
+ ]
11
22
  }
@@ -120,6 +120,32 @@ module.exports = function buildInboxRouter() {
120
120
  idea.template_key = row.template_key;
121
121
  idea.field_values = row.field_values;
122
122
  idea.completeness_score = row.completeness_score;
123
+
124
+ // Stream 2 (ADR 0172 / IC2): a Full Idea that clears the completeness
125
+ // bar pays its author, once, on the IDEATOR credit lane — a separate
126
+ // reward from the builder estimate, so cost-plus-only does NOT
127
+ // suppress it. Resolved through the economy `reward` port (this
128
+ // module declares `reward` in its consumes), never a direct import.
129
+ // FAIL-OPEN, inside the same guard: a credit hiccup must never lose
130
+ // the capture — the idea is already stored. The port method is
131
+ // idempotent (reason = idea.credit.full_pass:<id>), so this cannot
132
+ // double-pay on a re-capture. The pool is the tx client — the write
133
+ // is one atomic idempotent statement.
134
+ try {
135
+ const reward = api.resolveOptional && api.resolveOptional('reward');
136
+ if (reward && reward.awardIdeaFullPassCredit) {
137
+ const r = await reward.awardIdeaFullPassCredit(api.pool, {
138
+ ideaId: idea.id,
139
+ authorId: req.builder.id,
140
+ completenessScore: row.completeness_score,
141
+ });
142
+ if (r && r.credited > 0) idea.ideator_credit = r.credited;
143
+ }
144
+ } catch (creditErr) {
145
+ console.error('[gds] POST /inbox full-idea pass-credit failed (idea captured + graded, credit skipped):', creditErr);
146
+ // no warning surfaced — the idea and its grade are intact; the credit
147
+ // is the only thing missed, and a reconcile/re-grade path can pay it.
148
+ }
123
149
  } catch (gradeErr) {
124
150
  console.error('[gds] POST /inbox full-grade write failed (idea captured, landing as quick):', gradeErr);
125
151
  grade_warning = 'captured_but_ungraded';
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.177",
3
+ "version": "1.19.178",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.177",
9
+ "version": "1.19.178",
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.177",
3
+ "version": "1.19.178",
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
@@ -781,8 +781,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
781
781
  // landed since 1.19.175 with no explicit bump. run 31971125373. (task 1002620)
782
782
  // 1.19.177 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
783
783
  // landed since 1.19.176 with no explicit bump. run 31971532260. (task 1002620)
784
+ // 1.19.178 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
785
+ // landed since 1.19.177 with no explicit bump. run 31971769062. (task 1002620)
784
786
  // ---------------------------------------------------------------------------
785
- const CORE_VERSION = '1.19.177'; // CI auto-patch carrier (ADR 0161); see changelog above
787
+ const CORE_VERSION = '1.19.178'; // CI auto-patch carrier (ADR 0161); see changelog above
786
788
 
787
789
  // A namespaced logger so a module's log lines are attributable + consistent.
788
790
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,113 @@
1
+ // tests/idea_full_pass_credit.mjs
2
+ //
3
+ // task 1002996 (BV1.R172 / IC2, goal 1000067) — stream 2 of the ideator credit
4
+ // lane: a Full Idea with a passing completeness score pays its author 5 credits,
5
+ // once. ADR 0172.
6
+ //
7
+ // The done-when as assertions: a passing Full Idea books exactly 5; a thin /
8
+ // null-scored one books 0; re-running never double-pays; cost-plus-only does NOT
9
+ // suppress it (it gates on ideatorCredit, never perTaskCredit); an instance that
10
+ // opts the ideator lane out pays nothing.
11
+ //
12
+ // The credit fn (awardIdeaFullPassCredit) is exercised over a stub client that
13
+ // records statements and simulates the WHERE-NOT-EXISTS idempotency, so the
14
+ // idempotency logic is proven without a DB. Pure module import.
15
+ //
16
+ // Run: node tests/idea_full_pass_credit.mjs
17
+
18
+ import { strict as assert } from 'node:assert';
19
+ import { createRequire } from 'node:module';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const credits = require('../modules/economy/credits.js');
23
+
24
+ let passed = 0;
25
+ let failed = 0;
26
+ async function test(name, fn) {
27
+ try { await fn(); passed++; console.log(` ok ${name}`); }
28
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
29
+ }
30
+
31
+ // A stub client whose credit_log "table" is an in-memory set of reasons, so the
32
+ // INSERT ... WHERE NOT EXISTS (SELECT 1 ... WHERE reason=$3) behaves exactly like
33
+ // Postgres: the second insert of the same reason is a no-op (rowCount 0).
34
+ function stubClient() {
35
+ const rows = [];
36
+ return {
37
+ rows,
38
+ async query(sql, params) {
39
+ // our fn issues exactly one INSERT ... SELECT ... WHERE NOT EXISTS, whose
40
+ // subquery filters on builder_id AND reason (the index-friendly predicate).
41
+ // Mirror both terms so the stub matches the real query.
42
+ const builderId = params[0];
43
+ const reason = params[2];
44
+ assert.match(sql, /builder_id = \$1 AND reason = \$3/, 'idempotency check must scope by builder_id to hit the index');
45
+ const exists = rows.some((r) => r.builder_id === builderId && r.reason === reason);
46
+ if (exists) return { rowCount: 0, rows: [] };
47
+ rows.push({ builder_id: builderId, delta: params[1], reason, description: params[3] });
48
+ return { rowCount: 1, rows: [] };
49
+ },
50
+ };
51
+ }
52
+
53
+ const PASS = 72; // clears the 60 bar
54
+ const THIN = 40; // below the bar
55
+
56
+ await test('a passing Full Idea books exactly 5 credits to its author', async () => {
57
+ const c = stubClient();
58
+ const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 9, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } });
59
+ assert.equal(r.credited, credits.IDEA_FULL_PASS_CREDIT);
60
+ assert.equal(r.reason, 'paid');
61
+ assert.equal(c.rows.length, 1);
62
+ assert.equal(c.rows[0].delta, 5);
63
+ assert.equal(c.rows[0].builder_id, 3);
64
+ assert.equal(c.rows[0].reason, 'idea.credit.full_pass:9');
65
+ });
66
+
67
+ await test('re-running pays nothing — idempotent per idea', async () => {
68
+ const c = stubClient();
69
+ const opts = { ideaId: 9, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } };
70
+ const first = await credits.awardIdeaFullPassCredit(c, opts);
71
+ const second = await credits.awardIdeaFullPassCredit(c, opts);
72
+ const third = await credits.awardIdeaFullPassCredit(c, opts);
73
+ assert.equal(first.credited, 5);
74
+ assert.equal(second.credited, 0);
75
+ assert.equal(second.reason, 'already_paid');
76
+ assert.equal(third.credited, 0);
77
+ assert.equal(c.rows.length, 1, 'exactly one credit_log row ever, across three calls');
78
+ });
79
+
80
+ await test('a below-bar or null score books 0 — scoring gates the pay, not capture', async () => {
81
+ const c = stubClient();
82
+ assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 1, authorId: 3, completenessScore: THIN, config: { ideatorCredit: true } })).reason, 'below_bar');
83
+ assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 2, authorId: 3, completenessScore: null, config: { ideatorCredit: true } })).reason, 'below_bar');
84
+ assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 3, authorId: 3, completenessScore: undefined, config: { ideatorCredit: true } })).reason, 'below_bar');
85
+ assert.equal(c.rows.length, 0, 'nothing written for any sub-bar score');
86
+ });
87
+
88
+ await test('COST-PLUS-ONLY does NOT suppress it: the ideator lane pays regardless of perTaskCredit', async () => {
89
+ // the live cloudbongos.com policy: builder estimate off, ideator lane on
90
+ const policy = credits.rewardPolicy({ mode: 'cost-plus-only' });
91
+ assert.equal(policy.perTaskCredit, false);
92
+ assert.equal(policy.ideatorCredit, true);
93
+ const c = stubClient();
94
+ const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 5, authorId: 3, completenessScore: PASS, config: policy });
95
+ assert.equal(r.credited, 5, 'paid despite cost-plus-only — this is the whole point of ADR 0172');
96
+ });
97
+
98
+ await test('an instance that opts the ideator lane out pays nothing', async () => {
99
+ const c = stubClient();
100
+ const r = await credits.awardIdeaFullPassCredit(c, { ideaId: 6, authorId: 3, completenessScore: PASS, config: { ideatorCredit: false } });
101
+ assert.equal(r.credited, 0);
102
+ assert.equal(r.reason, 'ideator_lane_off');
103
+ assert.equal(c.rows.length, 0);
104
+ });
105
+
106
+ await test('a missing author or idea is a safe 0, never a throw', async () => {
107
+ const c = stubClient();
108
+ assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: 6, authorId: null, completenessScore: PASS, config: { ideatorCredit: true } })).reason, 'missing_author_or_idea');
109
+ assert.equal((await credits.awardIdeaFullPassCredit(c, { ideaId: null, authorId: 3, completenessScore: PASS, config: { ideatorCredit: true } })).reason, 'missing_author_or_idea');
110
+ });
111
+
112
+ console.log(`\nidea_full_pass_credit: ${passed} passed, ${failed} failed`);
113
+ if (failed > 0) process.exit(1);