@bongos/core 1.19.472 → 1.19.473

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.472",
6
- "core_contract": "1.19.472",
7
- "source_commit": "6b69181019333048f1fcb17d03f9d04b6b098fb5",
5
+ "core_version": "1.19.473",
6
+ "core_contract": "1.19.473",
7
+ "source_commit": "f858b1895c3573e86927088c29b26f2ad38491a8",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-02T15:04:57.327Z",
9
+ "built_at": "2026-09-02T15:11:50.486Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 411,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1833,
14
+ "functional_verbatim": 1834,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2268,
20
- "tree_sha256": "d824d0535e867ee19410330bc863104779eab940c7a8403f755f98805c928072",
19
+ "file_count": 2269,
20
+ "tree_sha256": "75c680a966e595c6ecad7c239e057f8c023780495df07c5195c859e293942c0b",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2137,7 +2137,7 @@
2137
2137
  {
2138
2138
  "path": "docs/module-api-changelog.md",
2139
2139
  "mode": "0000644",
2140
- "sha256": "901d3e0044c9b6f0437df6424de404e6d367a0e2dceb7226eb3b1c4df7d906b8"
2140
+ "sha256": "f0ec801852ae00f83d8f1a14319f04a442a85541624b3c96e7c9606e80f3a046"
2141
2141
  },
