@bongos/core 1.19.180 → 1.19.181
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 +18 -13
- package/modules/economy/credits.js +73 -0
- package/modules/economy/reward.js +2 -0
- package/modules/lifecycle/db.js +13 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/idea_task_ship_credit.mjs +117 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.181",
|
|
6
|
+
"core_contract": "1.19.181",
|
|
7
|
+
"source_commit": "3335e6696cc6d8b3c0a4d800860f76287c1fcc03",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T21:
|
|
9
|
+
"built_at": "2026-08-16T21:10:51.840Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 308,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1277,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1606,
|
|
20
|
+
"tree_sha256": "411dfd39dd80ffce17841372dfaf756045f842fb17380c1caca33aa10251bc42",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2842,7 +2842,7 @@
|
|
|
2842
2842
|
{
|
|
2843
2843
|
"path": "modules/economy/credits.js",
|
|
2844
2844
|
"mode": "0000644",
|
|
2845
|
-
"sha256": "
|
|
2845
|
+
"sha256": "71248889315a29d2a1714ad55ffc685987d862b74e877a29ac2de010bea140f3"
|
|
2846
2846
|
},
|
|
2847
2847
|
{
|
|
2848
2848
|
"path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
|
|
@@ -2857,7 +2857,7 @@
|
|
|
2857
2857
|
{
|
|
2858
2858
|
"path": "modules/economy/reward.js",
|
|
2859
2859
|
"mode": "0000644",
|
|
2860
|
-
"sha256": "
|
|
2860
|
+
"sha256": "59952daf3cb8cadc33f5249fb07552949d390ba37c71299117a2d503f83e422a"
|
|
2861
2861
|
},
|
|
2862
2862
|
{
|
|
2863
2863
|
"path": "modules/economy/routes/cost.js",
|
|
@@ -3532,7 +3532,7 @@
|
|
|
3532
3532
|
{
|
|
3533
3533
|
"path": "modules/lifecycle/db.js",
|
|
3534
3534
|
"mode": "0000644",
|
|
3535
|
-
"sha256": "
|
|
3535
|
+
"sha256": "3b7e1979609fa7ba55253e193596d8d0db169925574934083232057dcd1b3e6a"
|
|
3536
3536
|
},
|
|
3537
3537
|
{
|
|
3538
3538
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -4242,12 +4242,12 @@
|
|
|
4242
4242
|
{
|
|
4243
4243
|
"path": "package-lock.json",
|
|
4244
4244
|
"mode": "0000644",
|
|
4245
|
-
"sha256": "
|
|
4245
|
+
"sha256": "eb81d3478ad0a0cf69bc7595c726032d4e815e3a63c4c3c4a62ff56c5a8e5e0f"
|
|
4246
4246
|
},
|
|
4247
4247
|
{
|
|
4248
4248
|
"path": "package.json",
|
|
4249
4249
|
"mode": "0000644",
|
|
4250
|
-
"sha256": "
|
|
4250
|
+
"sha256": "c1255deacb68b9b3ea77218a7d5a9072b8203d1b0c4549d25f62eda4a12626a6"
|
|
4251
4251
|
},
|
|
4252
4252
|
{
|
|
4253
4253
|
"path": "public-docs/index.html",
|
|
@@ -5852,7 +5852,7 @@
|
|
|
5852
5852
|
{
|
|
5853
5853
|
"path": "src/module-api.js",
|
|
5854
5854
|
"mode": "0000644",
|
|
5855
|
-
"sha256": "
|
|
5855
|
+
"sha256": "4d1b546ca70ac1b708cab0aec7a1933bf2e877e3d4ce6f170fdb9c5702df5ce9"
|
|
5856
5856
|
},
|
|
5857
5857
|
{
|
|
5858
5858
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6999,6 +6999,11 @@
|
|
|
6999
6999
|
"mode": "0000644",
|
|
7000
7000
|
"sha256": "cd782459f1e4e6ca6df8a0349521aedcc6a867128240a78d2f00e15f0fd5f259"
|
|
7001
7001
|
},
|
|
7002
|
+
{
|
|
7003
|
+
"path": "tests/idea_task_ship_credit.mjs",
|
|
7004
|
+
"mode": "0000644",
|
|
7005
|
+
"sha256": "e7f0807491c094fddedd518380af73a98c7ac17ac9d42736d9ed9a7cd6d4bf51"
|
|
7006
|
+
},
|
|
7002
7007
|
{
|
|
7003
7008
|
"path": "tests/idea_templates.mjs",
|
|
7004
7009
|
"mode": "0000644",
|
|
@@ -117,6 +117,8 @@ const IDEA_BONUS_FRACTION = 0.25; // 25% slice to the idea's original capturer
|
|
|
117
117
|
// (per-task-ship IC4, governance-pass IC5) will be siblings of this function.
|
|
118
118
|
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
|
+
const IDEA_TASK_SHIP_BASE = 5; // stream 1: base credit per shipped task under an idea
|
|
121
|
+
const QUICK_IDEA_FACTOR = 0.6; // a quick/spark idea's grade factor (ADR 0172 default)
|
|
120
122
|
|
|
121
123
|
// =========================================================================
|
|
122
124
|
// Session token reward (flat cost-plus) — task 1005 (ADR 0054).
|
|
@@ -385,6 +387,74 @@ async function awardIdeaFullPassCredit(client, { ideaId, authorId, completenessS
|
|
|
385
387
|
: { credited: 0, reason: 'already_paid' };
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
// Stream 1 (ADR 0172 / goal 1000067 IC4): as each task descended from an idea
|
|
391
|
+
// SHIPS, pay the idea's author IDEA_TASK_SHIP_BASE credits, SCALED by the idea's
|
|
392
|
+
// grade — a Full Idea by its completeness, a Quick/Spark idea by a flat factor.
|
|
393
|
+
// The ideator lane, so it gates on config.ideatorCredit, never perTaskCredit.
|
|
394
|
+
//
|
|
395
|
+
// THIS REPLACES the ADR 0023 25% idea-promotion slice (maybeAwardIdeaPromotionBonus).
|
|
396
|
+
// The lifecycle ship hook (awardLandingCredit) now calls THIS instead of the
|
|
397
|
+
// slice, so there is one idea-credit-on-ship path, not two (ADR 0172 §4). The
|
|
398
|
+
// old function stays for its unit tests + any compat caller, but is no longer
|
|
399
|
+
// wired at ship.
|
|
400
|
+
//
|
|
401
|
+
// LINEAGE READ, NOT A CODE IMPORT. It reads the ideas-owned lineage + grade
|
|
402
|
+
// tables directly (ideas_task_lineage → idea_inbox → ideas_grades), exactly as
|
|
403
|
+
// maybeAwardIdeaPromotionBonus reads idea_inbox — a cross-table SQL read is
|
|
404
|
+
// allowed across the module boundary; a cross-module require is not.
|
|
405
|
+
//
|
|
406
|
+
// client — pg client / pool (transaction participant)
|
|
407
|
+
// taskId — the task that just shipped
|
|
408
|
+
// config — resolved reward policy; gates on config.ideatorCredit
|
|
409
|
+
//
|
|
410
|
+
// Returns { credited, reason, ideaId, authorId } — credited 0 with a reason for
|
|
411
|
+
// every no-pay path (no source idea, lane off, already paid). Idempotent per
|
|
412
|
+
// (idea, task) via reason='idea.credit.task_ship:<idea>:<task>', builder-scoped
|
|
413
|
+
// so the NOT EXISTS hits idx_credit_log_builder (the IC2 index lesson).
|
|
414
|
+
async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig() } = {}) {
|
|
415
|
+
if (config && config.ideatorCredit === false) {
|
|
416
|
+
return { credited: 0, reason: 'ideator_lane_off' };
|
|
417
|
+
}
|
|
418
|
+
// Which idea does this task descend from, who authored it, and at what grade?
|
|
419
|
+
const { rows } = await client.query(
|
|
420
|
+
`SELECT l.idea_id,
|
|
421
|
+
i.captured_by,
|
|
422
|
+
COALESCE(g.grade, 'quick') AS grade,
|
|
423
|
+
g.completeness_score AS completeness
|
|
424
|
+
FROM ideas_task_lineage l
|
|
425
|
+
JOIN idea_inbox i ON i.id = l.idea_id
|
|
426
|
+
LEFT JOIN ideas_grades g ON g.idea_id = l.idea_id
|
|
427
|
+
WHERE l.task_id = $1`,
|
|
428
|
+
[taskId]
|
|
429
|
+
);
|
|
430
|
+
if (rows.length === 0 || rows[0].captured_by == null) {
|
|
431
|
+
return { credited: 0, reason: 'no_source_idea', ideaId: null, authorId: null };
|
|
432
|
+
}
|
|
433
|
+
const { idea_id: ideaId, captured_by: authorId, grade, completeness } = rows[0];
|
|
434
|
+
// Grade factor: a Full Idea earns in proportion to its completeness; a Quick
|
|
435
|
+
// or Spark idea (no completeness score) earns the flat quick factor. A full
|
|
436
|
+
// idea with no score yet also falls back to the quick factor rather than 0 —
|
|
437
|
+
// the work shipped, the idea is at least quick-grade.
|
|
438
|
+
const factor = (grade === 'full' && completeness != null && Number.isFinite(Number(completeness)))
|
|
439
|
+
? Math.max(0, Math.min(1, Number(completeness) / 100))
|
|
440
|
+
: QUICK_IDEA_FACTOR;
|
|
441
|
+
const delta = Math.round(IDEA_TASK_SHIP_BASE * factor);
|
|
442
|
+
if (delta <= 0) {
|
|
443
|
+
return { credited: 0, reason: 'zero_after_scaling', ideaId: Number(ideaId), authorId: Number(authorId) };
|
|
444
|
+
}
|
|
445
|
+
const reason = `idea.credit.task_ship:${ideaId}:${taskId}`;
|
|
446
|
+
const description = `Task #${taskId} shipped under idea #${ideaId} (${grade}${completeness != null ? ` ${Math.round(Number(completeness))}%` : ''}) → ${delta}c to its author (ideator lane stream 1, ADR 0172)`;
|
|
447
|
+
const { rowCount } = await client.query(
|
|
448
|
+
`INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
|
|
449
|
+
SELECT $1, $2, $3, $4, $5
|
|
450
|
+
WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $4)`,
|
|
451
|
+
[authorId, taskId, delta, reason, description]
|
|
452
|
+
);
|
|
453
|
+
return rowCount > 0
|
|
454
|
+
? { credited: delta, reason: 'paid', ideaId: Number(ideaId), authorId: Number(authorId) }
|
|
455
|
+
: { credited: 0, reason: 'already_paid', ideaId: Number(ideaId), authorId: Number(authorId) };
|
|
456
|
+
}
|
|
457
|
+
|
|
388
458
|
// Award the flat cost-plus session token reward, idempotent per session_id
|
|
389
459
|
// (task 1005, ADR 0054). Called inside the same transaction as the session
|
|
390
460
|
// upsert (see upsertSessionRecordWithReward).
|
|
@@ -596,8 +666,11 @@ module.exports = {
|
|
|
596
666
|
insertMultipliedConfirmCredit,
|
|
597
667
|
maybeAwardIdeaPromotionBonus,
|
|
598
668
|
awardIdeaFullPassCredit,
|
|
669
|
+
awardIdeaTaskShipCredit,
|
|
599
670
|
IDEA_FULL_PASS_CREDIT,
|
|
600
671
|
IDEA_FULL_PASS_BAR,
|
|
672
|
+
IDEA_TASK_SHIP_BASE,
|
|
673
|
+
QUICK_IDEA_FACTOR,
|
|
601
674
|
awardSessionTokenReward,
|
|
602
675
|
NET_NEGATIVE_BONUS,
|
|
603
676
|
SUBTRACTOR_THRESHOLD,
|
|
@@ -26,6 +26,8 @@ module.exports = {
|
|
|
26
26
|
maybeAwardIdeaPromotionBonus: (...a) => credits.maybeAwardIdeaPromotionBonus(...a),
|
|
27
27
|
// ADR 0172 IC2 — the ideator credit lane (stream 2): a passing Full Idea pays its author, once.
|
|
28
28
|
awardIdeaFullPassCredit: (...a) => credits.awardIdeaFullPassCredit(...a),
|
|
29
|
+
// ADR 0172 IC4 — stream 1: pay the idea's author per shipped task, grade-scaled (replaces the 25% slice at ship).
|
|
30
|
+
awardIdeaTaskShipCredit: (...a) => credits.awardIdeaTaskShipCredit(...a),
|
|
29
31
|
awardNetNegativeBonus: (...a) => credits.awardNetNegativeBonus(...a),
|
|
30
32
|
awardSessionTokenReward: (...a) => credits.awardSessionTokenReward(...a),
|
|
31
33
|
// gamification hooks the lifecycle fires at claim/ship
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -3597,9 +3597,19 @@ async function awardLandingCredit(client, { taskId, creditedBuilderId, creditsRe
|
|
|
3597
3597
|
const credit = await reward.insertMultipliedConfirmCredit(
|
|
3598
3598
|
client, creditedBuilderId, taskId, creditsReward, kind, 'task.shipped'
|
|
3599
3599
|
);
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
)
|
|
3600
|
+
// Stream 1 of the ideator credit lane (ADR 0172 / IC4) REPLACES the ADR 0023
|
|
3601
|
+
// 25% idea-promotion slice here — one idea-credit-on-ship path, not two
|
|
3602
|
+
// (ADR 0172 §4). Unlike the slice (25% of the builder estimate, so zero under
|
|
3603
|
+
// cost-plus-only), this pays 5×grade-factor on the IDEATOR lane, so an idea
|
|
3604
|
+
// earns as its work ships even when per-task builder credit is withheld. It
|
|
3605
|
+
// reads the one-idea-many-tasks lineage (ideas_task_lineage, IC3), so a Full
|
|
3606
|
+
// Idea that became a whole goal pays on every one of its shipped tasks — the
|
|
3607
|
+
// old slice only saw the single promoted_to_task_id. `maybeAwardIdeaPromotionBonus`
|
|
3608
|
+
// stays on the port for its unit tests / any compat caller, but is no longer
|
|
3609
|
+
// wired at ship. Guarded so a null return (lane off, no source idea) is fine.
|
|
3610
|
+
const ideaBonus = reward.awardIdeaTaskShipCredit
|
|
3611
|
+
? await reward.awardIdeaTaskShipCredit(client, { taskId })
|
|
3612
|
+
: null;
|
|
3603
3613
|
return {
|
|
3604
3614
|
creditsAwarded: credit.creditsAwarded,
|
|
3605
3615
|
creditsRaw: credit.raw,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.181",
|
|
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.181",
|
|
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.181",
|
|
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
|
@@ -787,8 +787,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
787
787
|
// landed since 1.19.178 with no explicit bump. run 31972209764. (task 1002620)
|
|
788
788
|
// 1.19.180 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
789
789
|
// landed since 1.19.179 with no explicit bump. run 31972568511. (task 1002620)
|
|
790
|
+
// 1.19.181 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
791
|
+
// landed since 1.19.180 with no explicit bump. run 31972686846. (task 1002620)
|
|
790
792
|
// ---------------------------------------------------------------------------
|
|
791
|
-
const CORE_VERSION = '1.19.
|
|
793
|
+
const CORE_VERSION = '1.19.181'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
792
794
|
|
|
793
795
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
794
796
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/idea_task_ship_credit.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002998 (BV1.R174 / IC4, goal 1000067) — stream 1 of the ideator credit
|
|
4
|
+
// lane: as each task descended from an idea SHIPS, pay the idea's author
|
|
5
|
+
// 5 × grade-factor, once. ADR 0172. Replaces the ADR 0023 25% slice at ship.
|
|
6
|
+
//
|
|
7
|
+
// The done-when as assertions: a shipped task under a Full Idea pays
|
|
8
|
+
// 5 × completeness%; under a Quick idea pays 5 × the flat factor; a Full Idea
|
|
9
|
+
// that became a GOAL pays on EVERY one of its shipped tasks (the one-idea-many-
|
|
10
|
+
// tasks point); re-running never double-pays per (idea, task); cost-plus-only
|
|
11
|
+
// does NOT suppress it; a task with no source idea pays nothing.
|
|
12
|
+
//
|
|
13
|
+
// awardIdeaTaskShipCredit is exercised over a stub client whose credit_log is a
|
|
14
|
+
// set of reasons and whose lineage/idea/grade SELECT returns a scripted row —
|
|
15
|
+
// so the grade-factor math and the per-(idea,task) idempotency are proven
|
|
16
|
+
// without a DB.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/idea_task_ship_credit.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const credits = require('../modules/economy/credits.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
async function test(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
30
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `source` is the {idea_id, captured_by, grade, completeness} the lineage SELECT
|
|
34
|
+
// returns for a task (null → no source idea). `credited` is the in-memory ledger.
|
|
35
|
+
function stubClient(source) {
|
|
36
|
+
const credited = [];
|
|
37
|
+
return {
|
|
38
|
+
credited,
|
|
39
|
+
async query(sql, params) {
|
|
40
|
+
if (/FROM ideas_task_lineage/i.test(sql)) {
|
|
41
|
+
return { rows: source ? [source] : [] };
|
|
42
|
+
}
|
|
43
|
+
if (/INSERT INTO credit_log/i.test(sql)) {
|
|
44
|
+
assert.match(sql, /builder_id = \$1 AND reason = \$4/, 'idempotency must be builder-scoped for the index');
|
|
45
|
+
const reason = params[3];
|
|
46
|
+
if (credited.some((r) => r.reason === reason)) return { rowCount: 0, rows: [] };
|
|
47
|
+
credited.push({ builder_id: params[0], task_id: params[1], delta: params[2], reason });
|
|
48
|
+
return { rowCount: 1, rows: [] };
|
|
49
|
+
}
|
|
50
|
+
return { rows: [] };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ON = { ideatorCredit: true };
|
|
56
|
+
|
|
57
|
+
await test('a shipped task under a FULL idea pays 5 × completeness%', async () => {
|
|
58
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
59
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
60
|
+
assert.equal(r.credited, 4, '5 × 0.80 = 4');
|
|
61
|
+
assert.equal(r.reason, 'paid');
|
|
62
|
+
assert.equal(c.credited[0].builder_id, 3);
|
|
63
|
+
assert.equal(c.credited[0].reason, 'idea.credit.task_ship:7:100');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('a shipped task under a QUICK idea pays 5 × the flat factor', async () => {
|
|
67
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'quick', completeness: null });
|
|
68
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
69
|
+
assert.equal(r.credited, Math.round(credits.IDEA_TASK_SHIP_BASE * credits.QUICK_IDEA_FACTOR)); // 5 × 0.6 = 3
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await test('a full idea with 100% pays the full base; a spark falls back to the quick factor', async () => {
|
|
73
|
+
const full = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 100 });
|
|
74
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(full, { taskId: 100, config: ON })).credited, 5);
|
|
75
|
+
const spark = stubClient({ idea_id: 7, captured_by: 3, grade: 'spark', completeness: null });
|
|
76
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(spark, { taskId: 100, config: ON })).credited, 3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await test('one idea → many tasks: EACH shipped task pays (the whole point of stream 1)', async () => {
|
|
80
|
+
// same idea 7, three different tasks — three separate credits
|
|
81
|
+
for (const taskId of [100, 101, 102]) {
|
|
82
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
83
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId, config: ON });
|
|
84
|
+
assert.equal(r.credited, 4, `task ${taskId} pays`);
|
|
85
|
+
assert.equal(c.credited[0].reason, `idea.credit.task_ship:7:${taskId}`, 'distinct per task');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('re-running the same (idea, task) never double-pays', async () => {
|
|
90
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
91
|
+
const first = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
92
|
+
const second = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
93
|
+
assert.equal(first.credited, 4);
|
|
94
|
+
assert.equal(second.credited, 0);
|
|
95
|
+
assert.equal(second.reason, 'already_paid');
|
|
96
|
+
assert.equal(c.credited.length, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await test('COST-PLUS-ONLY does NOT suppress stream 1 — the ideator lane pays', async () => {
|
|
100
|
+
const policy = credits.rewardPolicy({ mode: 'cost-plus-only' });
|
|
101
|
+
assert.equal(policy.perTaskCredit, false);
|
|
102
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
103
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: policy });
|
|
104
|
+
assert.equal(r.credited, 4, 'paid despite cost-plus-only');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('the lane opted out, or a task with no source idea, pays nothing', async () => {
|
|
108
|
+
const off = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
109
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(off, { taskId: 100, config: { ideatorCredit: false } })).reason, 'ideator_lane_off');
|
|
110
|
+
const orphan = stubClient(null);
|
|
111
|
+
const r = await credits.awardIdeaTaskShipCredit(orphan, { taskId: 100, config: ON });
|
|
112
|
+
assert.equal(r.credited, 0);
|
|
113
|
+
assert.equal(r.reason, 'no_source_idea');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`\nidea_task_ship_credit: ${passed} passed, ${failed} failed`);
|
|
117
|
+
if (failed > 0) process.exit(1);
|