@bongos/core 1.19.865 → 1.19.866

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.
@@ -2204,5 +2204,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2204
2204
  landed since 1.19.863 with no explicit bump. run 35294233113. (task 1002620)
2205
2205
  1.19.865 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2206
2206
  landed since 1.19.864 with no explicit bump. run 35295856053. (task 1002620)
2207
+ 1.19.866 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2208
+ landed since 1.19.865 with no explicit bump. run 35355019469. (task 1002620)
2207
2209
  ---------------------------------------------------------------------------
2208
2210
  ```
@@ -790,7 +790,7 @@
790
790
  const title = escapeHtml(t.value_summary || t.title || `task #${t.id}`);
791
791
  return `
792
792
  <tr data-acclaim-row="${tid}">
793
- <td>${title}</td>
793
+ <td>${title}${acclaimEvidenceHtml(t)}</td>
794
794
  <td>${by}</td>
795
795
  <td class="acclaim__net ${netCls}" data-acclaim-net="${tid}">${netText}</td>
796
796
  <td>${controls}</td>
@@ -1424,7 +1424,7 @@
1424
1424
  // reads what this page holds NOW rather than what it held at boot. Everything
1425
1425
  // else is stable for the life of the page. This call sits AFTER every
1426
1426
  // declaration it passes — moving it earlier is a temporal-dead-zone crash.
1427
- const { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
1427
+ const { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
1428
1428
  API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
1429
1429
  get instanceRepo() { return instanceRepo; },
1430
1430
  get lastClaimableCount() { return lastClaimableCount; },
@@ -332,6 +332,46 @@
332
332
  </div>`;
333
333
  }
334
334
 
335
+ // task 1002835: a karma vote on a peer's ship was a vote on a title alone.
336
+ // The two facts the ship already recorded — what the grader scored it and
337
+ // what it cost — turn it into an informed one. ABSENCE IS NOT ZERO: an
338
+ // ungraded ship says so, and so does an unlogged cost, because "0.0" and
339
+ // "$0.00" would read as measurements that were never taken.
340
+ function acclaimEvidenceHtml(t) {
341
+ const score = t.grade_score == null ? null : Number(t.grade_score);
342
+ const grade = score == null || !Number.isFinite(score)
343
+ ? '<span class="ledger__verdict--none">not graded</span>'
344
+ : `<span class="${t.grade_passed ? 'ledger__verdict--ok' : 'ledger__verdict--danger'}">${score.toFixed(1)} / 10</span>`;
345
+ // Three states, not two. The route OMITS cost_usd for a builder who may not
346
+ // vote, and that is not the same fact as a logged cost of nothing: a reader
347
+ // who is shown no figure is told nothing, never "no cost logged".
348
+ const parts = [grade];
349
+ if (Object.prototype.hasOwnProperty.call(t, 'cost_usd')) {
350
+ const usd = t.cost_usd == null ? null : Number(t.cost_usd);
351
+ parts.push(usd == null || !Number.isFinite(usd)
352
+ ? '<span class="ledger__verdict--none">no cost logged</span>'
353
+ : `<span class="acclaim__cost">$${usd.toFixed(2)}</span>`);
354
+ }
355
+ return `<span class="acclaim__evidence">${parts.join(' · ')}</span>`;
356
+ }
357
+ // The RETURNING builder's door (task 1002851). `open` and `invite_only`
358
+ // already render a real .landing__btn sign-in inside the primary door, so a
359
+ // second button there would be a third scattered path rather than a clearer
360
+ // one — those modes keep the prose note alone. Only the `apply` door, whose
361
+ // primary control is the request FORM, left signing in as a footnote link.
362
+ // It gets a real button: the request button's geometry so it reads as a
363
+ // control, a quieter fill so the request path still leads for the strangers
364
+ // who are most of this page's traffic.
365
+ function returningDoorHtml(mode) {
366
+ if (mode !== 'apply') return '';
367
+ return `
368
+ <div class="landing__returning" id="landing-returning">
369
+ <h3 class="landing__request-h">Already a member?</h3>
370
+ <p class="landing__request-lede">You've been approved before — sign in and pick up where you left off.</p>
371
+ <p class="landing__cta"><a class="landing__btn landing__btn--quiet" href="${API}/auth/web/start">Sign in with GitHub</a></p>
372
+ </div>`;
373
+ }
374
+
335
375
  function renderLanding() {
336
376
  // task 1723: collapse the shell nav to the signed-out minimum (Home + a
337
377
  // Sign-in button). Cosmetic — the server gates every page regardless.
@@ -343,14 +383,20 @@
343
383
  const landing = $('#landing-root');
344
384
  if (landing) {
345
385
  landing.hidden = false;
386
+ const mode = joinabilityMode();
387
+ // On the apply door the sign-in is a real button above, so the note keeps
388
+ // only the privacy sentence — one sign-in path per landing, never two.
389
+ const signInNote = mode === 'apply'
390
+ ? ''
391
+ : `Already a member? <a href="${API}/auth/web/start">Sign in with GitHub</a>. `;
346
392
  landing.innerHTML = `
347
393
  <div class="scroll landing">
348
394
  <h2 class="scroll__h">Welcome</h2>
349
- ${landingDoorHtml(joinabilityMode())}
395
+ ${landingDoorHtml(mode)}
396
+ ${returningDoorHtml(mode)}
350
397
 
351
398
  <p class="landing__signin-note">
352
- Already a member? <a href="${API}/auth/web/start">Sign in with GitHub</a>.
353
- We store only your GitHub login, avatar, and the work you ship.
399
+ ${signInNote}We store only your GitHub login, avatar, and the work you ship.
354
400
  </p>
355
401
  </div>
356
402
  `;
@@ -1305,6 +1351,6 @@
1305
1351
  return idx === -1 ? null : { place: idx + 1, total: ranked.length };
1306
1352
  }
1307
1353
 
1308
- return { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
1354
+ return { SETUP_CONFIG_KEY, acclaimEvidenceHtml, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderGreeting, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
1309
1355
  };
1310
1356
  })();
