@bongos/core 1.19.1025 → 1.19.1026

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.
@@ -0,0 +1,235 @@
1
+ // modules/platform-identity/community-leaderboard.js — the PUBLIC windowed
2
+ // leaderboard (D3, docs/specs/bongos-v1-community-tab.md; task 1002978, goal
3
+ // 1000110 CT2): "who earned the most across every project, this week / month /
4
+ // year / ever", optionally narrowed to one discipline. The read behind
5
+ // GET /community/leaderboard, for the hub's /community page (task 1003363).
6
+ //
7
+ // WHO: rollupVisibleSql — active + public + NOT hide_stats, the privacy matrix's
8
+ // NUMBERS predicate, imported from account-visibility.js and never restated. A
9
+ // leaderboard row IS stats, so hide_stats excludes; private and provisional
10
+ // accounts never reach it. Deliberately NOT scoutingListableSql: the recruiting
11
+ // opt-out governs the recruiting directory only (spec D3). Viewer-independent —
12
+ // nothing here takes a viewer, so every caller sees the same board.
13
+ //
14
+ // WHAT (spec D2): all-time = the live rollup rows summed across projects. A
15
+ // window = per (project, builder), the live row minus the last snapshot
16
+ // at-or-before the window start (activity-snapshots.js), summed across projects;
17
+ // no such snapshot counts as zero. So until a builder's first changed report
18
+ // after snapshots began, their whole lifetime total counts inside every window —
19
+ // the spec's accepted rollout behaviour, self-correcting. Deltas attribute to
20
+ // REPORTING time, not earning time (the spec's stated V1 limitation).
21
+ //
22
+ // TWO HALVES, because the board is public and unauthenticated. The expensive
23
+ // half — every builder's window totals, one snapshot probe per activity row — is
24
+ // memoized per window for TOTALS_TTL_MS, so traffic cannot multiply it. The cheap
25
+ // half — who is eligible, and their name and avatar — is re-read on EVERY
26
+ // request, so a builder who hides or goes private leaves the board on the very
27
+ // next request (hide must beat the cache, ADR 0171 D5). The memo can only lag on
28
+ // numbers, never on who is shown.
29
+ //
30
+ // A builder with nothing to show in the window (credits, ships and karma all
31
+ // unchanged) is left off that window's board rather than listed at zero.
32
+ // github_id never crosses the projection; handle is the address.
33
+ 'use strict';
34
+
35
+ const { pool: defaultPool } = require('../../src/module-api');
36
+ const { rollupVisibleSql } = require('./account-visibility');
37
+ // The sibling public community read's paging + input caps, shared rather than
38
+ // copied so the two routes cannot drift apart.
39
+ const { clampOffset, MAX_OFFSET, MAX_DISCIPLINE_CHARS } = require('./builder-search');
40
+
41
+ // The spec's four windows. null = all-time (no baseline subtracted).
42
+ const WINDOW_DAYS = Object.freeze({ '7d': 7, '30d': 30, '365d': 365, all: null });
43
+ const WINDOWS = Object.freeze(Object.keys(WINDOW_DAYS));
44
+ const DEFAULT_WINDOW = 'all';
45
+ const DAY_MS = 24 * 60 * 60 * 1000;
46
+
47
+ const DEFAULT_LIMIT = 25;
48
+ const MAX_LIMIT = 100;
49
+ // How long one window's totals are reused. Numbers are reporting-time already
50
+ // (they move when an instance reports), so half a minute of lag is invisible;
51
+ // eligibility is never memoized.
52
+ const TOTALS_TTL_MS = 30 * 1000;
53
+
54
+ function isWindow(value) {
55
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(WINDOW_DAYS, value);
56
+ }
57
+
58
+ // The instant a window opens, or null for all-time. `now` is injectable so a
59
+ // test can pin the boundary.
60
+ function windowStart(window, now = new Date()) {
61
+ const days = WINDOW_DAYS[window];
62
+ if (days == null) return null;
63
+ return new Date(now.getTime() - days * DAY_MS);
64
+ }
65
+
66
+ function clampLimit(limit) {
67
+ const n = parseInt(limit, 10);
68
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
69
+ return Math.min(n, MAX_LIMIT);
70
+ }
71
+
72
+ // Every eligible builder's totals for one window, unsorted and unpaged:
73
+ // [{ githubId, credits, tasks_shipped, karma, disciplines }]. The expensive
74
+ // half. Arithmetic is bigint: the columns are instance-reported int4, and one
75
+ // extreme report must not overflow the whole public board into a 500.
76
+ async function computeWindowTotals(since, pool) {
77
+ const { rows } = await pool.query(
78
+ `WITH act AS (
79
+ SELECT a.client_id, a.github_id, a.credits, a.tasks_shipped, a.karma, a.disciplines
80
+ FROM platform_identity_builder_project_activity a
81
+ JOIN platform_identity_accounts pa ON pa.github_id = a.github_id
82
+ WHERE ${rollupVisibleSql('pa')}
83
+ ), per_project AS (
84
+ -- The baseline probe walks platform_identity_activity_snapshots_builder_time
85
+ -- in index order. All-time binds $1 NULL, so the probe never runs.
86
+ SELECT act.github_id,
87
+ act.credits::bigint - COALESCE(b.credits, 0) AS credits,
88
+ act.tasks_shipped::bigint - COALESCE(b.tasks_shipped, 0) AS tasks_shipped,
89
+ act.karma::bigint - COALESCE(b.karma, 0) AS karma
90
+ FROM act
91
+ LEFT JOIN LATERAL (
92
+ SELECT s.credits, s.tasks_shipped, s.karma
93
+ FROM platform_identity_activity_snapshots s
94
+ WHERE $1::timestamptz IS NOT NULL
95
+ AND s.client_id = act.client_id AND s.github_id = act.github_id
96
+ AND s.reported_at <= $1::timestamptz
97
+ ORDER BY s.reported_at DESC, s.id DESC
98
+ LIMIT 1
99
+ ) b ON true
100
+ ), board AS (
101
+ SELECT github_id,
102
+ sum(credits)::bigint AS credits,
103
+ sum(tasks_shipped)::bigint AS tasks_shipped,
104
+ sum(karma)::bigint AS karma
105
+ FROM per_project
106
+ GROUP BY github_id
107
+ ), disc AS (
108
+ SELECT x.github_id, array_agg(DISTINCT x.d ORDER BY x.d) AS disciplines
109
+ FROM (SELECT act.github_id, unnest(act.disciplines) AS d FROM act) x
110
+ WHERE x.d IS NOT NULL
111
+ GROUP BY x.github_id
112
+ )
113
+ SELECT board.github_id, board.credits, board.tasks_shipped, board.karma, disc.disciplines
114
+ FROM board
115
+ LEFT JOIN disc ON disc.github_id = board.github_id
116
+ WHERE board.credits <> 0 OR board.tasks_shipped <> 0 OR board.karma <> 0`,
117
+ [since],
118
+ );
119
+ // int8 arrives from node-postgres as a string; any real total is far inside
120
+ // Number's exact-integer range.
121
+ return rows.map((r) => ({
122
+ githubId: String(r.github_id),
123
+ credits: Number(r.credits),
124
+ tasks_shipped: Number(r.tasks_shipped),
125
+ karma: Number(r.karma),
126
+ disciplines: Array.isArray(r.disciplines) ? r.disciplines : [],
127
+ }));
128
+ }
129
+
130
+ // window -> { expires, since, promise }. Concurrent requests share one
131
+ // in-flight computation; a failed one is dropped so the next request retries.
132
+ const totalsMemo = new Map();
133
+
134
+ function memoizedWindowTotals(window, pool) {
135
+ const hit = totalsMemo.get(window);
136
+ if (hit && hit.pool === pool && hit.expires > Date.now()) return hit;
137
+ const since = windowStart(window);
138
+ const entry = { pool, since, expires: Date.now() + TOTALS_TTL_MS, promise: computeWindowTotals(since, pool) };
139
+ entry.promise.catch(() => { if (totalsMemo.get(window) === entry) totalsMemo.delete(window); });
140
+ totalsMemo.set(window, entry);
141
+ return entry;
142
+ }
143
+
144
+ function clearTotalsMemo() {
145
+ totalsMemo.clear();
146
+ }
147
+
148
+ // The cheap half, re-read on every request: which of these builders are
149
+ // eligible RIGHT NOW, and how they are shown. Map githubId -> identity.
150
+ async function eligibleIdentities(githubIds, pool) {
151
+ if (githubIds.length === 0) return new Map();
152
+ const { rows } = await pool.query(
153
+ `SELECT pa.github_id, pa.handle, pa.github_login, pa.display_name, pa.avatar_url
154
+ FROM platform_identity_accounts pa
155
+ WHERE pa.github_id = ANY($1::bigint[])
156
+ AND ${rollupVisibleSql('pa')}`,
157
+ [githubIds],
158
+ );
159
+ return new Map(rows.map((r) => [String(r.github_id), r]));
160
+ }
161
+
162
+ // Credits, then ships, then karma, then handle/login so a tie is stable —
163
+ // compared by code unit, so the order never depends on the server's locale.
164
+ const byText = (x, y) => { const a = String(x ?? ''); const b = String(y ?? ''); return a < b ? -1 : a > b ? 1 : 0; };
165
+ function compareRows(a, b) {
166
+ return (b.credits - a.credits) || (b.tasks_shipped - a.tasks_shipped) || (b.karma - a.karma)
167
+ || byText(a.handle, b.handle) || byText(a.github_login, b.github_login);
168
+ }
169
+
170
+ // listLeaderboard — one page of the board. Throws on an unknown window (the
171
+ // route validates and 400s first). Returns { window, since, results, limit,
172
+ // offset }: `since` is the window start the totals were computed from (null
173
+ // for all-time) and limit/offset are the CLAMPED values, so the route echoes
174
+ // rather than re-derives them. `place` is the 1-based position on the whole
175
+ // board — named so it can never be read as a builder's rank. Passing `now`
176
+ // pins the window start and bypasses the memo (tests of the boundary).
177
+ async function listLeaderboard({ window = DEFAULT_WINDOW, discipline, limit, offset, now, pool = defaultPool } = {}) {
178
+ if (!isWindow(window)) throw new Error(`listLeaderboard: unknown window ${JSON.stringify(window)}`);
179
+ const lim = clampLimit(limit);
180
+ const off = clampOffset(offset);
181
+ const disc = typeof discipline === 'string' ? discipline.trim().slice(0, MAX_DISCIPLINE_CHARS) : '';
182
+
183
+ let since;
184
+ let totals;
185
+ if (now !== undefined) {
186
+ since = windowStart(window, now);
187
+ totals = await computeWindowTotals(since, pool);
188
+ } else {
189
+ const entry = memoizedWindowTotals(window, pool);
190
+ since = entry.since;
191
+ totals = await entry.promise;
192
+ }
193
+
194
+ const candidates = disc ? totals.filter((t) => t.disciplines.includes(disc)) : totals;
195
+ const identities = await eligibleIdentities(candidates.map((t) => t.githubId), pool);
196
+ const board = candidates
197
+ .filter((t) => identities.has(t.githubId))
198
+ .map((t) => {
199
+ const who = identities.get(t.githubId);
200
+ return {
201
+ handle: who.handle,
202
+ github_login: who.github_login,
203
+ display_name: who.display_name,
204
+ avatar_url: who.avatar_url,
205
+ credits: t.credits,
206
+ tasks_shipped: t.tasks_shipped,
207
+ karma: t.karma,
208
+ disciplines: t.disciplines,
209
+ };
210
+ })
211
+ .sort(compareRows);
212
+
213
+ return {
214
+ window,
215
+ since: since ? since.toISOString() : null,
216
+ results: board.slice(off, off + lim).map((row, i) => ({ place: off + i + 1, ...row })),
217
+ limit: lim,
218
+ offset: off,
219
+ };
220
+ }
221
+
222
+ module.exports = {
223
+ listLeaderboard,
224
+ clearTotalsMemo,
225
+ windowStart,
226
+ isWindow,
227
+ clampLimit,
228
+ WINDOWS,
229
+ DEFAULT_WINDOW,
230
+ DEFAULT_LIMIT,
231
+ MAX_LIMIT,
232
+ MAX_OFFSET,
233
+ MAX_DISCIPLINE_CHARS,
234
+ TOTALS_TTL_MS,
235
+ };
@@ -10,7 +10,7 @@
10
10
  "maintainer": "Cloud Bongos maintainers",
