@bongos/core 1.19.1028 → 1.19.1030

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.1028",
6
- "core_contract": "1.19.1028",
7
- "source_commit": "56463d9a30a9ee0f32df461962daa7af33212ae5",
5
+ "core_version": "1.19.1030",
6
+ "core_contract": "1.19.1030",
7
+ "source_commit": "198e316ae502b4929018d6c592d28fd6bfa3b81d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-25T20:58:56.481Z",
9
+ "built_at": "2026-09-25T21:18:42.202Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 529,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3014,
20
- "tree_sha256": "3ca110c17a46fb54f2debd7630fb29f210243604a8c10d3e7cef945f794019eb",
20
+ "tree_sha256": "980cdb45376b2b35067d51b269d2709e1e1922dcbfb541554877cbcb9ae0c9e1",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2757,7 +2757,7 @@
2757
2757
  {
2758
2758
  "path": "docs/module-api-changelog.md",
2759
2759
  "mode": "0000644",
2760
- "sha256": "0857762db09c3257ba652f6ddc7210e09cda9663c470b79f81441090e2d64f7f"
2760
+ "sha256": "bb67419c4dd3f2311a231615a96bd462cc6806b6e3a1967ea0614e3966ef9b0e"
2761
2761
  },
2762
2762
  {
2763
2763
  "path": "docs/modules-contract.md",
@@ -5882,7 +5882,7 @@
5882
5882
  {
5883
5883
  "path": "modules/lifecycle/db-goals.js",
5884
5884
  "mode": "0000644",
5885
- "sha256": "a36a23b4f67dc43848bc0798e7b9724aee1290799dc8fa1b22e5260268bf5c70"
5885
+ "sha256": "e15bf5a3f2ec8ede9b5a8c90c3e00c472c229f351b809333beebfeb90dad2bcc"
5886
5886
  },
5887
5887
  {
5888
5888
  "path": "modules/lifecycle/db-grade.js",
@@ -6192,7 +6192,7 @@
6192
6192
  {
6193
6193
  "path": "modules/lifecycle/routes/goals.js",
6194
6194
  "mode": "0000644",
6195
- "sha256": "654addc8813a815d862da35677bb823a5d98a3685d03653fdbcb58a64aca38f4"
6195
+ "sha256": "520b63212462c762fdf23e40ffdb084c22f786616d3a5694b4dc182e334eeaa7"
6196
6196
  },
6197
6197
  {
6198
6198
  "path": "modules/lifecycle/routes/help-requests.js",
@@ -6452,7 +6452,7 @@
6452
6452
  {
6453
6453
  "path": "modules/platform-identity/activity-snapshots.js",
6454
6454
  "mode": "0000644",
6455
- "sha256": "6cf9dce12fa2689b29a149ccb56204fb494ea7b8c4cc59a3421f471162dbb529"
6455
+ "sha256": "c483a9ef792ad0ac3600dc1eb227323c6f851e7cd33189cc418a4e0ef3443ed6"
6456
6456
  },
6457
6457
  {
6458
6458
  "path": "modules/platform-identity/applicant-profile.js",
@@ -6652,7 +6652,7 @@
6652
6652
  {
6653
6653
  "path": "modules/platform-identity/platform-identity.js",
6654
6654
  "mode": "0000644",
6655
- "sha256": "16cae6287be02d85baa6ea359d80ba963df59a1d5f787f4234079b66b97812a6"
6655
+ "sha256": "0f60239ec8ae79e1cb87870d7454f6b03de84170d557a06fae93cc556757004a"
6656
6656
  },
6657
6657
  {
6658
6658
  "path": "modules/platform-identity/pollers/code-sweep.js",
@@ -6767,12 +6767,12 @@
6767
6767
  {
6768
6768
  "path": "modules/platform-identity/tests/activity-snapshots.mjs",
6769
6769
  "mode": "0000644",
6770
- "sha256": "19682ecba44275080f00d203b4f708ea4f244f6107d3dce73960f97529c3874b"
6770
+ "sha256": "47e4970496e303b66007c3c8cee9ecf1b1a10fe8805d28c8ef44d545ed7f1b4f"
6771
6771
  },
6772
6772
  {
6773
6773
  "path": "modules/platform-identity/tests/platform-identity.mjs",
6774
6774
  "mode": "0000644",
6775
- "sha256": "7db35ebebd3101fb2cb9d6a7d7e3d99c2aae176456a93d5b109f5f3ba72420e6"
6775
+ "sha256": "6bbddd8d1b85f159ba1bade8df61552979e85e50dace2e1c9128de4b9ee8beb9"
6776
6776
  },
6777
6777
  {
6778
6778
  "path": "modules/platform-identity/ui/.gitkeep",
@@ -8362,12 +8362,12 @@
8362
8362
  {
8363
8363
  "path": "package-lock.json",
8364
8364
  "mode": "0000644",
8365
- "sha256": "8a41416b983d8c2f82e4cdb0c64856dd5a304ca8414a723f648ea9b072a8f076"
8365
+ "sha256": "ab4b3ba1d87734173359f3ade610dc804a339ac3a518e7e5efebee2a1ff68cd0"
8366
8366
  },
8367
8367
  {
8368
8368
  "path": "package.json",
8369
8369
  "mode": "0000644",
8370
- "sha256": "c5a4583b1eb2bd9fafb13ecb68661a6e1f81fbbbcf4444155864af2782a0232d"
8370
+ "sha256": "ddb2d7173af124e48c454d0ba5db240e96d35541495fe7cf414246b1df433cd7"
8371
8371
  },
8372
8372
  {
8373
8373
  "path": "public-docs/index.html",
@@ -10412,7 +10412,7 @@
10412
10412
  {
10413
10413
  "path": "src/module-api.js",
10414
10414
  "mode": "0000644",
10415
- "sha256": "af6c6577a4ca4d909583bebf81192b552bc96b47dc43f387e0468fb7bdf306bd"
10415
+ "sha256": "439c0db441cc17b1ce9da268d30703325667f137d05f4b810786b356c9d56cfd"
10416
10416
  },
10417
10417
  {
10418
10418
  "path": "src/module-loader/catalog.js",
@@ -10517,7 +10517,7 @@
10517
10517
  {
10518
10518
  "path": "tests/activity_snapshots_db.mjs",
10519
10519
  "mode": "0000644",
10520
- "sha256": "5dcd2f23e6549938decf39306807dac6405f95e3245bd9fbb59a4e482a2c694d"
10520
+ "sha256": "219569f3160eb22a89e92a217b854a194afba6ebc7032dc07eff2534efc4390a"
10521
10521
  },
10522
10522
  {
10523
10523
  "path": "tests/admission_ceiling_gate.mjs",
@@ -11982,7 +11982,7 @@
11982
11982
  {
11983
11983
  "path": "tests/goal_member_kinds.mjs",
11984
11984
  "mode": "0000644",
11985
- "sha256": "3bb40fceeeeaa835db715b724ac73475be2a9ae166c740e9daa78eafc6957b03"
11985
+ "sha256": "5ae48afae42839d5b8ea6002b5d9c892d027be87eda2e60bded481437a3306a6"
11986
11986
  },
11987
11987
  {
11988
11988
  "path": "tests/goal_membership_requests.mjs",
@@ -12562,7 +12562,7 @@
12562
12562
  {
12563
12563
  "path": "tests/hub_recent_ships.mjs",
12564
12564
  "mode": "0000644",
12565
- "sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
12565
+ "sha256": "ad183edeb3b24bdd70de80aad370c5411d32257907baa36c65b70a8143d8ec53"
12566
12566
  },
12567
12567
  {
12568
12568
  "path": "tests/idea_brightness.mjs",
@@ -14597,7 +14597,7 @@
14597
14597
  {
14598
14598
  "path": "tests/sso_checkin_identity_spoof.mjs",
14599
14599
  "mode": "0000644",
14600
- "sha256": "27bd2a87e001eec2bc58e13cfa58e53f88c73ebe7504b944c718182247d98ca7"
14600
+ "sha256": "628db25b4f0c5dc157e4aa9bc9d136efd474e88769641061dfbeca6d3e013cc2"
14601
14601
  },
14602
14602
  {
14603
14603
  "path": "tests/sso_checkin_legacy_field_gone.mjs",
@@ -2535,5 +2535,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2535
2535
  landed since 1.19.1026 with no explicit bump. run 36187742785. (task 1002620)
2536
2536
  1.19.1028 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2537
2537
  landed since 1.19.1027 with no explicit bump. run 36188894725. (task 1002620)
2538
+ 1.19.1029 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2539
+ landed since 1.19.1028 with no explicit bump. run 36189693139. (task 1002620)
2540
+ 1.19.1030 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2541
+ landed since 1.19.1029 with no explicit bump. run 36190779629. (task 1002620)
2538
2542
  ---------------------------------------------------------------------------
2539
2543
  ```
@@ -942,8 +942,14 @@ async function setGoalMembershipKind({ goalId, builderId, membershipKind }, deps
942
942
  // "drops to manager" — exactly the handoff semantics the route promises. The
943
943
  // route gates WHO may call this (owner | Archon, target Metic+); this helper
944
944
  // just performs the move. Returns the updated goal row, or null for unknown id.
945
+ //
946
+ // The new owner arriving as 'lead' is a membership arrival like any other, so it
947
+ // also closes that builder's still-pending ask/invite on this goal (task
948
+ // 1003661, the third path after addGoalMember — see its comment for why
949
+ // 'cancelled'). Same statement, so the cancel commits with the transfer or not
950
+ // at all; resolvedBy is the ACTOR handing the goal over.
945
951
 
946
- async function transferGoalOwnership({ goalId, newOwnerId }, deps = {}) {
952
+ async function transferGoalOwnership({ goalId, newOwnerId, resolvedBy = null }, deps = {}) {
947
953
  const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
948
954
  const { rows } = await activePool.query(
949
955
  `WITH g AS (
@@ -955,9 +961,15 @@ async function transferGoalOwnership({ goalId, newOwnerId }, deps = {}) {
955
961
  INSERT INTO goal_members (goal_id, builder_id, membership_kind)
956
962
  SELECT g.id, $2, 'lead' FROM g
957
963
  ON CONFLICT (goal_id, builder_id) DO UPDATE SET membership_kind = 'lead'
964
+ ), cancelled AS (
965
+ UPDATE goal_membership_requests
966
+ SET status = 'cancelled', resolved_by = $3, resolved_at = now(), updated_at = now()
967
+ WHERE goal_id = $1 AND builder_id = $2 AND status = 'pending'
968
+ AND EXISTS (SELECT 1 FROM g)
969
+ RETURNING id
958
970
  )
959
971
  SELECT * FROM g`,
960
- [goalId, newOwnerId]
972
+ [goalId, newOwnerId, resolvedBy]
961
973
  );
962
974
  return rows[0] ?? null;
963
975
  }
@@ -899,7 +899,7 @@ module.exports = function buildGoalsRouter() {
899
899
  if (decision.noop) {
900
900
  return res.json({ ok: true, goal, changed: false });
901
901
  }
902
- const updated = await db.transferGoalOwnership({ goalId: id, newOwnerId });
902
+ const updated = await db.transferGoalOwnership({ goalId: id, newOwnerId, resolvedBy: req.builder.id });
903
903
  if (!updated) {
904
904
  return res.fail('transfer_conflict', { status: 409, message: 'The goal changed underneath this request — re-read it and retry.' });
905
905
  }
@@ -11,37 +11,53 @@
11
11
  // Compaction (daily granularity after 90 days) is a later, volume-triggered task
12
12
  // — nothing here prunes. A disconnected/re-registered project's snapshots are
13
13
  // likewise not purged here — platform_identity_builder_project_activity (the
14
- // table this mirrors) has the identical gap today; the future leaderboard reader
15
- // is where consent (rollupVisibleSql) is enforced, per that table's own precedent.
14
+ // table this mirrors) has the identical gap today; the leaderboard reader
15
+ // (community-leaderboard.js) is where consent (rollupVisibleSql) is enforced.
16
16
  'use strict';
17
17
 
18
- const { pool: defaultPool } = require('../../src/module-api');
18
+ const api = require('../../src/module-api');
19
19
 
20
- // Append one snapshot row iff the totals differ from the last one on record for
21
- // this (client, builder) pair — ONE statement, not a read then a write: the CTE
22
- // and the conditional INSERT run in the same round trip, closing the two-round-
23
- // trip check-then-insert race the earlier design had. A DB-level race remains
24
- // under READ COMMITTED — two truly concurrent statements for the same pair can
25
- // each see no conflicting row and both insert — but the worst case is a harmless
26
- // duplicate snapshot with IDENTICAL totals, which reads the same either way in
27
- // the windowed-delta arithmetic. A first-ever report (no prior row) always
28
- // inserts — NOT EXISTS is true against an empty CTE regardless of the
29
- // comparison. `id DESC` breaks a tie on an identical `reported_at` so "the last
30
- // snapshot" is always one specific row.
31
- async function recordActivitySnapshot({ clientId, githubId, credits = 0, tasksShipped = 0, karma = 0 }, { pool = defaultPool } = {}) {
32
- const { rows } = await pool.query(
33
- `WITH last AS (
34
- SELECT credits, tasks_shipped, karma FROM platform_identity_activity_snapshots
35
- WHERE client_id = $1 AND github_id = $2
36
- ORDER BY reported_at DESC, id DESC LIMIT 1
37
- )
38
- INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
39
- SELECT $1, $2, $3, $4, $5, now()
40
- WHERE NOT EXISTS (SELECT 1 FROM last WHERE credits = $3 AND tasks_shipped = $4 AND karma = $5)
41
- RETURNING id, reported_at`,
42
- [clientId, githubId, credits, tasksShipped, karma],
43
- );
44
- return rows[0] || null;
20
+ // Snapshot the LIVE totals of one (project, builder) iff they differ from the
21
+ // last snapshot. Must run on the transaction that just upserted the live row:
22
+ // that upsert's row lock queues racing reports, so this sees every earlier
23
+ // snapshot and a live row no one else can change (task 1004244).
24
+ // Best-effort: a fault rolls back to the savepoint, is logged, and returns null,
25
+ // so the upsert still commits. If even that rollback fails the transaction is
26
+ // gone, and the error propagates rather than let COMMIT silently drop the upsert.
27
+ // reported_at is the write's own moment (clock_timestamp(); now() would be the
28
+ // transaction start, before any wait on the lock), never earlier than the last
29
+ // row's, so the index order (reported_at, id) always ends at the last write.
30
+ async function recordActivitySnapshot({ clientId, githubId }, { client }) {
31
+ await client.query('SAVEPOINT activity_snapshot');
32
+ try {
33
+ const { rows } = await client.query(
34
+ `WITH live AS (
35
+ SELECT credits, tasks_shipped, karma FROM platform_identity_builder_project_activity
36
+ WHERE client_id = $1 AND github_id = $2
37
+ ), last AS (
38
+ SELECT credits, tasks_shipped, karma, reported_at FROM platform_identity_activity_snapshots
39
+ WHERE client_id = $1 AND github_id = $2
40
+ ORDER BY reported_at DESC, id DESC LIMIT 1
41
+ )
42
+ INSERT INTO platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at)
43
+ SELECT $1, $2, live.credits, live.tasks_shipped, live.karma,
44
+ GREATEST(clock_timestamp(), (SELECT reported_at FROM last))
45
+ FROM live
46
+ WHERE NOT EXISTS (
47
+ SELECT 1 FROM last
48
+ WHERE last.credits = live.credits
49
+ AND last.tasks_shipped = live.tasks_shipped
50
+ AND last.karma = live.karma)
51
+ RETURNING id, reported_at`,
52
+ [clientId, githubId],
53
+ );
54
+ await client.query('RELEASE SAVEPOINT activity_snapshot');
55
+ return rows[0] || null;
56
+ } catch (err) {
57
+ await client.query('ROLLBACK TO SAVEPOINT activity_snapshot');
58
+ api.logger('platform-identity').error({ err }, 'activity snapshot write failed (non-fatal)');
59
+ return null;
60
+ }
45
61
  }
46
62
 
47
63
  module.exports = { recordActivitySnapshot };
@@ -666,31 +666,26 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
666
666
  .filter((d) => typeof d === 'string' && d.length > 0 && d.length <= MAX_DISCIPLINE_LENGTH)
667
667
  .slice(0, MAX_ROLLUP_DISCIPLINES);
668
668
  const ships = sanitizeRecentShips(recentShips, clientOrigin);
669
- const { rows } = await pool.query(
670
- `INSERT INTO platform_identity_builder_project_activity
671
- (client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, recent_ships, last_active, updated_at)
672
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now(), now())
673
- ON CONFLICT (client_id, github_id) DO UPDATE SET
674
- credits = EXCLUDED.credits, tasks_shipped = EXCLUDED.tasks_shipped, karma = EXCLUDED.karma,
675
- rank_held = EXCLUDED.rank_held, disciplines = EXCLUDED.disciplines,
676
- recent_ships = EXCLUDED.recent_ships,
677
- last_active = now(), updated_at = now()
678
- RETURNING id, client_id, github_id`,
679
- // jsonb is bound as a JSON STRING: node-postgres serializes a JS array as a
680
- // Postgres ARRAY literal, which the jsonb column would reject.
681
- [clientId, githubId, credits | 0, tasksShipped | 0, karma | 0, safeRank, disc, JSON.stringify(ships)],
682
- );
683
- // Windowed data plane (task 1002977): one snapshot per report whose totals
684
- // changed, skipped on a no-op re-report — see activity-snapshots.js.
685
- // Best-effort, the application-echo.js precedent: descriptive analytics, never
686
- // authority, so a fault here (e.g. migration 022 not yet applied on this
687
- // instance) must never fail the rollup report that already committed above.
688
- try {
689
- await recordActivitySnapshot({ clientId, githubId, credits: credits | 0, tasksShipped: tasksShipped | 0, karma: karma | 0 }, { pool });
690
- } catch (err) {
691
- api.logger('platform-identity').error(`activity snapshot write failed (non-fatal): ${err && err.message}`);
692
- }
693
- return rows[0];
669
+ // One transaction with the snapshot: the upsert's row lock queues racing reports (task 1004244).
670
+ return api.withTx(async (client) => {
671
+ const { rows } = await client.query(
672
+ `INSERT INTO platform_identity_builder_project_activity
673
+ (client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, recent_ships, last_active, updated_at)
674
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now(), now())
675
+ ON CONFLICT (client_id, github_id) DO UPDATE SET
676
+ credits = EXCLUDED.credits, tasks_shipped = EXCLUDED.tasks_shipped, karma = EXCLUDED.karma,
677
+ rank_held = EXCLUDED.rank_held, disciplines = EXCLUDED.disciplines,
678
+ recent_ships = EXCLUDED.recent_ships,
679
+ last_active = now(), updated_at = now()
680
+ RETURNING id, client_id, github_id`,
681
+ // jsonb is bound as a JSON STRING: node-postgres serializes a JS array as a
682
+ // Postgres ARRAY literal, which the jsonb column would reject.
683
+ [clientId, githubId, credits | 0, tasksShipped | 0, karma | 0, safeRank, disc, JSON.stringify(ships)],
684
+ );
685
+ // Windowed data plane (task 1002977) — best-effort, see activity-snapshots.js.
686
+ await recordActivitySnapshot({ clientId, githubId }, { client });
687
+ return rows[0];
688
+ }, { pool });
694
689
  }
695
690
 
696
691
  // The scouting rollup: one row PER BUILDER, aggregated across every project they
@@ -1,11 +1,14 @@
1
1
  // modules/platform-identity/tests/activity-snapshots.mjs
2
2
  //
3
3
  // DB-free unit suite for the windowed data plane's append-only snapshot history
4
- // (community spec D2, task 1002977). Recording-fake-pool style, matching
5
- // tests/platform-identity.mjs: proves the SQL SHAPE (one statement, correct
6
- // table/columns, bound params, the tiebreak) — a fake pool cannot execute the
7
- // `WHERE NOT EXISTS` itself, so the skip-on-unchanged BEHAVIOUR is proved
8
- // against real Postgres in tests/activity_snapshots_db.mjs instead.
4
+ // (community spec D2, task 1002977; the live-row read, task 1004244).
5
+ // Recording-fake-client style, matching tests/platform-identity.mjs: proves the
6
+ // SHAPE — a SAVEPOINT on the caller's transaction, ONE INSERT that reads the
7
+ // totals from the live row (never from the caller) guarded by the last snapshot,
8
+ // RELEASE; on a fault, ROLLBACK TO SAVEPOINT, log the Error, return null. A fake
9
+ // cannot execute `WHERE NOT EXISTS` or a lock, so WHICH reports insert and how
10
+ // racing reports settle are proved against real Postgres in
11
+ // tests/activity_snapshots_db.mjs.
9
12
  //
10
13
  // Run standalone: node modules/platform-identity/tests/activity-snapshots.mjs
11
14
 
@@ -13,6 +16,14 @@ import { strict as assert } from 'node:assert';
13
16
  import { createRequire } from 'node:module';
14
17
 
15
18
  const require = createRequire(import.meta.url);
19
+
20
+ // Pin the doorway's logger so the test can read what a fault logs.
21
+ const logged = [];
22
+ const moduleApiPath = require.resolve('../../../src/module-api.js');
23
+ require.cache[moduleApiPath] = {
24
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
25
+ exports: { logger: () => ({ error: (obj, msg) => logged.push({ obj, msg }) }) },
26
+ };
16
27
  const { recordActivitySnapshot } = require('../activity-snapshots.js');
17
28
 
18
29
  let passed = 0, failed = 0;
@@ -21,47 +32,75 @@ async function ta(name, fn) {
21
32
  catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
22
33
  }
23
34
 
24
- // A recording fake pool: returns queued responses in order and records every call.
25
- function mockPool(responses = []) {
35
+ // A recording fake client: answers the INSERT with `insertRows`, or fails it;
36
+ // `failRollback` also fails the savepoint rollback (a dead connection).
37
+ function mockClient({ insertRows = [], failInsert = null, failRollback = null } = {}) {
26
38
  const calls = [];
27
- let i = 0;
28
39
  return {
29
40
  calls,
30
41
  query: async (text, params) => {
31
42
  calls.push({ text: String(text), params: params || [] });
32
- const r = responses[i++];
33
- return r || { rows: [], rowCount: 0 };
43
+ if (/INSERT INTO platform_identity_activity_snapshots/.test(text)) {
44
+ if (failInsert) throw failInsert;
45
+ return { rows: insertRows };
46
+ }
47
+ if (/^ROLLBACK TO SAVEPOINT/.test(text) && failRollback) throw failRollback;
48
+ return { rows: [] };
34
49
  },
35
50
  };
36
51
  }
37
52
 
38
- console.log('recordActivitySnapshot (community spec D2, task 1002977):');
53
+ const texts = (client) => client.calls.map((c) => c.text.replace(/\s+/g, ' ').trim());
54
+
55
+ console.log('recordActivitySnapshot (community spec D2, tasks 1002977 + 1004244):');
39
56
 
40
- await ta('one statement: a CTE for the last snapshot, guarding a conditional INSERT', async () => {
41
- const pool = mockPool([{ rows: [{ id: '1', reported_at: 'T1' }] }]);
42
- const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
43
- assert.equal(pool.calls.length, 1, 'one round trip, not a read then a write');
44
- const c = pool.calls[0];
45
- assert.match(c.text, /WITH last AS/);
46
- assert.match(c.text, /FROM platform_identity_activity_snapshots/);
47
- assert.match(c.text, /ORDER BY reported_at DESC, id DESC LIMIT 1/, 'a deterministic tiebreak on an identical reported_at');
48
- assert.match(c.text, /INSERT INTO platform_identity_activity_snapshots/);
49
- assert.match(c.text, /WHERE NOT EXISTS \(SELECT 1 FROM last WHERE credits = \$3 AND tasks_shipped = \$4 AND karma = \$5\)/);
50
- assert.deepEqual(c.params, ['c1', 42, 100, 5, 3]);
57
+ await ta('on the caller\'s transaction: SAVEPOINT, one conditional INSERT, RELEASE', async () => {
58
+ const client = mockClient({ insertRows: [{ id: '1', reported_at: 'T1' }] });
59
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
60
+ const t = texts(client);
61
+ assert.equal(t.length, 3);
62
+ assert.equal(t[0], 'SAVEPOINT activity_snapshot');
63
+ assert.match(t[1], /^WITH live AS/);
64
+ assert.equal(t[2], 'RELEASE SAVEPOINT activity_snapshot');
51
65
  assert.deepEqual(out, { id: '1', reported_at: 'T1' });
52
66
  });
53
67
 
54
- await ta('a skipped report (the fake pool answers no rows, as a real WHERE NOT EXISTS would on a match) returns null', async () => {
55
- const pool = mockPool([{ rows: [] }]);
56
- const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
57
- assert.equal(pool.calls.length, 1);
68
+ await ta('the INSERT reads the totals from the LIVE row, never from the caller', async () => {
69
+ const client = mockClient();
70
+ await recordActivitySnapshot({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { client });
71
+ assert.deepEqual(client.calls[1].params, ['c1', 42], 'only the pair is bound — a captured total has nowhere to go');
72
+ const sql = texts(client)[1];
73
+ assert.match(sql, /WITH live AS \( SELECT credits, tasks_shipped, karma FROM platform_identity_builder_project_activity WHERE client_id = \$1 AND github_id = \$2 \)/);
74
+ assert.match(sql, /SELECT \$1, \$2, live\.credits, live\.tasks_shipped, live\.karma, GREATEST\(clock_timestamp\(\), \(SELECT reported_at FROM last\)\) FROM live/,
75
+ 'stamped at the write, and never earlier than the last row');
76
+ assert.ok(!/\$3/.test(sql), 'no third parameter exists to carry a caller value');
77
+ });
78
+
79
+ await ta('skipped when the live totals equal the last snapshot, found in migration 022\'s index order', async () => {
80
+ const client = mockClient();
81
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
82
+ const sql = texts(client)[1];
83
+ assert.match(sql, /last AS \( SELECT credits, tasks_shipped, karma, reported_at FROM platform_identity_activity_snapshots WHERE client_id = \$1 AND github_id = \$2 ORDER BY reported_at DESC, id DESC LIMIT 1 \)/,
84
+ '(client_id, github_id, reported_at DESC, id DESC) serves this as an index-order LIMIT 1');
85
+ assert.match(sql, /WHERE NOT EXISTS \( SELECT 1 FROM last WHERE last\.credits = live\.credits AND last\.tasks_shipped = live\.tasks_shipped AND last\.karma = live\.karma\)/);
86
+ assert.equal(out, null, 'no row returned → null');
87
+ });
88
+
89
+ await ta('a fault rolls back to the savepoint, logs the Error itself, and returns null — the caller\'s upsert still commits', async () => {
90
+ logged.length = 0;
91
+ const fault = Object.assign(new Error('relation "platform_identity_activity_snapshots" does not exist'), { code: '42P01' });
92
+ const client = mockClient({ failInsert: fault });
93
+ const out = await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client });
58
94
  assert.equal(out, null);
95
+ const t = texts(client);
96
+ assert.equal(t[t.length - 1], 'ROLLBACK TO SAVEPOINT activity_snapshot');
97
+ assert.equal(logged.length, 1);
98
+ assert.equal(logged[0].obj.err, fault, 'the Error object itself, so its stack and SQLSTATE reach the log');
59
99
  });
60
100
 
61
- await ta('missing totals default to zero, like upsertActivity\'s own defaults', async () => {
62
- const pool = mockPool([{ rows: [{ id: '3', reported_at: 'T3' }] }]);
63
- await recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { pool });
64
- assert.deepEqual(pool.calls[0].params, ['c1', 42, 0, 0, 0]);
101
+ await ta('if the savepoint rollback itself fails, the error propagates — COMMIT must not silently drop the upsert', async () => {
102
+ const client = mockClient({ failInsert: new Error('boom'), failRollback: new Error('connection terminated') });
103
+ await assert.rejects(() => recordActivitySnapshot({ clientId: 'c1', githubId: 42 }, { client }), /connection terminated/);
65
104
  });
66
105
 
67
106
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -558,11 +558,26 @@ await ta('getAccountByLogin resolves case-insensitively, newest first', async ()
558
558
 
559
559
  console.log('\ncross-project activity (§4 scouting data plane):');
560
560
 
561
+ // upsertActivity runs in a transaction, so a positional answer queue would be
562
+ // spent by BEGIN: answer by statement shape instead, and find the upsert by text.
563
+ const activityPool = ({ failSnapshot = false } = {}) => {
564
+ const calls = [];
565
+ const query = async (text, params) => {
566
+ calls.push({ text: String(text), params: params || [] });
567
+ if (failSnapshot && /INSERT INTO platform_identity_activity_snapshots/.test(text)) {
568
+ throw new Error('relation "platform_identity_activity_snapshots" does not exist');
569
+ }
570
+ if (/INSERT INTO platform_identity_builder_project_activity/.test(text)) return { rows: [{ id: '1' }] };
571
+ return { rows: [] };
572
+ };
573
+ return { calls, query, connect: async () => ({ query, release: () => {} }) };
574
+ };
575
+ const upsertCall = (pool) => pool.calls.find((c) => /INSERT INTO platform_identity_builder_project_activity/.test(c.text));
576
+
561
577
  await ta('upsertActivity upserts per (client_id, github_id) with the talent snapshot', async () => {
562
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
578
+ const pool = activityPool();
563
579
  await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3, rank: 'metic', disciplines: ['engineer'] }, { pool });
564
- const c = pool.calls[0];
565
- assert.match(c.text, /INSERT INTO platform_identity_builder_project_activity/);
580
+ const c = upsertCall(pool);
566
581
  assert.match(c.text, /ON CONFLICT \(client_id, github_id\) DO UPDATE/);
567
582
  // recent_ships (task 1002803) rides the same row as a jsonb STRING param —
568
583
  // an empty list when the sender reported none, never NULL. Its own cap,
@@ -571,7 +586,7 @@ await ta('upsertActivity upserts per (client_id, github_id) with the talent snap
571
586
  });
572
587
 
573
588
  await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
574
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
589
+ const pool = activityPool();
575
590
  await P.upsertActivity({
576
591
  clientId: 'c1', githubId: 42,
577
592
  rank: 'Supreme Overlord', // not one of the four core rank keys → stored null
@@ -581,7 +596,7 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
581
596
  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', // valid tail — capped to 8 total
582
597
  ],
583
598
  }, { pool });
584
- const c = pool.calls[0];
599
+ const c = upsertCall(pool);
585
600
  assert.equal(c.params[5], null, 'an invented rank label is never stored');
586
601
  const disc = c.params[6];
587
602
  assert.ok(disc.length <= 8, `disciplines capped at 8 (got ${disc.length})`);
@@ -591,30 +606,35 @@ await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-la
591
606
  });
592
607
 
593
608
  await ta('upsertActivity keeps a legitimate core rank key', async () => {
594
- const pool = mockPool([{ rows: [{ id: '1' }] }]);
609
+ const pool = activityPool();
595
610
  await P.upsertActivity({ clientId: 'c1', githubId: 42, rank: 'archon', disciplines: [] }, { pool });
596
- assert.equal(pool.calls[0].params[5], 'archon');
597
- });
598
-
599
- await ta('upsertActivity also appends to the snapshot history (task 1002977, community spec D2) — deleting that call would leave this red', async () => {
600
- const pool = mockPool([{ rows: [{ id: '1' }] }, { rows: [{ id: 's1', reported_at: 'T' }] }]);
601
- await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
602
- assert.equal(pool.calls.length, 2, 'the rollup upsert, then the snapshot statement');
603
- assert.match(pool.calls[1].text, /INSERT INTO platform_identity_activity_snapshots/);
604
- assert.deepEqual(pool.calls[1].params, ['c1', 42, 100, 5, 3]);
611
+ assert.equal(upsertCall(pool).params[5], 'archon');
605
612
  });
606
613
 
607
- await ta('upsertActivity survives a snapshot-write failure (best-effort — the application-echo.js precedent): the rollup report still succeeds', async () => {
608
- const pool = {
609
- calls: [],
610
- query: async (text, params) => {
611
- pool.calls.push({ text: String(text), params });
612
- if (/INSERT INTO platform_identity_activity_snapshots/.test(text)) throw new Error('relation "platform_identity_activity_snapshots" does not exist');
613
- return { rows: [{ id: '1' }] };
614
- },
615
- };
614
+ await ta('upsertActivity writes the upsert and its snapshot in ONE transaction, the snapshot reading the live row (tasks 1002977 + 1004244)', async () => {
615
+ const pool = activityPool();
616
+ const out = await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
617
+ const t = pool.calls.map((c) => c.text.replace(/\s+/g, ' ').trim().split(' ').slice(0, 4).join(' '));
618
+ assert.deepEqual(t, [
619
+ 'BEGIN',
620
+ 'INSERT INTO platform_identity_builder_project_activity (client_id,',
621
+ 'SAVEPOINT activity_snapshot',
622
+ 'WITH live AS (',
623
+ 'RELEASE SAVEPOINT activity_snapshot',
624
+ 'COMMIT',
625
+ ], 'the upsert holds the row lock until its snapshot is written; deleting the snapshot call leaves this red');
626
+ const snap = pool.calls.find((c) => /INSERT INTO platform_identity_activity_snapshots/.test(c.text));
627
+ assert.deepEqual(snap.params, ['c1', 42], 'the report\'s own totals are never handed to the snapshot');
628
+ assert.equal(out.id, '1');
629
+ });
630
+
631
+ await ta('upsertActivity survives a snapshot-write failure (best-effort — the application-echo.js precedent): the savepoint rolls back alone and the upsert commits', async () => {
632
+ const pool = activityPool({ failSnapshot: true });
616
633
  const out = await P.upsertActivity({ clientId: 'c1', githubId: 42, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
617
634
  assert.equal(out.id, '1', 'the rollup upsert result still returns despite the snapshot write throwing');
635
+ const t = pool.calls.map((c) => c.text.trim());
636
+ assert.ok(t.includes('ROLLBACK TO SAVEPOINT activity_snapshot'));
637
+ assert.equal(t[t.length - 1], 'COMMIT', 'the transaction still commits the upsert');
618
638
  });
619
639
 
620
640
  await ta('getMembership reads one (client, builder) row — the rollup membership requirement', async () => {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1028",
3
+ "version": "1.19.1030",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1028",
9
+ "version": "1.19.1030",
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.1028",
3
+ "version": "1.19.1030",
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
@@ -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.1028'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1030'; // 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');
@@ -1,11 +1,12 @@
1
- // tests/activity_snapshots_db.mjs — task 1002977 (community spec D2; real DB).
1
+ // tests/activity_snapshots_db.mjs — tasks 1002977 + 1004244 (community spec D2; real DB).
2
2
  //
3
3
  // Proves the windowed data plane's append-only history against real SQL. The
4
- // skip-on-unchanged rule lives entirely inside one INSERT ... WHERE NOT EXISTS
5
- // statement (activity-snapshots.js), so a fake pool can only prove the query
6
- // SHAPE (modules/platform-identity/tests/activity-snapshots.mjs); only a real
7
- // Postgres can prove WHICH reports it actually inserts, and that upsertActivity
8
- // (the existing POST /sso/activity/rollup path) really calls it end to end.
4
+ // skip-on-unchanged rule lives inside one INSERT ... WHERE NOT EXISTS, and racing
5
+ // reports are serialised by the upsert's own row lock, so a fake pool can only
6
+ // prove the SHAPE (modules/platform-identity/tests/activity-snapshots.mjs); only
7
+ // a real Postgres can prove WHICH reports insert, that upsertActivity (the
8
+ // POST /sso/activity/rollup path) writes the upsert and its snapshot together,
9
+ // and that the newest snapshot always agrees with the live row.
9
10
  //
10
11
  // Real-DB integration test: self-skips without a reachable Postgres; in the
11
12
  // INTEGRATION set (scripts/gds/run-unit-tests.js) so the default DB-free lane
@@ -37,58 +38,126 @@ try {
37
38
  }
38
39
 
39
40
  // High-numbered fixture ids so a shared DB can never collide with real data.
40
- const BUILDER = 90002977, OTHER_BUILDER = 90002978;
41
- const IDS = [BUILDER, OTHER_BUILDER];
41
+ const BUILDER = 90002977, OTHER_BUILDER = 90002978, RACE_BUILDER = 90002979, FLIGHT_BUILDER = 90002980;
42
+ const IDS = [BUILDER, OTHER_BUILDER, RACE_BUILDER, FLIGHT_BUILDER];
42
43
  const CLIENT = 't1002977-client';
43
44
  const q = (sql, params) => pool.query(sql, params);
45
+ const report = (githubId, credits) =>
46
+ pi.upsertActivity({ clientId: CLIENT, githubId, credits, tasksShipped: 1, karma: 0 }, { pool });
44
47
 
45
48
  async function cleanup() {
46
49
  await q(`DELETE FROM platform_identity_activity_snapshots WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
47
50
  await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [IDS]).catch(() => {});
48
51
  }
52
+ // The writer runs on its caller's transaction, right after the live row is set.
53
+ async function setLiveAndSnapshot(githubId, credits) {
54
+ const client = await pool.connect();
55
+ try {
56
+ await client.query('BEGIN');
57
+ await client.query(
58
+ `INSERT INTO platform_identity_builder_project_activity (client_id, github_id, credits, tasks_shipped, karma)
59
+ VALUES ($1, $2, $3, 5, 3)
60
+ ON CONFLICT (client_id, github_id) DO UPDATE SET credits = EXCLUDED.credits`,
61
+ [CLIENT, githubId, credits],
62
+ );
63
+ const row = await recordActivitySnapshot({ clientId: CLIENT, githubId }, { client });
64
+ await client.query('COMMIT');
65
+ return row;
66
+ } finally {
67
+ client.release();
68
+ }
69
+ }
70
+ async function history(githubId) {
71
+ const { rows } = await q(
72
+ `SELECT credits FROM platform_identity_activity_snapshots
73
+ WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
74
+ [CLIENT, githubId],
75
+ );
76
+ return rows.map((r) => r.credits);
77
+ }
78
+ // The newest snapshot (in the index order the writer and the leaderboard use) against the live row.
79
+ async function newestVsLive(githubId) {
80
+ const { rows } = await q(
81
+ `SELECT (SELECT credits FROM platform_identity_activity_snapshots
82
+ WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at DESC, id DESC LIMIT 1) AS newest,
83
+ (SELECT credits FROM platform_identity_builder_project_activity
84
+ WHERE client_id = $1 AND github_id = $2) AS live`,
85
+ [CLIENT, githubId],
86
+ );
87
+ return [rows[0].newest, rows[0].live];
88
+ }
49
89
 
50
90
  let pass = 0;
51
91
  const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
92
+ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
52
93
 
53
94
  try {
54
95
  await q(migrationSql('platform_identity_005_builder_activity.sql'));
55
96
  // upsertActivity's INSERT always names recent_ships (012_recent_ships adds the
56
- // column); without it here, step 2 below fails 42703 on a fresh database.
97
+ // column); without it here, the upsertActivity steps fail 42703 on a fresh database.
57
98
  await q(migrationSql('platform_identity_012_recent_ships.sql'));
58
99
  await q(migrationSql('platform_identity_022_activity_snapshots.sql'));
59
100
  await cleanup();
60
101
 
61
- // 1 — recordActivitySnapshot directly: the changed-totals skip rule.
62
- const first = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
63
- ok(first !== null, 'a first-ever report inserts — nothing to be unchanged from');
64
-
65
- const unchanged = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 100, tasksShipped: 5, karma: 3 }, { pool });
66
- ok(unchanged === null, 'an identical re-report inserts nothing');
67
-
68
- const changed = await recordActivitySnapshot({ clientId: CLIENT, githubId: BUILDER, credits: 150, tasksShipped: 5, karma: 3 }, { pool });
69
- ok(changed !== null, 'a changed report (credits differ) inserts a new row');
70
-
71
- const { rows: history } = await q(
72
- `SELECT credits FROM platform_identity_activity_snapshots
73
- WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
102
+ // 1 — the writer snapshots the LIVE row, and only when it changed.
103
+ ok(await setLiveAndSnapshot(BUILDER, 100) !== null, 'a first-ever report inserts — nothing to be unchanged from');
104
+ ok(await setLiveAndSnapshot(BUILDER, 100) === null, 'an unchanged live row inserts nothing');
105
+ ok(await setLiveAndSnapshot(BUILDER, 150) !== null, 'a changed live row (credits differ) inserts a new row');
106
+ same(await history(BUILDER), [100, 150], 'exactly two rows, in write order — the unchanged report left no trace');
107
+ // A clock that stepped back: the last row carries a later stamp than "now".
108
+ await q(
109
+ `UPDATE platform_identity_activity_snapshots SET reported_at = clock_timestamp() + interval '1 hour'
110
+ WHERE client_id = $1 AND github_id = $2 AND credits = 150`,
74
111
  [CLIENT, BUILDER],
75
112
  );
76
- ok(history.length === 2, 'exactly two rows exist — the unchanged report left no trace');
77
- assert.deepEqual(history.map((r) => r.credits), [100, 150]);
78
- pass++; console.log(' ok - the history is in report order');
113
+ const later = await setLiveAndSnapshot(BUILDER, 175);
114
+ same((await newestVsLive(BUILDER))[0], 175, 'a write after the clock stepped back is still the newest in index order');
115
+ ok(later !== null, 'and it was written, stamped no earlier than the row before it');
79
116
 
80
- // 2 — upsertActivity (the real POST /sso/activity/rollup path) really calls it.
81
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
82
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 10, tasksShipped: 1, karma: 0 }, { pool });
83
- await pi.upsertActivity({ clientId: CLIENT, githubId: OTHER_BUILDER, credits: 20, tasksShipped: 1, karma: 0 }, { pool });
84
- const { rows: viaRollup } = await q(
85
- `SELECT credits FROM platform_identity_activity_snapshots
86
- WHERE client_id = $1 AND github_id = $2 ORDER BY reported_at, id`,
87
- [CLIENT, OTHER_BUILDER],
88
- );
89
- ok(viaRollup.length === 2, 'upsertActivity appends a snapshot per changed report — two calls, one repeat, one row skipped');
90
- assert.deepEqual(viaRollup.map((r) => r.credits), [10, 20]);
91
- pass++; console.log(' ok - deleting the call inside upsertActivity would leave this red');
117
+ // 2 — a report whose snapshot is still pending holds the row: its upsert is
118
+ // not visible yet, and a second report queues behind it. The prior state is the
119
+ // hard case the live-row read alone left open — the last snapshot (110) already
120
+ // equals the fresh report, while a stale report (100) is mid-flight. A table
121
+ // lock parks the stale report at its snapshot INSERT, deterministically.
122
+ await report(FLIGHT_BUILDER, 110);
123
+ const parker = await pool.connect();
124
+ let stale, fresh;
125
+ const settled = { stale: false, fresh: false };
126
+ try {
127
+ await parker.query('BEGIN');
128
+ await parker.query('LOCK TABLE platform_identity_activity_snapshots IN SHARE ROW EXCLUSIVE MODE');
129
+ stale = report(FLIGHT_BUILDER, 100).then(() => { settled.stale = true; });
130
+ await new Promise((r) => setTimeout(r, 300));
131
+ same((await newestVsLive(FLIGHT_BUILDER))[1], 110,
132
+ "the stale report's upsert is invisible while its snapshot is pending — the two commit together");
133
+ fresh = report(FLIGHT_BUILDER, 110).then(() => { settled.fresh = true; });
134
+ await new Promise((r) => setTimeout(r, 300));
135
+ ok(!settled.stale && !settled.fresh, 'and the fresh report queues behind it on the row lock');
136
+ await parker.query('COMMIT');
137
+ } finally {
138
+ parker.release();
139
+ }
140
+ await Promise.all([stale, fresh]);
141
+ same(await newestVsLive(FLIGHT_BUILDER), [110, 110],
142
+ 'once both land, the newest snapshot agrees with the live row (the stale value is recorded, then superseded)');
143
+ same(await history(FLIGHT_BUILDER), [110, 100, 110], 'every change the live row went through is in the history');
144
+
145
+ // 3 — racing reports, end to end: however they interleave, once they settle
146
+ // the newest snapshot equals the live row.
147
+ for (let round = 0; round < 15; round++) {
148
+ const credits = [0, 1, 2, 3, 4].map((i) => 1000 + round * 10 + ((i * 7 + round) % 5));
149
+ await Promise.all(credits.map((c) => report(RACE_BUILDER, c)));
150
+ const [newest, live] = await newestVsLive(RACE_BUILDER);
151
+ assert.equal(newest, live, `round ${round}: newest snapshot ${newest} disagrees with the live row ${live}`);
152
+ }
153
+ pass++; console.log(' ok - 15 rounds of five racing reports: the newest snapshot always equals the live row');
154
+
155
+ // 4 — upsertActivity (the real POST /sso/activity/rollup path) really snapshots.
156
+ await report(OTHER_BUILDER, 10);
157
+ await report(OTHER_BUILDER, 10);
158
+ await report(OTHER_BUILDER, 20);
159
+ same(await history(OTHER_BUILDER), [10, 20],
160
+ 'upsertActivity appends a snapshot per changed report — deleting the call inside it would leave this red');
92
161
 
93
162
  console.log(`\nactivity_snapshots_db.mjs: ${pass} passed`);
94
163
  } finally {
@@ -400,7 +400,7 @@ await test('transferGoalOwnership: created_by moves and the new owner upserts to
400
400
  const out = await transferGoalOwnership({ goalId: 7, newOwnerId: 5 }, { pool });
401
401
  assert.deepEqual(out, GOAL_ROW);
402
402
  const upd = find(pool.queries, /UPDATE goals SET created_by = \$2/);
403
- assert.deepEqual(upd.params, [7, 5]);
403
+ assert.deepEqual(upd.params, [7, 5, null], 'resolvedBy defaults to null for a non-route caller');
404
404
  const sql = String(upd.sql).replace(/\s+/g, ' ');
405
405
  assert.match(sql, /INSERT INTO goal_members \(goal_id, builder_id, membership_kind\)/,
406
406
  'the new owner\'s membership rides in the same statement');
@@ -409,6 +409,29 @@ await test('transferGoalOwnership: created_by moves and the new owner upserts to
409
409
  assert.match(sql, /updated_at = now\(\)/);
410
410
  });
411
411
 
412
+ // task 1003661 — the third arrival path (after join / direct-add, task 1002888):
413
+ // handing a goal to a builder who still has a pending ask outstanding on it must
414
+ // close that ask in the SAME statement, or the stale row lingers in the inbox.
415
+ await test('transferGoalOwnership: cancels the new owner\'s pending ask in the same statement, attributed to the actor', async () => {
416
+ const pool = fakePool((s) => (/UPDATE goals SET created_by = \$2/.test(s) ? { rows: [GOAL_ROW] } : null));
417
+ await transferGoalOwnership({ goalId: 7, newOwnerId: 5, resolvedBy: 99 }, { pool });
418
+ assert.equal(pool.queries.length, 1, 'ONE statement — the cancel commits with the transfer or not at all');
419
+ const upd = find(pool.queries, /UPDATE goals SET created_by = \$2/);
420
+ assert.deepEqual(upd.params, [7, 5, 99], 'goal, new owner, actor');
421
+ const sql = String(upd.sql).replace(/\s+/g, ' ');
422
+ assert.match(sql, /UPDATE goal_membership_requests SET status = 'cancelled', resolved_by = \$3/);
423
+ assert.match(sql, /WHERE goal_id = \$1 AND builder_id = \$2 AND status = 'pending'/,
424
+ 'scoped to this goal + new owner, pending rows only');
425
+ assert.doesNotMatch(sql, /status = 'accepted'/);
426
+ assert.match(sql, /AND EXISTS \(SELECT 1 FROM g\)/, 'an unknown goal (no g row) must cancel nothing');
427
+ });
428
+
429
+ await test('routes: POST /goals/:id/transfer passes resolvedBy to transferGoalOwnership', async () => {
430
+ const { readFileSync } = await import('node:fs');
431
+ const src = readFileSync(new URL('../modules/lifecycle/routes/goals.js', import.meta.url), 'utf8');
432
+ assert.match(src, /db\.transferGoalOwnership\(\{[^}]*resolvedBy:\s*req\.builder\.id/);
433
+ });
434
+
412
435
  await test('transferGoalOwnership: unknown goal → null', async () => {
413
436
  const pool = fakePool(() => ({ rows: [] }));
414
437
  assert.equal(await transferGoalOwnership({ goalId: 999, newOwnerId: 5 }, { pool }), null);
@@ -50,12 +50,15 @@ async function t(name, fn) {
50
50
  // asserts the params the real query actually bound.
51
51
  function mockPool(answer = () => ({ rows: [] })) {
52
52
  const calls = [];
53
+ const query = async (text, params = []) => {
54
+ calls.push({ text: String(text), params });
55
+ return answer(String(text), params) || { rows: [] };
56
+ };
53
57
  return {
54
58
  calls,
55
- query: async (text, params = []) => {
56
- calls.push({ text: String(text), params });
57
- return answer(String(text), params) || { rows: [] };
58
- },
59
+ query,
60
+ // upsertActivity runs its upsert + snapshot as one transaction on a pooled client.
61
+ connect: async () => ({ query, release: () => {} }),
59
62
  find: (re) => calls.find((c) => re.test(c.text)),
60
63
  };
61
64
  }
@@ -73,6 +73,7 @@ require.cache[moduleApiPath] = {
73
73
  id: moduleApiPath, filename: moduleApiPath, loaded: true,
74
74
  exports: {
75
75
  pool,
76
+ withTx: (fn) => fn(pool), // transactions are not under test; keep the queue positional
76
77
  validateOrRespond: helpers.validateOrRespond,
77
78
  resolveSession: async () => null, // the browser leg is not under test
78
79
  logger: () => noopLog,