2142
2142
  {
2143
2143
  "path": "docs/modules-contract.md",
@@ -6667,12 +6667,12 @@
6667
6667
  {
6668
6668
  "path": "package-lock.json",
6669
6669
  "mode": "0000644",
6670
- "sha256": "9950b1cf51f93769fdfe48d5001b54bb1274cc4359988e0a2ec98865fe551be0"
6670
+ "sha256": "f7be3efd0df18000603a05ccfb7a2b2f11e6b7aee81298c66cbbd4820d51a191"
6671
6671
  },
6672
6672
  {
6673
6673
  "path": "package.json",
6674
6674
  "mode": "0000644",
6675
- "sha256": "2c9a8851c9bfeaa466c27bc933049734fcde9b77ce276bf2604882a0feff20b7"
6675
+ "sha256": "34165e1371803af75314357a120c881daefc4611a3a3669bd0cb483b9d127893"
6676
6676
  },
6677
6677
  {
6678
6678
  "path": "public-docs/index.html",
@@ -7527,7 +7527,7 @@
7527
7527
  {
7528
7528
  "path": "scripts/gds/run-unit-tests.js",
7529
7529
  "mode": "0000644",
7530
- "sha256": "9a173b5e307f5c1396f1d72b88d0b05534cccbff53af0a39a620001df69bf1a8"
7530
+ "sha256": "56478faf9ee0b9df3c7285096dacb88e37d95744bbb18552d46005fc580dfce0"
7531
7531
  },
7532
7532
  {
7533
7533
  "path": "scripts/gds/sandbox-stage.js",
@@ -8327,7 +8327,7 @@
8327
8327
  {
8328
8328
  "path": "src/module-api.js",
8329
8329
  "mode": "0000644",
8330
- "sha256": "35edfe1acbfd33316d1bbc681817dc06af19336873acd4abe239af41efbc22f2"
8330
+ "sha256": "7b8c9a525a256e4c0428b2b600220ceb8b7c3665bbc1146da9e6bccc832994da"
8331
8331
  },
8332
8332
  {
8333
8333
  "path": "src/module-loader/catalog.js",
@@ -9834,6 +9834,11 @@
9834
9834
  "mode": "0000644",
9835
9835
  "sha256": "fd96eb0dd472810854d9e0a7ee29725764cf0c3f4ad502c57d6d4a324da25ef5"
9836
9836
  },
9837
+ {
9838
+ "path": "tests/idea_credit_streams_db.mjs",
9839
+ "mode": "0000644",
9840
+ "sha256": "3e43b2afa7ea9e8ccee99759c27f7ab28f7a49823cc3b7e921071714d2d0d7f1"
9841
+ },
9837
9842
  {
9838
9843
  "path": "tests/idea_detail_page.mjs",
9839
9844
  "mode": "0000644",
@@ -1381,5 +1381,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1381
1381
  landed since 1.19.470 with no explicit bump. run 33645344292. (task 1002620)
1382
1382
  1.19.472 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1383
1383
  landed since 1.19.471 with no explicit bump. run 33646171957. (task 1002620)
1384
+ 1.19.473 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1385
+ landed since 1.19.472 with no explicit bump. run 33646919877. (task 1002620)
1384
1386
  ---------------------------------------------------------------------------
1385
1387
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.472",
3
+ "version": "1.19.473",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.472",
9
+ "version": "1.19.473",
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.472",
3
+ "version": "1.19.473",
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",
@@ -95,6 +95,17 @@ const INTEGRATION = new Set([
95
95
  // set out of the source, so a quoted word in a comment reads as an entry.)
96
96
  'copy_desk_flags_db',
97
97
  'discord_autojoin', 'discord_oauth',
98
+ // task 1003000 (IC6): the three ideator credit streams against a real Postgres.
99
+ // The three DB-free sibling suites stub the client and SIMULATE the
100
+ // INSERT ... WHERE NOT EXISTS, so the whole no-double-pay argument of the lane
101
+ // is asserted against a fake there; here the statement meets a real transaction
102
+ // engine and the real credit trigger that moves a balance. Every fixture and
103
+ // credit row is written inside a transaction that is always rolled back, so it
104
+ // is safe against a dev database and never TRUNCATEs anything.
105
+ // (Prose here avoids apostrophes and single-quoted tokens: the review gate
106
+ // parses this set out of the source, so a quoted word in a comment reads as an
107
+ // entry — the same reason the copy_desk_flags_db note above is worded that way.)
108
+ 'idea_credit_streams_db',
98
109
  // RENAMED by ADR 0174 (goal 1000068) — these entries said governance_* until task
99
110
  // 1003091. A curated set keyed by a stem that no longer exists silently stops
100
111
  // covering its file: both of these dropped OUT of the integration set and INTO the
package/src/module-api.js CHANGED
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
34
34
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
35
35
  // the entry to that file. Look for a version's history there, not here.
36
36
  // ---------------------------------------------------------------------------
37
- const CORE_VERSION = '1.19.472'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
+ const CORE_VERSION = '1.19.473'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
38
38
 
39
39
  // A namespaced logger so a module's log lines are attributable + consistent.
40
40
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,326 @@
1
+ // tests/idea_credit_streams_db.mjs
2
+ //
3
+ // task 1003000 (BV1.R176 / IC6, goal 1000067) — the ideator credit lane's three
4
+ // streams against a REAL Postgres, so the claims IC6 makes are executable rather
5
+ // than argued. ADR 0172; docs/governance/bongos-v1-ideator-pay-proofs.md is the
6
+ // proof record this harness belongs to.
7
+ //
8
+ // WHY A DB TEST EXISTS AT ALL, when three DB-free suites already cover these
9
+ // functions: those suites stub the client and SIMULATE `INSERT ... WHERE NOT
10
+ // EXISTS` in JavaScript. The whole idempotency argument of the credit lane — the
11
+ // thing that stops an author being paid twice for one idea — is therefore
12
+ // asserted against a fake. Here the statement meets a real transaction engine,
13
+ // real unique indexes, and the real `pms_apply_credit` trigger that moves
14
+ // builders.total_credits. That is the only way to prove the money semantics
15
+ // rather than restate them.
16
+ //
17
+ // NON-DESTRUCTIVE BY CONSTRUCTION: every fixture and every credit row is written
18
+ // inside ONE transaction that is ALWAYS rolled back, so this is safe to point at
19
+ // a dev database that holds real rows. It never TRUNCATEs and never commits.
20
+ //
21
+ // Real-DB (self-skips without Postgres; belongs in the INTEGRATION set so the
22
+ // DB-free lane stays DB-free).
23
+ //
24
+ // Run: DATABASE_URL=postgres://... node tests/idea_credit_streams_db.mjs
25
+ //
26
+ // PROVENANCE, stated plainly: written from the migration DDL and NOT executed by
27
+ // its author — the machine it was written on could not run Postgres at all
28
+ // (no docker, no psql, and an embedded server died on the Windows shared-memory
29
+ // reserve, error 487). The first person to run it with a DATABASE_URL is the
30
+ // first person to see it pass. Treat a failure here as a bug in this file until
31
+ // proven otherwise, and record the outcome in the proof document above.
32
+
33
+ import { strict as assert } from 'node:assert';
34
+ import { createRequire } from 'node:module';
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const { Pool } = require('pg');
38
+ const credits = require('../modules/economy/credits.js');
39
+
40
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
41
+
42
+ let passed = 0;
43
+ let failed = 0;
44
+ async function test(name, fn) {
45
+ try { await fn(); passed++; console.log(` ok ${name}`); }
46
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
47
+ }
48
+
49
+ // The reward policy the live instance runs: builder estimate OFF, ideator lane
50
+ // ON. Every stream below is driven through THIS, so a pass here is also the
51
+ // ADR 0172 §2 exemption holding under the real configuration.
52
+ const COST_PLUS_ONLY = credits.rewardPolicy({ mode: 'cost-plus-only' });
53
+
54
+ // insertRow — insert into `table`, supplying `explicit` and auto-filling every
55
+ // other NOT NULL column that has no default. Schema-driven rather than
56
+ // hand-listed: the fixture tables (versions, tasks, idea_inbox) carry columns
57
+ // this test does not care about, and a hand-written column list would rot the
58
+ // first time a migration adds a NOT NULL one. Identity/serial columns are left
59
+ // to the sequence. Returns the inserted row.
60
+ const schemaCache = new Map();
61
+
62
+ async function columnsOf(client, table) {
63
+ if (schemaCache.has(table)) return schemaCache.get(table);
64
+ const { rows } = await client.query(
65
+ `SELECT column_name, data_type, is_nullable, column_default, is_identity
66
+ FROM information_schema.columns
67
+ WHERE table_schema = 'public' AND table_name = $1`,
68
+ [table]
69
+ );
70
+ schemaCache.set(table, rows);
71
+ return rows;
72
+ }
73
+
74
+ // `table` and the column names are interpolated rather than parameterised
75
+ // because SQL cannot parameterise an identifier. Both are trusted by
76
+ // construction: `table` is always a literal from this file's own fixture(), and
77
+ // the column names come from information_schema metadata for that table — never
78
+ // from input. Nothing here is reachable from a request.
79
+ async function insertRow(client, table, explicit = {}) {
80
+ const cols = await columnsOf(client, table);
81
+ if (cols.length === 0) throw new Error(`table ${table} not found — is this database migrated?`);
82
+ const values = { ...explicit };
83
+ for (const c of cols) {
84
+ if (c.column_name in values) continue;
85
+ if (c.is_nullable === 'YES') continue; // NULL is fine
86
+ if (c.column_default !== null) continue; // the default covers it
87
+ if (c.is_identity === 'YES') continue; // sequence owns it
88
+ values[c.column_name] = placeholderFor(c.data_type, c.column_name);
89
+ }
90
+ const names = Object.keys(values);
91
+ const params = names.map((_, i) => `$${i + 1}`);
92
+ const { rows } = await client.query(
93
+ `INSERT INTO ${table} (${names.map((n) => `"${n}"`).join(', ')})
94
+ VALUES (${params.join(', ')}) RETURNING *`,
95
+ names.map((n) => values[n])
96
+ );
97
+ return rows[0];
98
+ }
99
+
100
+ function placeholderFor(dataType, columnName) {
101
+ switch (dataType) {
102
+ case 'integer': case 'bigint': case 'smallint':
103
+ case 'numeric': case 'real': case 'double precision':
104
+ return 0;
105
+ case 'boolean': return false;
106
+ case 'timestamp with time zone': case 'timestamp without time zone': case 'date':
107
+ return new Date();
108
+ case 'json': case 'jsonb': return {};
109
+ case 'ARRAY': return [];
110
+ default: return `ic6-${columnName}`;
111
+ }
112
+ }
113
+
114
+ // One fully-wired idea: an author, an optional developer, a Full Idea at
115
+ // `completeness`, and `taskCount` shipped tasks descended from it (the IC3
116
+ // lineage stream 1 counts). Ids come from the sequences, never hardcoded.
117
+ async function fixture(client, { completeness = 70, developed = false, taskCount = 0 } = {}) {
118
+ const author = await insertRow(client, 'builders', {
119
+ github_id: `ic6-author-${Date.now()}-${Math.random()}`,
120
+ github_login: 'ic6-author', display_name: 'ic6 author', total_credits: 0,
121
+ });
122
+ const developer = developed
123
+ ? await insertRow(client, 'builders', {
124
+ github_id: `ic6-dev-${Date.now()}-${Math.random()}`,
125
+ github_login: 'ic6-dev', display_name: 'ic6 developer', total_credits: 0,
126
+ })
127
+ : null;
128
+ const idea = await insertRow(client, 'idea_inbox', {
129
+ title: 'ic6 — a Full Idea used to prove the ideator credit lane',
130
+ kind: 'feature', status: 'open', captured_by: author.id,
131
+ });
132
+ await insertRow(client, 'ideas_grades', {
133
+ idea_id: idea.id,
134
+ grade: 'full',
135
+ completeness_score: completeness,
136
+ ...(developer ? { developed_by: developer.id } : {}),
137
+ });
138
+ const version = await insertRow(client, 'versions', { id: `IC6-${Date.now()}-${Math.random()}`.slice(0, 40) });
139
+ const tasks = [];
140
+ for (let i = 0; i < taskCount; i++) {
141
+ const task = await insertRow(client, 'tasks', {
142
+ version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped',
143
+ });
144
+ await insertRow(client, 'ideas_task_lineage', {
145
+ task_id: task.id, idea_id: idea.id, origin: 'promote',
146
+ });
147
+ tasks.push(task);
148
+ }
149
+ return { author, developer, idea, tasks };
150
+ }
151
+
152
+ // Every ideator-lane row for ONE idea, matched on EXACT reason keys.
153
+ //
154
+ // The obvious spelling of this — `reason LIKE 'idea.credit.%' || ideaId || '%'` —
155
+ // is an unanchored substring match on the id, so for idea 5 it also matches
156
+ // `idea.credit.task_ship:15:99` and `idea.credit.full_pass:51`. Against a
157
+ // database that already holds real ideator rows (which this file promises is
158
+ // safe) that silently inflates the counts the assertions below turn on, which
159
+ // is the opposite of the guarantee. So: the two per-idea keys are compared for
160
+ // equality, and the per-task key is matched on the `<id>:` prefix — anchored by
161
+ // the delimiter, so idea 5 can never collide with idea 15 or 51.
162
+ async function ledger(client, ideaId) {
163
+ const id = String(ideaId);
164
+ const { rows } = await client.query(
165
+ `SELECT builder_id, delta, reason FROM credit_log
166
+ WHERE reason = 'idea.credit.full_pass:' || $1
167
+ OR reason = 'idea.credit.governance_pass:' || $1
168
+ OR reason LIKE 'idea.credit.task_ship:' || $1 || ':%'
169
+ ORDER BY id`,
170
+ [id]
171
+ );
172
+ return rows;
173
+ }
174
+
175
+ // Every test runs in its own always-rolled-back transaction.
176
+ async function inTx(fn) {
177
+ const client = await pool.connect();
178
+ try {
179
+ await client.query('BEGIN');
180
+ await fn(client);
181
+ } finally {
182
+ await client.query('ROLLBACK').catch(() => {});
183
+ client.release();
184
+ }
185
+ }
186
+
187
+ try {
188
+ await pool.query('SELECT 1');
189
+ } catch (e) {
190
+ console.log(`idea_credit_streams_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
191
+ process.exit(0);
192
+ }
193
+
194
+ await test('the reward policy under test is the live one: estimate off, ideator lane on', async () => {
195
+ assert.equal(COST_PLUS_ONLY.perTaskCredit, false);
196
+ assert.equal(COST_PLUS_ONLY.ideatorCredit, true);
197
+ });
198
+
199
+ await test('stream 2 — a passing Full Idea books exactly 5, and the REAL trigger moves the balance', async () => {
200
+ await inTx(async (client) => {
201
+ const { author, idea } = await fixture(client, { completeness: 72 });
202
+ const r = await credits.awardIdeaFullPassCredit(client, {
203
+ ideaId: idea.id, authorId: author.id, completenessScore: 72, config: COST_PLUS_ONLY,
204
+ });
205
+ assert.equal(r.credited, 5);
206
+ const rows = await ledger(client, idea.id);
207
+ assert.equal(rows.length, 1);
208
+ assert.equal(rows[0].reason, `idea.credit.full_pass:${idea.id}`);
209
+ // The DB-free suites cannot see this: pms_apply_credit is a real trigger.
210
+ const { rows: [b] } = await client.query('SELECT total_credits FROM builders WHERE id = $1', [author.id]);
211
+ assert.equal(Number(b.total_credits), 5, 'builders.total_credits is moved by the ledger trigger, not by the award fn');
212
+ });
213
+ });
214
+
215
+ await test('stream 2 — REAL SQL idempotency: three calls, one row, one payment', async () => {
216
+ await inTx(async (client) => {
217
+ const { author, idea } = await fixture(client, { completeness: 72 });
218
+ const args = { ideaId: idea.id, authorId: author.id, completenessScore: 72, config: COST_PLUS_ONLY };
219
+ const first = await credits.awardIdeaFullPassCredit(client, args);
220
+ const second = await credits.awardIdeaFullPassCredit(client, args);
221
+ const third = await credits.awardIdeaFullPassCredit(client, args);
222
+ assert.equal(first.credited, 5);
223
+ assert.equal(second.credited, 0);
224
+ assert.equal(second.reason, 'already_paid');
225
+ assert.equal(third.credited, 0);
226
+ assert.equal((await ledger(client, idea.id)).length, 1, 'WHERE NOT EXISTS held against a real engine');
227
+ const { rows: [b] } = await client.query('SELECT total_credits FROM builders WHERE id = $1', [author.id]);
228
+ assert.equal(Number(b.total_credits), 5, 'no double-pay reached the balance either');
229
+ });
230
+ });
231
+
232
+ await test('stream 1 — each shipped task under the idea pays, grade-scaled, once per task', async () => {
233
+ await inTx(async (client) => {
234
+ const { author, idea, tasks } = await fixture(client, { completeness: 80, taskCount: 3 });
235
+ let total = 0;
236
+ for (const t of tasks) {
237
+ const r = await credits.awardIdeaTaskShipCredit(client, { taskId: t.id, config: COST_PLUS_ONLY });
238
+ assert.equal(r.reason, 'paid', `task ${t.id} should pay`);
239
+ total += r.credited;
240
+ }
241
+ // round(5 × 80/100) = 4, three times.
242
+ assert.equal(total, 12);
243
+ // Re-driving every one pays nothing more.
244
+ for (const t of tasks) {
245
+ const again = await credits.awardIdeaTaskShipCredit(client, { taskId: t.id, config: COST_PLUS_ONLY });
246
+ assert.equal(again.credited, 0);
247
+ assert.equal(again.reason, 'already_paid');
248
+ }
249
+ const { rows: [b] } = await client.query('SELECT total_credits FROM builders WHERE id = $1', [author.id]);
250
+ assert.equal(Number(b.total_credits), 12, 'one idea, many tasks — paid per task, never twice');
251
+ });
252
+ });
253
+
254
+ await test('stream 3 — a board-ratified Full Idea books its completeness percent, once per idea', async () => {
255
+ await inTx(async (client) => {
256
+ const { author, idea } = await fixture(client, { completeness: 70 });
257
+ const args = { ideaId: idea.id, authorId: author.id, completenessScore: 70, config: COST_PLUS_ONLY };
258
+ const r = await credits.awardIdeaGovernancePassCredit(client, args);
259
+ assert.equal(r.credited, 70, "the owner's worked example: 70%-complete + passes = 70");
260
+ const again = await credits.awardIdeaGovernancePassCredit(client, args);
261
+ assert.equal(again.credited, 0);
262
+ assert.equal(again.reason, 'already_paid');
263
+ const rows = await ledger(client, idea.id);
264
+ assert.equal(rows.length, 1);
265
+ assert.equal(rows[0].reason, `idea.credit.governance_pass:${idea.id}`);
266
+ });
267
+ });
268
+
269
+ await test('stream 3 — a constitutional amendment books nothing', async () => {
270
+ await inTx(async (client) => {
271
+ const { author, idea } = await fixture(client, { completeness: 90 });
272
+ const r = await credits.awardIdeaGovernancePassCredit(client, {
273
+ ideaId: idea.id, authorId: author.id, completenessScore: 90,
274
+ subjectType: 'constitutional_amendment', config: COST_PLUS_ONLY,
275
+ });
276
+ assert.equal(r.credited, 0);
277
+ assert.equal(r.reason, 'not_a_full_idea');
278
+ assert.equal((await ledger(client, idea.id)).length, 0);
279
+ });
280
+ });
281
+
282
+ await test('the spark split is real rows: 70/30 under ONE reason key, total conserved', async () => {
283
+ await inTx(async (client) => {
284
+ const { author, developer, idea } = await fixture(client, { completeness: 70, developed: true });
285
+ const r = await credits.awardIdeaGovernancePassCredit(client, {
286
+ ideaId: idea.id, authorId: author.id, completenessScore: 70, config: COST_PLUS_ONLY,
287
+ });
288
+ assert.equal(r.credited, 70);
289
+ const rows = await ledger(client, idea.id);
290
+ assert.equal(rows.length, 2, 'two payees, one reason — the builder-scoped predicate is load-bearing here');
291
+ assert.equal(rows[0].reason, rows[1].reason);
292
+ const byBuilder = new Map(rows.map((x) => [String(x.builder_id), Number(x.delta)]));
293
+ assert.equal(byBuilder.get(String(developer.id)), 49, '70% of 70, rounded');
294
+ assert.equal(byBuilder.get(String(author.id)), 21, 'the remainder — never independently rounded');
295
+ });
296
+ });
297
+
298
+ await test('all three streams coexist on one idea without double-paying', async () => {
299
+ await inTx(async (client) => {
300
+ const { author, idea, tasks } = await fixture(client, { completeness: 70, taskCount: 1 });
301
+ await credits.awardIdeaFullPassCredit(client, { ideaId: idea.id, authorId: author.id, completenessScore: 70, config: COST_PLUS_ONLY });
302
+ await credits.awardIdeaTaskShipCredit(client, { taskId: tasks[0].id, config: COST_PLUS_ONLY });
303
+ await credits.awardIdeaGovernancePassCredit(client, { ideaId: idea.id, authorId: author.id, completenessScore: 70, config: COST_PLUS_ONLY });
304
+ const rows = await ledger(client, idea.id);
305
+ const reasons = rows.map((r) => r.reason.split(':')[0]).sort();
306
+ assert.deepEqual([...new Set(reasons)], ['idea.credit.full_pass', 'idea.credit.governance_pass', 'idea.credit.task_ship']);
307
+ // 5 (grade) + round(5 × 0.7) = 4 (one shipped task) + 70 (ratification) = 79
308
+ const { rows: [b] } = await client.query('SELECT total_credits FROM builders WHERE id = $1', [author.id]);
309
+ assert.equal(Number(b.total_credits), 79, 'three distinct reasons, three distinct payments, no overlap');
310
+ });
311
+ });
312
+
313
+ // DELIBERATELY NOT ASSERTED HERE: claim 6, that the retired ADR 0023 slice no
314
+ // longer fires alongside stream 1. The tempting test — call
315
+ // awardIdeaTaskShipCredit, then check no `idea_promotion_bonus` row appeared —
316
+ // is a TAUTOLOGY: that function contains no code path that could write that
317
+ // reason, so the assertion passes without touching the thing claim 6 is about,
318
+ // which is the CALL SITE in modules/lifecycle/db-ship.js. Driving the real
319
+ // awardLandingCredit needs a claim/session fixture well beyond this harness, so
320
+ // claim 6's evidence stays the static read recorded in the proof document
321
+ // (db-ship.js:236 calls stream 1 and nothing else, and no ship path calls the
322
+ // slice). A test that cannot fail is worse than no test: it reads as coverage.
323
+
324
+ console.log(`\nidea_credit_streams_db: ${passed} passed, ${failed} failed`);
325
+ await pool.end();
326
+ if (failed > 0) process.exit(1);