11
11
  "maintenance": { "status": "core-maintained" },
12
12
  "contributes": {
13
- "routes": ["sso", "projects", "my-projects", "scouting", "profile", "public-profile", "connections", "community-search"],
13
+ "routes": ["sso", "projects", "my-projects", "scouting", "profile", "public-profile", "connections", "community-search", "community-leaderboard"],
14
14
  "migrations": true,
15
15
  "pollers": ["code-sweep", "visibility-pull"]
16
16
  },
@@ -0,0 +1,61 @@
1
+ // modules/platform-identity/read-rate-limit.js — a per-IP read cap for an
2
+ // UNAUTHENTICATED public GET in this module. The global limiter meters writes
3
+ // only, and core's per-IP sliding window (src/bongos/middleware/rate-limit.js
4
+ // perIpSlidingWindow) reaches modules only as ready-built instances on the
5
+ // doorway (ADR 0209: an instance per budget), none of which is this module's.
6
+ // This is the module-side twin of that function — same 429 envelope and
7
+ // headers — written once (task 1002978) and mounted by both community reads
8
+ // (routes/community-leaderboard.js, routes/community-search.js).
9
+ // routes/public-profile.js and routes/sso.js still carry older vendored copies.
10
+ 'use strict';
11
+
12
+ const SWEEP_EVERY_MS = 5 * 60 * 1000;
13
+ // Bounds the bucket map between sweeps: rotating source addresses (an IPv6
14
+ // block) must not grow the heap without limit on an unauthenticated route.
15
+ const MAX_TRACKED_IPS = 10000;
16
+
17
+ // createReadRateLimit({ scope, limit, windowMs, maxTrackedIps }) → Express
18
+ // middleware allowing `limit` requests per IP per sliding `windowMs`, then 429
19
+ // rate_limited with Retry-After. `scope` names the budget in the response so a
20
+ // client can tell which cap it hit. Buckets live in this closure: one budget
21
+ // per call. The map is kept in least-recently-seen order, so at the tracking
22
+ // cap the IP evicted is the one idle longest — O(1), no scan on the hot path.
23
+ function createReadRateLimit({ scope, limit, windowMs = 60 * 1000, maxTrackedIps = MAX_TRACKED_IPS }) {
24
+ const buckets = new Map(); // ip -> timestamp[], least recently seen first
25
+
26
+ const sweepTimer = setInterval(() => {
27
+ const cutoff = Date.now() - windowMs;
28
+ for (const [k, arr] of buckets) {
29
+ while (arr.length && arr[0] < cutoff) arr.shift();
30
+ if (arr.length === 0) buckets.delete(k);
31
+ }
32
+ }, SWEEP_EVERY_MS);
33
+ if (typeof sweepTimer.unref === 'function') sweepTimer.unref();
34
+
35
+ return function readRateLimit(req, res, next) {
36
+ const now = Date.now();
37
+ const key = req.ip || req.socket?.remoteAddress || 'unknown';
38
+ let bucket = buckets.get(key);
39
+ if (bucket) {
40
+ buckets.delete(key); // re-inserted below: most recently seen goes last
41
+ } else {
42
+ if (buckets.size >= maxTrackedIps) buckets.delete(buckets.keys().next().value);
43
+ bucket = [];
44
+ }
45
+ buckets.set(key, bucket);
46
+ const cutoff = now - windowMs;
47
+ while (bucket.length && bucket[0] < cutoff) bucket.shift();
48
+ if (bucket.length >= limit) {
49
+ const retryAfter = Math.max(1, Math.ceil((bucket[0] + windowMs - now) / 1000));
50
+ res.set('Retry-After', String(retryAfter));
51
+ res.set('X-RateLimit-Scope', scope);
52
+ res.set('X-RateLimit-Limit', String(limit));
53
+ res.set('X-RateLimit-Window-Seconds', String(windowMs / 1000));
54
+ return res.fail('rate_limited', 429, { scope, retry_after_seconds: retryAfter });
55
+ }
56
+ bucket.push(now);
57
+ return next();
58
+ };
59
+ }
60
+
61
+ module.exports = { createReadRateLimit, MAX_TRACKED_IPS };
@@ -0,0 +1,69 @@
1
+ // modules/platform-identity/routes/community-leaderboard.js — GET
2
+ // /community/leaderboard, the PUBLIC windowed leaderboard behind the hub's
3
+ // /community page (D3, docs/specs/bongos-v1-community-tab.md; task 1002978,
4
+ // goal 1000110 CT2). The sibling of routes/community-search.js: same module,
5
+ // same public posture. Who appears and how a window is counted live entirely in
6
+ // ../community-leaderboard.js; this file is the HTTP shell — validation, the
7
+ // per-IP read cap, the response envelope. It never reads a session, so the
8
+ // board is the same for every viewer.
9
+ //
10
+ // Reaches core only through the doorway (ADR 0083).
11
+ 'use strict';
12
+
13
+ const express = require('express');
14
+ const api = require('../../../src/module-api');
15
+ const leaderboard = require('../community-leaderboard');
16
+ const { createReadRateLimit } = require('../read-rate-limit');
17
+
18
+ const { pool, corsPublicGet } = api;
19
+
20
+ const READ_RATE_LIMIT = 60; // per IP per minute — the community-search budget
21
+ const READ_RATE_SCOPE = 'community-leaderboard-read';
22
+ const leaderboardReadRateLimit = createReadRateLimit({ scope: READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
23
+
24
+ module.exports = function communityLeaderboardRoutes() {
25
+ const router = express.Router();
26
+
27
+ router.use('/community/leaderboard', corsPublicGet('/community/leaderboard'));
28
+
29
+ // rank: public — the cross-project leaderboard (?window=7d|30d|365d|all,
30
+ // default all; ?discipline=; ?limit=&offset=). Eligibility is the privacy
31
+ // matrix's numbers predicate (active + public + stats shown); the recruiting
32
+ // opt-out does not apply. no-store: hide must beat the cache (ADR 0171 D5).
33
+ // Rollout note: until a builder's first changed report after snapshot history
34
+ // began, their whole lifetime total counts inside every window (spec D2's
35
+ // missing-baseline-is-zero rule) — expected on day one, and self-correcting.
36
+ router.get('/community/leaderboard', leaderboardReadRateLimit, async (req, res) => {
37
+ const window = req.query.window === undefined || req.query.window === ''
38
+ ? leaderboard.DEFAULT_WINDOW
39
+ : req.query.window;
40
+ if (!leaderboard.isWindow(window)) {
41
+ return res.fail('bad_window', { status: 400, message: `window must be one of: ${leaderboard.WINDOWS.join(', ')}` });
42
+ }
43
+ const discipline = (typeof req.query.discipline === 'string' ? req.query.discipline : '').trim();
44
+ if (discipline.length > leaderboard.MAX_DISCIPLINE_CHARS) {
45
+ return res.fail('discipline_too_long', { status: 400, message: `discipline must be at most ${leaderboard.MAX_DISCIPLINE_CHARS} characters` });
46
+ }
47
+ try {
48
+ const board = await leaderboard.listLeaderboard({
49
+ window, discipline, limit: req.query.limit, offset: req.query.offset, pool,
50
+ });
51
+ res.set('Cache-Control', 'no-store');
52
+ res.json({
53
+ window: board.window,
54
+ since: board.since,
55
+ discipline: discipline || null,
56
+ count: board.results.length,
57
+ results: board.results,
58
+ page: { limit: board.limit, offset: board.offset },
59
+ });
60
+ } catch (err) {
61
+ api.logger('platform-identity').error({ err: err && err.message }, 'GET /community/leaderboard failed');
62
+ res.fail('leaderboard_failed', { status: 500, message: 'internal error' });
63
+ }
64
+ });
65
+
66
+ return router;
67
+ };
68
+
69
+ module.exports._internals = { READ_RATE_LIMIT, READ_RATE_SCOPE };
@@ -16,6 +16,7 @@
16
16
  const express = require('express');
17
17
  const api = require('../../../src/module-api');
18
18
  const builderSearch = require('../builder-search');
19
+ const { createReadRateLimit } = require('../read-rate-limit');
19
20
 
20
21
  const { pool, corsPublicGet } = api;
21
22
 
@@ -33,39 +34,11 @@ const log = {
33
34
 
34
35
  // Per-IP read cap for this UNAUTHENTICATED route (the public-profile.js R05
35
36
  // posture: the global limiter only meters WRITES, so an uncapped scraper could
36
- // otherwise run the trigram leg at line rate). Vendored, not shared — the core
37
- // /public/* limiter is not on the doorway, and each public GET here owns its
38
- // own budget rather than reaching into a sibling module's copy.
37
+ // otherwise run the trigram leg at line rate). Its own budget, built by the
38
+ // module's shared factory (task 1002978) rather than a vendored copy.
39
39
  const READ_RATE_WINDOW_MS = 60 * 1000;
40
40
  const READ_RATE_LIMIT = 60;
41
- const buckets = new Map(); // ip -> timestamp[]
42
-
43
- const sweepTimer = setInterval(() => {
44
- const cutoff = Date.now() - READ_RATE_WINDOW_MS;
45
- for (const [k, arr] of buckets) {
46
- while (arr.length && arr[0] < cutoff) arr.shift();
47
- if (arr.length === 0) buckets.delete(k);
48
- }
49
- }, 5 * 60 * 1000);
50
- if (typeof sweepTimer.unref === 'function') sweepTimer.unref();
51
-
52
- function searchReadRateLimit(req, res, next) {
53
- const now = Date.now();
54
- const key = req.ip || req.socket?.remoteAddress || 'unknown';
55
- let bucket = buckets.get(key);
56
- if (!bucket) { bucket = []; buckets.set(key, bucket); }
57
- const cutoff = now - READ_RATE_WINDOW_MS;
58
- while (bucket.length && bucket[0] < cutoff) bucket.shift();
59
- if (bucket.length >= READ_RATE_LIMIT) {
60
- const retryAfter = Math.max(1, Math.ceil((bucket[0] + READ_RATE_WINDOW_MS - now) / 1000));
61
- res.set('Retry-After', String(retryAfter));
62
- res.set('X-RateLimit-Scope', 'community-search-read');
63
- res.set('X-RateLimit-Limit', String(READ_RATE_LIMIT));
64
- return res.fail('rate_limited', 429, { scope: 'community-search-read', retry_after_seconds: retryAfter });
65
- }
66
- bucket.push(now);
67
- return next();
68
- }
41
+ const searchReadRateLimit = createReadRateLimit({ scope: 'community-search-read', limit: READ_RATE_LIMIT, windowMs: READ_RATE_WINDOW_MS });
69
42
 
70
43
  module.exports = function communitySearchRoutes() {
71
44
  const router = express.Router();
@@ -120,7 +120,7 @@ module.exports = function projectsRoutes() {
120
120
  // as any-builder — wrong in the dangerous direction (the ADR 0210 rule).
121
121
 
122
122
  // rank: metic+archon — own-or-metic in effect: invite a builder into a project
123
- // (ADR 0141 §5, task #1002115), called by a `project.curate` holder OR the project's OWN
123
+ // (ADR 0141 §5, task 1002115), called by a `project.curate` holder OR the project's OWN
124
124
  // OWNER (task 1003580; the ownership proof is `provisioning_instances.owner_builder_id`,
125
125
  // reached through a port — see invite-authz.js). The wizard's audience is any signed-in
126
126
  // GitHub user, typically a xenos who used to get 403 on their own project. A caller who is
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1025",
3
+ "version": "1.19.1026",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1025",
9
+ "version": "1.19.1026",
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.1025",
3
+ "version": "1.19.1026",
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",
@@ -87,6 +87,11 @@ const INTEGRATION = new Set([
87
87
  // WHERE NOT EXISTS statement) and the upsertActivity wiring against real SQL —
88
88
  // a fake pool can prove the query shape but not which reports it inserts.
89
89
  'activity_snapshots_db',
90
+ // task 1002978: the windowed leaderboard against real SQL. Which snapshot
91
+ // becomes a window baseline (a LATERAL at-or-before probe), a missing baseline
92
+ // counting as zero, and the privacy matrix filtering are all planner facts the
93
+ // DB-free route test can only assert the shape of.
94
+ 'community_leaderboard_db',
90
95
  'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
91
96
  'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
92
97
  // 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.1025'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1026'; // 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');