@@ -24,5 +24,14 @@
24
24
  .dir-who__t { display: flex; flex-direction: column; gap: 1px; min-width: 132px; }
25
25
  .dir-name { font-weight: 500; color: var(--ink); overflow-wrap: anywhere; }
26
26
  .dir-login { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-faint); }
27
+ /* The roster's place cell carries two facts: the place, and under it the
28
+ builder's number in the same mono/faint voice the login uses (task
29
+ 1002837). Block display so the id never wraps up beside the place. */
30
+ .ledger__place .ledger__id {
31
+ display: block;
32
+ font-family: var(--font-mono);
33
+ font-size: 11.5px;
34
+ color: var(--ink-faint);
35
+ }
27
36
  .dir-crafts { display: inline-flex; flex-wrap: wrap; gap: 6px; }
28
37
  .dir-none { color: var(--ink-faint); }
@@ -148,9 +148,19 @@
148
148
  return fmtInt(Math.round((Number(b.tokens_used) || 0) / credits)) + ' tokens per ' + one;
149
149
  }
150
150
 
151
+ // task 1002837: a builder's place answers "who is ahead", their NUMBER
152
+ // answers "which builder is this" — the id every task row, claim and ship
153
+ // note already cites. It sits under the place in the same mono/faint voice
154
+ // the login uses, and only when the payload carries one, so an older
155
+ // /builders response degrades to the place alone.
156
+ function idHtml(b) {
157
+ if (b.id == null || b.id === '') return '';
158
+ return '<span class="ledger__id">#' + esc(String(b.id)) + '</span>';
159
+ }
160
+
151
161
  function rowHtml(b) {
152
162
  return '<tr>'
153
- + '<td class="ledger__num">' + esc(String(b.__place || '—')) + '</td>'
163
+ + '<td class="ledger__num ledger__place">' + esc(String(b.__place || '—')) + idHtml(b) + '</td>'
154
164
  + '<td>' + whoHtml(b) + achRowHtml(b.achievements) + '</td>'
155
165
  + '<td class="ledger__rank">' + rankBadgeHtml(b.rank) + '</td>'
156
166
  + '<td class="ledger__num">' + esc(fmtInt(b.total_credits)) + '</td>'
@@ -1536,6 +1536,16 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
1536
1536
  .ledger__verdict--ok { color: var(--ok); font-weight: 600; }
1537
1537
  .ledger__verdict--danger { color: var(--danger); font-weight: 600; }
1538
1538
  .ledger__verdict--none { color: var(--ink-soft); }
1539
+ /* The acclaim feed's evidence line (task 1002835): the grade and the cost the
1540
+ ship recorded, under the work it describes. Secondary to the title — this is
1541
+ what a voter checks, not what they read first. */
1542
+ .acclaim__evidence {
1543
+ display: block;
1544
+ margin-top: 3px;
1545
+ font-size: 12px;
1546
+ color: var(--ink-faint);
1547
+ }
1548
+ .acclaim__cost { font-variant-numeric: tabular-nums; }
1539
1549
  .ledger th.ledger__sortable { cursor: pointer; user-select: none; }
1540
1550
  .ledger th.ledger__sortable:hover { color: var(--ink); }
1541
1551
  .ledger th.ledger__sortable[aria-sort="ascending"]::after { content: " ↑"; }
@@ -2492,6 +2502,24 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
2492
2502
  .landing__btn--request { align-self: flex-start; }
2493
2503
  .landing__request-status { font-size: 13px; margin: 0; color: var(--ok); }
2494
2504
  .landing__request-status[data-kind="err"] { color: var(--danger); }
2505
+ /* The returning builder's door, rendered only on the `apply` landing (task
2506
+ 1002851). Same panel as the request block and the same button geometry, so
2507
+ signing in reads as a real control rather than a footnote — but a quiet
2508
+ fill, because the request path is what most of this page's traffic needs. */
2509
+ .landing__returning {
2510
+ border: 0;
2511
+ border-radius: var(--radius);
2512
+ background: var(--surface-2);
2513
+ padding: 18px 20px;
2514
+ margin: 16px 0;
2515
+ }
2516
+ .landing__returning .landing__cta { margin: 0; }
2517
+ .landing__btn--quiet {
2518
+ background: transparent;
2519
+ color: var(--ink);
2520
+ box-shadow: inset 0 0 0 1px var(--border-strong);
2521
+ }
2522
+ .landing__btn--quiet:hover { background: var(--surface-2); color: var(--ink); }
2495
2523
  .landing__signin-note { font-size: 13px; color: var(--ink-soft); }
2496
2524
 
2497
2525
  /* ---------- goals ---------- */
@@ -29,6 +29,10 @@ const api = require('../../../src/module-api');
29
29
  // depRefusalRow: the ONE shape a dependency refusal carries, shared with the claim
30
30
  // path so promote and claim describe a blocked task identically (task 1003553).
31
31
  const { depRefusalRow } = require('../dead-deps.js');
32
+ // gradeQualityWhere: the ONE definition of a task_grades row that is an actual
33
+ // quality verdict rather than an outage or an errored worker (task 1002652).
34
+ // The acclaim feed reads it so a grader outage never reaches a voter as 0.0.
35
+ const { gradeQualityWhere } = require('../db-shared');
32
36
  const auth = api;
33
37
  const db = require('../db');
34
38
  const hierarchy = require('../hierarchy');
@@ -771,9 +775,28 @@ module.exports = function buildTasksRouter() {
771
775
  t.shipped_at, t.shipped_by,
772
776
  b.github_login, b.display_name,
773
777
  COALESCE(pv.net, 0)::int AS net_votes,
774
- mine.vote AS my_vote
778
+ mine.vote AS my_vote,
779
+ -- task 1002835: a karma vote is uninformed without the two facts
780
+ -- the ship already recorded. BOTH stay NULL when absent — an
781
+ -- ungraded ship and a zero-scoring one are different claims, and
782
+ -- so are an unlogged cost and a free one, so neither is
783
+ -- COALESCEd to a number the client would print as a measurement.
784
+ --
785
+ -- BOTH JOINS ARE INDEXED LOOKUPS, not scans: task_grades.task_id
786
+ -- is declared UNIQUE (migrations/016_pms_task_grades.sql:14, which
787
+ -- gives it its own index) and cost_log.task_id carries
788
+ -- idx_cost_log_task (migrations/003_pms.sql:152). Neither needs a
789
+ -- migration here, and neither degrades as cost_log grows.
790
+ CASE WHEN ${gradeQualityWhere('g')} THEN g.score END AS grade_score,
791
+ CASE WHEN ${gradeQualityWhere('g')} THEN g.passed END AS grade_passed,
792
+ cost.total_usd AS cost_usd
775
793
  FROM tasks t
776
794
  LEFT JOIN builders b ON b.id = t.shipped_by
795
+ LEFT JOIN task_grades g ON g.task_id = t.id
796
+ LEFT JOIN LATERAL (
797
+ SELECT SUM(amount_usd) AS total_usd
798
+ FROM cost_log WHERE task_id = t.id
799
+ ) cost ON true
777
800
  LEFT JOIN LATERAL (
778
801
  SELECT SUM(vote) AS net
779
802
  FROM peer_votes WHERE target_task_id = t.id
@@ -788,7 +811,15 @@ module.exports = function buildTasksRouter() {
788
811
  LIMIT $2`,
789
812
  [req.builder.id, limit]
790
813
  );
791
- res.json({ tasks: rows, can_vote: ['metic', 'archon'].includes(req.builder.rank) });
814
+ const canVote = ['metic', 'archon'].includes(req.builder.rank);
815
+ // The cost is INTERNAL SPEND, and it is on this feed for one reason: to
816
+ // inform a vote. So it reaches only the ranks that can cast one. The grade
817
+ // is a quality verdict rather than spend and stays readable by any builder,
818
+ // as it already is on the grades table. Note the field is OMITTED, not
819
+ // nulled: a null here means "nothing was logged", and a reader who is not
820
+ // shown a figure must not be told a figure is missing.
821
+ const tasks = canVote ? rows : rows.map(({ cost_usd, ...rest }) => rest);
822
+ res.json({ tasks, can_vote: canVote });
792
823
  } catch (err) {
793
824
  // Log the detail server-side; don't return err.message — a raw Postgres
794
825
  // error can leak schema internals (table/column/constraint names).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.865",
3
+ "version": "1.19.866",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.865",
9
+ "version": "1.19.866",
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.865",
3
+ "version": "1.19.866",
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",
@@ -71,6 +71,11 @@ const SUPPORT_MODULES = new Set(['helpers']);
71
71
 
72
72
  // Need a live DB / server / network — run in the integration-smoke gate instead.
73
73
  const INTEGRATION = new Set([
74
+ // task 1004004: the acclaim feed proves a claim about SQL semantics — that a
75
+ // grader outage nulls rather than scoring 0.0, and that a summed cost over no
76
+ // rows is NULL rather than a free ship. Only a transaction engine settles
77
+ // those, so this runs here and the DB-free sibling covers the renderer.
78
+ 'acclaim_feed_evidence_db',
74
79
  'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
75
80
  'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
76
81
  // consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.865'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.866'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,186 @@
1
+ // tests/acclaim_feed_evidence_db.mjs — the peer-acclaim feed's grade + cost
2
+ // evidence, against a REAL Postgres (task 1004004, from task 1002835).
3
+ //
4
+ // WHY THIS EXISTS AND ITS DB-FREE SIBLING IS NOT ENOUGH.
5
+ // tests/hall_builders_page_pass.mjs runs the RENDERER over every shape the row
6
+ // can take, and asserts the route's SQL by reading its source. That proves the
7
+ // client is honest about a NULL — it cannot prove the query ever PRODUCES one.
8
+ // The whole argument of part C is a claim about SQL semantics that only a
9
+ // transaction engine can settle:
10
+ //
11
+ // - a grader OUTAGE row carries score 0 / passed=false in task_grades, and
12
+ // gradeQualityWhere must turn that into NULL rather than let a voter read
13
+ // it as a 0.0 verdict;
14
+ // - SUM(amount_usd) over ZERO cost_log rows is NULL, not 0, which is the
15
+ // difference between "no cost logged" and "this ship was free";
16
+ // - a ship with several cost rows must sum them, not pick one;
17
+ // - the two LATERALs and the grades join must not multiply the row out.
18
+ //
19
+ // Runs the route's own query text — sliced out of the shipped source, never
20
+ // retyped — so a future edit to the route is tested rather than a copy of it.
21
+ // Self-skips without a reachable Postgres; in the INTEGRATION set, so the
22
+ // DB-free unit gate never runs it and .github/workflows/integration.yml does.
23
+ //
24
+ // Run: DATABASE_URL=... node tests/acclaim_feed_evidence_db.mjs
25
+ import { createRequire } from 'node:module';
26
+ import { strict as assert } from 'node:assert';
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const { Pool } = require('pg');
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+
35
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
36
+ try {
37
+ await pool.query('SELECT 1');
38
+ } catch (e) {
39
+ console.log(`acclaim_feed_evidence_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
40
+ await pool.end().catch(() => {});
41
+ process.exit(0);
42
+ }
43
+
44
+ // The query under test, taken from the shipped route. gradeQualityWhere is
45
+ // interpolated by the route at call time, so interpolate it the same way.
46
+ const { gradeQualityWhere } = require('../modules/lifecycle/db-shared.js');
47
+ const routeSrc = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
48
+ const start = routeSrc.indexOf('`SELECT t.id, t.title, t.value_summary, t.version_id, t.status,');
49
+ assert.ok(start > 0, 'the peer-votes feed query is still where this test slices it from');
50
+ const end = routeSrc.indexOf('LIMIT $2`', start);
51
+ assert.ok(end > start, 'the feed query still ends at its LIMIT');
52
+ const SQL = routeSrc
53
+ .slice(start + 1, end + 'LIMIT $2'.length)
54
+ .replace(/\$\{gradeQualityWhere\('g'\)\}/g, gradeQualityWhere('g'));
55
+ assert.ok(!SQL.includes('${'), 'every interpolation in the sliced query was resolved');
56
+
57
+ const VOTER = 90000801;
58
+ const SHIPPER = 90000802;
59
+ const VER = 'ZZZ-ACC';
60
+ // graded+costed, outage, ungraded, costed-twice, zero-cost
61
+ const T = { GOOD: 90000811, OUTAGE: 90000812, UNGRADED: 90000813, TWICE: 90000814, FREE: 90000815 };
62
+ const IDS = Object.values(T);
63
+
64
+ const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
65
+
66
+ async function cleanup() {
67
+ await q('DELETE FROM peer_votes WHERE target_task_id = ANY($1)', [IDS]);
68
+ await q('DELETE FROM cost_log WHERE task_id = ANY($1)', [IDS]);
69
+ await q('DELETE FROM task_grades WHERE task_id = ANY($1)', [IDS]);
70
+ await q('DELETE FROM tasks WHERE id = ANY($1)', [IDS]);
71
+ await q('DELETE FROM builders WHERE id = ANY($1)', [[VOTER, SHIPPER]]);
72
+ await q('DELETE FROM versions WHERE id = $1', [VER]);
73
+ }
74
+
75
+ let failures = 0;
76
+ const check = (name, fn) => {
77
+ try { fn(); console.log(` PASS ${name}`); } catch (e) {
78
+ failures += 1; console.log(` FAIL ${name}\n ${e.message}`);
79
+ }
80
+ };
81
+
82
+ try {
83
+ await cleanup();
84
+
85
+ await q("INSERT INTO versions (id, name, status) VALUES ($1, 'acclaim evidence fixture', 'planning')", [VER]);
86
+ for (const [id, login] of [[VOTER, 'zz-acc-voter'], [SHIPPER, 'zz-acc-shipper']]) {
87
+ await q("INSERT INTO builders (id, github_login, display_name, rank) VALUES ($1, $2, $2, 'metic')", [id, login]);
88
+ }
89
+ for (const [id, title] of [
90
+ [T.GOOD, '__acc__ graded and costed'],
91
+ [T.OUTAGE, '__acc__ grader outage'],
92
+ [T.UNGRADED, '__acc__ never graded'],
93
+ [T.TWICE, '__acc__ two cost rows'],
94
+ [T.FREE, '__acc__ a real zero'],
95
+ ]) {
96
+ await q(
97
+ `INSERT INTO tasks (id, version_id, title, status, shipped_by, shipped_at, discipline, kind)
98
+ VALUES ($1, $2, $3, 'shipped', $4, now(), 'engineer', 'feature')`,
99
+ [id, VER, title, SHIPPER],
100
+ );
101
+ }
102
+
103
+ const grade = (taskId, score, passed, rubric, signals) => q(
104
+ `INSERT INTO task_grades (task_id, score, passed, threshold, grader_kind, grader_version, rubric_scores, signals)
105
+ VALUES ($1, $2, $3, 7.0, 'subagent', 'test', $4::jsonb, $5::jsonb)`,
106
+ [taskId, score, passed, JSON.stringify(rubric), JSON.stringify(signals)],
107
+ );
108
+ const FULL = { functional_fidelity: 8, code_quality: 8 };
109
+ await grade(T.GOOD, 8.5, true, FULL, {});
110
+ // The shape that matters: an outage parks score 0 / passed=false with
111
+ // json-null rubric axes. A voter must never read that as a 0.0 verdict.
112
+ await grade(T.OUTAGE, 0, false, { functional_fidelity: null, code_quality: null }, { panel_outcome: 'unavailable' });
113
+ await grade(T.TWICE, 6.0, false, FULL, {});
114
+ await grade(T.FREE, 0, false, FULL, {});
115
+
116
+ const cost = (taskId, amount) => q(
117
+ "INSERT INTO cost_log (task_id, amount_usd, category, source) VALUES ($1, $2, 'api', 'test')",
118
+ [taskId, amount],
119
+ );
120
+ await cost(T.GOOD, '12.3456');
121
+ await cost(T.TWICE, '1.50');
122
+ await cost(T.TWICE, '2.25');
123
+ await cost(T.FREE, '0');
124
+ // T.OUTAGE and T.UNGRADED get no cost row at all.
125
+
126
+ const rows = await q(SQL, [VOTER, 100]);
127
+ const byId = new Map(rows.map((r) => [String(r.id), r]));
128
+ const got = (id) => {
129
+ const row = byId.get(String(id));
130
+ assert.ok(row, `task ${id} is on the feed`);
131
+ return row;
132
+ };
133
+
134
+ check('every fixture ship reaches the feed exactly once (the joins do not multiply rows)', () => {
135
+ for (const id of IDS) got(id);
136
+ for (const id of IDS) {
137
+ const n = rows.filter((r) => String(r.id) === String(id)).length;
138
+ assert.equal(n, 1, `task ${id} appears once, not ${n}`);
139
+ }
140
+ });
141
+
142
+ check('a graded, costed ship carries both figures', () => {
143
+ const r = got(T.GOOD);
144
+ assert.equal(Number(r.grade_score), 8.5);
145
+ assert.equal(r.grade_passed, true);
146
+ assert.equal(Number(r.cost_usd), 12.3456);
147
+ });
148
+
149
+ check('a GRADER OUTAGE arrives as NULL, never as its placeholder 0', () => {
150
+ const r = got(T.OUTAGE);
151
+ assert.equal(r.grade_score, null, 'the outage score is nulled by gradeQualityWhere');
152
+ assert.equal(r.grade_passed, null, 'and so is its passed flag — no verdict was reached');
153
+ });
154
+
155
+ check('a ship that was never graded has no grade row and so no score', () => {
156
+ assert.equal(got(T.UNGRADED).grade_score, null);
157
+ });
158
+
159
+ check('an unlogged cost is NULL, not 0 — the sum is never COALESCEd', () => {
160
+ assert.equal(got(T.UNGRADED).cost_usd, null, 'no cost_log row means no figure, not a free ship');
161
+ assert.equal(got(T.OUTAGE).cost_usd, null);
162
+ });
163
+
164
+ check('several cost rows sum rather than one winning', () => {
165
+ assert.equal(Number(got(T.TWICE).cost_usd), 3.75);
166
+ });
167
+
168
+ check('a genuine zero survives, because it IS a measurement', () => {
169
+ const r = got(T.FREE);
170
+ assert.equal(Number(r.cost_usd), 0, 'a logged 0 is a logged 0, distinct from NULL');
171
+ assert.notEqual(r.cost_usd, null);
172
+ assert.equal(Number(r.grade_score), 0, 'a real 0.0 verdict is not an outage and is not nulled');
173
+ assert.equal(r.grade_passed, false);
174
+ });
175
+
176
+ check('the caller never sees their own ships, so nobody votes on themselves', () => {
177
+ assert.ok(!rows.some((r) => String(r.shipped_by) === String(VOTER)));
178
+ });
179
+ } finally {
180
+ await cleanup().catch(() => {});
181
+ await pool.end().catch(() => {});
182
+ }
183
+
184
+ const passed = 8 - failures;
185
+ console.log(`\nacclaim_feed_evidence_db: ${passed} passed, ${failures} failed`);
186
+ process.exit(failures ? 1 : 0);