@bongos/core 1.19.411 → 1.19.412

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.411",
6
- "core_contract": "1.19.411",
7
- "source_commit": "82d0745903a411db2837e92a8f8fac0a25024fbd",
5
+ "core_version": "1.19.412",
6
+ "core_contract": "1.19.412",
7
+ "source_commit": "049b8f5c6bb893589c47e9e6e3855fc3da1bec06",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-28T22:50:38.276Z",
9
+ "built_at": "2026-08-29T15:07:18.728Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 354,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2044,
20
- "tree_sha256": "60b3e50594b84fae8251421e6d1632b4597d941a99cba38344c43aeb3e4e3fb1",
20
+ "tree_sha256": "d71641215ed53df26d8573408c98ba6afad6b1a95232f3dc1b8055688a98eb08",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1867,7 +1867,7 @@
1867
1867
  {
1868
1868
  "path": "docs/module-api-changelog.md",
1869
1869
  "mode": "0000644",
1870
- "sha256": "2da385cc79958922b0714f47a7f2c58535cd0e9db516924a6bb61f8ce01e1142"
1870
+ "sha256": "251455d2e8adfcad824b3ce55ba08418109cfb6e386990f3d9acfb551a992884"
1871
1871
  },
1872
1872
  {
1873
1873
  "path": "docs/modules-contract.md",
@@ -5367,12 +5367,12 @@
5367
5367
  {
5368
5368
  "path": "package-lock.json",
5369
5369
  "mode": "0000644",
5370
- "sha256": "e1ac444053590084d84aa27194b817d8e7f649ef172b63a2b0d6a4a5b51b83d0"
5370
+ "sha256": "07258342132575cf206c98c5c145423c12a5b16f96c3ecf9c8a5c4c4265bcafa"
5371
5371
  },
5372
5372
  {
5373
5373
  "path": "package.json",
5374
5374
  "mode": "0000644",
5375
- "sha256": "bfea05054d8eb816a33710f43d4b078238ef470615c5768645ee0d30aa547a0c"
5375
+ "sha256": "79513e9d85183a56152ef6bb5a75f61548efc663860f89c1b55df41d3ced73b2"
5376
5376
  },
5377
5377
  {
5378
5378
  "path": "public-docs/index.html",
@@ -7387,7 +7387,7 @@
7387
7387
  {
7388
7388
  "path": "src/module-api.js",
7389
7389
  "mode": "0000644",
7390
- "sha256": "06547b5cc63cc9ed1f50e49738d437beb1ffa8bf2ca81e396a4a0a795864a7f1"
7390
+ "sha256": "b182b43409b6311ab968db425b6064378cc2842d9abc1f4628f18a3ea7cde9dc"
7391
7391
  },
7392
7392
  {
7393
7393
  "path": "src/module-loader/catalog.js",
@@ -9382,7 +9382,7 @@
9382
9382
  {
9383
9383
  "path": "tests/profile_route.mjs",
9384
9384
  "mode": "0000644",
9385
- "sha256": "3b3045a6143d67c963516ed5703b5dabb9b8945a392118721e6a0a56258f1732"
9385
+ "sha256": "5af720ee7c18d4b47630fbbed7cb08901acfbf773960699e312496b8196c9fdc"
9386
9386
  },
9387
9387
  {
9388
9388
  "path": "tests/profile_ui_cross_project.mjs",
@@ -1259,5 +1259,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1259
1259
  landed since 1.19.409 with no explicit bump. run 33217599338. (task 1002620)
1260
1260
  1.19.411 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1261
1261
  landed since 1.19.410 with no explicit bump. run 33218290583. (task 1002620)
1262
+ 1.19.412 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1263
+ landed since 1.19.411 with no explicit bump. run 33259357678. (task 1002620)
1262
1264
  ---------------------------------------------------------------------------
1263
1265
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.411",
3
+ "version": "1.19.412",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.411",
9
+ "version": "1.19.412",
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.411",
3
+ "version": "1.19.412",
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
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.411'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.412'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -11,14 +11,38 @@
11
11
  // Stub auth + db + the lifecycle/reward ports (no network, no Postgres) exactly
12
12
  // as tests/onboarding_route_signals.mjs does for the sibling public route.
13
13
  //
14
+ // R06 (task 1002282) extends it with the consent PROOF — the five claims goal
15
+ // 1000045's C2 rests on, all DB-free so the `unit` gate actually runs them:
16
+ // 1. a two-project builder's COMBINED record is served to another account
17
+ // (the route is unauthenticated and viewer-independent, ADR 0171 D4);
18
+ // 2. flipping hide_stats removes it on the IMMEDIATE next load, and the
19
+ // response is `Cache-Control: no-store` — the old `public, max-age=60`
20
+ // would keep serving the pre-hide body for a minute (ADR 0171 D5);
21
+ // 3. `/scouting` (the endpoint the hall's /people page renders) drops the
22
+ // hidden builder too — enforcement is on every read path, not the profile;
23
+ // 4. the leak guard BITES: planting a private field turns the key-list and
24
+ // sentinel assertions red (a leak test that cannot fail is not a test);
25
+ // 5. no PARTIAL numbers survive a hide — a count beside a hidden total is a
26
+ // leak by subtraction.
27
+ //
28
+ // RESIDUAL, stated honestly (goal 1000045 C2): hide removes the hub's
29
+ // AGGREGATED view, not the per-instance public numbers it is derived from.
30
+ // Every instance's own public surfaces (its /public/leaderboard) stay public by
31
+ // design, so a determined reader can still reconstruct a sum the hard way.
32
+ //
14
33
  // Run: node tests/profile_route.mjs
15
34
 
16
35
  import { strict as assert } from 'node:assert';
17
36
  import { createRequire } from 'node:module';
18
37
  import http from 'node:http';
38
+ import { readFileSync } from 'node:fs';
39
+ import { fileURLToPath } from 'node:url';
40
+ import { dirname, join } from 'node:path';
19
41
 
20
42
  const require = createRequire(import.meta.url);
21
43
  const express = require('express');
44
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
45
+ const sourceOf = (rel) => readFileSync(join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
22
46
 
23
47
  let passed = 0;
24
48
  let failed = 0;
@@ -90,7 +114,7 @@ const CROSS_PROJECT_ROWS = [
90
114
  // shape — named projects only where the catalog row is public; the rest counted.
91
115
  function stubPublicProfileExtras() {
92
116
  const named = CROSS_PROJECT_ROWS.filter((r) => r.is_public)
93
- .map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held }));
117
+ .map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held, recent_ships: [] }));
94
118
  return {
95
119
  totals: { projects: 2, credits: 150, works_shipped: 7, karma: 4 },
96
120
  disciplines: ['engineer', 'ui'],
@@ -101,7 +125,28 @@ function stubPublicProfileExtras() {
101
125
  }
102
126
  // The key contract for the cross_project object + its project rows.
103
127
  const CP_PUBLIC_KEYS = ['totals', 'disciplines', 'last_active', 'projects', 'more_projects'];
104
- const CP_PROJECT_ROW_KEYS = ['name', 'origin', 'rank_held'];
128
+ // recent_ships rides the project row (task 1002803) and is ALWAYS present —
129
+ // getRollupForBuilder defaults it to [] — so it belongs in the exact key list.
130
+ const CP_PROJECT_ROW_KEYS = ['name', 'origin', 'rank_held', 'recent_ships'];
131
+ // Every module-internal value that must never reach a public body.
132
+ const CP_SENTINELS = [SECRET_CLIENT_ID, SECRET_PROJECT_NAME, SECRET_EMAIL, SECRET_USER, SECRET_REASON];
133
+
134
+ // The cross_project whitelist contract, in ONE place — the positive tests and
135
+ // the R06 negative control (task 1002282) run the SAME guard, which is what
136
+ // makes "planting a private field turns it red" a proof rather than a claim.
137
+ // Asserts the exact key list on the object and on every project row, and that
138
+ // no sentinel (nor the literal `client_id` key) survives in the raw body.
139
+ function assertCrossProjectContract(cp, text) {
140
+ assert.deepEqual(Object.keys(cp).sort(), [...CP_PUBLIC_KEYS].sort(), 'cross_project keys are exactly the contract');
141
+ assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
142
+ for (const row of cp.projects) {
143
+ assert.deepEqual(Object.keys(row).sort(), [...CP_PROJECT_ROW_KEYS].sort(), 'project rows carry only the whitelisted keys');
144
+ }
145
+ for (const secret of CP_SENTINELS) {
146
+ assert.ok(!text.includes(secret), `response body must not contain "${secret}"`);
147
+ }
148
+ assert.ok(!text.includes('client_id'), 'no client_id key may cross the port');
149
+ }
105
150
 
106
151
  // ---- stubs ----------------------------------------------------------------
107
152
 
@@ -161,9 +206,9 @@ function listen(app) {
161
206
  });
162
207
  }
163
208
 
164
- function get(port, path) {
209
+ function get(port, path, headers) {
165
210
  return new Promise((resolve, reject) => {
166
- const req = http.request({ host: '127.0.0.1', port, method: 'GET', path }, (res) => {
211
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path, headers: headers || {} }, (res) => {
167
212
  const chunks = [];
168
213
  res.on('data', (c) => chunks.push(c));
169
214
  res.on('end', () => {
@@ -235,8 +280,12 @@ await t('hub port ABSENT → no cross_project key + the cacheable posture stays'
235
280
  });
236
281
 
237
282
  let hubExtras = stubPublicProfileExtras();
283
+ // The port's public read sits behind a swappable reference: a port is
284
+ // single-provider by design (registerProvider throws on a duplicate), and the
285
+ // R06 proofs below repoint this at the REAL module read over a fake engine.
286
+ let hubPublicRead = async () => hubExtras;
238
287
  seams.registerProvider('platform-identity.hub', {
239
- getPublicProfileExtras: async () => hubExtras,
288
+ getPublicProfileExtras: async (githubId) => hubPublicRead(githubId),
240
289
  getOwnProfileExtras: async () => stubPublicProfileExtras(),
241
290
  });
242
291
 
@@ -245,11 +294,7 @@ await t('hub port present → cross_project carries exactly the whitelist shape'
245
294
  assert.equal(r.status, 200);
246
295
  const cp = r.json.cross_project;
247
296
  assert.ok(cp, 'cross_project attached when the port resolves');
248
- assert.deepEqual(Object.keys(cp).sort(), [...CP_PUBLIC_KEYS].sort(), 'cross_project keys are exactly the contract');
249
- assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
250
- for (const row of cp.projects) {
251
- assert.deepEqual(Object.keys(row).sort(), [...CP_PROJECT_ROW_KEYS].sort(), 'project rows carry only name/origin/rank_held');
252
- }
297
+ assertCrossProjectContract(cp, r.text);
253
298
  assert.equal(cp.projects.length, 1, 'only the public catalog row is named');
254
299
  assert.equal(cp.more_projects, 1, 'the non-public project is counted, never named');
255
300
  });
@@ -277,6 +322,241 @@ await t('hub port returns null (hidden/provisional/none) → cross_project: null
277
322
  assert.equal(r.headers['cache-control'], 'no-store');
278
323
  });
279
324
 
325
+ // ---- R06 PROOF: hide is honored on every read path (task 1002282) ---------
326
+ //
327
+ // Deps R03 (task 1002279 — the hide_stats flag) and R04 (task 1002280 — the
328
+ // profile UI section) shipped; this pins their behaviour so it cannot rot. The
329
+ // proofs below run the REAL module reads (getPublicProfileExtras,
330
+ // setAccountHideStats, listScoutingRollup) and the REAL routes over a fake
331
+ // engine, so what is proven is the COMPOSITION — route → port → module read →
332
+ // query — not a stub of the answer.
333
+
334
+ // A deliberately literal-minded stand-in for Postgres. It holds the fixture
335
+ // rows and applies ONLY the consent predicates the query it is handed actually
336
+ // declares. That is what makes the scouting proof real rather than circular:
337
+ // delete `NOT pa.hide_stats` from listScoutingRollup and the hidden builder
338
+ // walks straight into the response, turning these tests red. The SQL semantics
339
+ // themselves (the un-fanned sums) are proven against a live Postgres by
340
+ // tests/profile_rollup_consent.mjs and tests/scouting_rollup_fanout.mjs.
341
+ const HUB_GITHUB_ID = FULL_ROW.github_id;
342
+ const HIDDEN_LOGIN = 'hidden-DO-NOT-LEAK-r06';
343
+ const PROV_LOGIN = 'provisional-DO-NOT-LEAK-r06';
344
+
345
+ // TWO projects — one publicly catalogued, one not. Distinctive digits so the
346
+ // "no partial numbers" scan below cannot pass by coincidence.
347
+ const PROOF_PROJECTS = [
348
+ { name: 'R06 Public Proj', origin: 'https://r06-pub.example', rank_held: 'metic',
349
+ credits: 60271, tasks_shipped: 4103, karma: 9021, is_public: true },
350
+ { name: SECRET_PROJECT_NAME, origin: 'https://r06-priv.example', rank_held: 'archon',
351
+ credits: 40129, tasks_shipped: 2011, karma: 3007, is_public: false },
352
+ ];
353
+ const sumOf = (k) => PROOF_PROJECTS.reduce((n, p) => n + p[k], 0);
354
+
355
+ // The one account row the reads consult. setAccountHideStats mutates it.
356
+ const hubAccount = { profile_state: 'active', hide_stats: false, handle: 'r06-handle' };
357
+
358
+ // The scouting "table": one visible builder plus the two that consent excludes.
359
+ const SCOUTING_TABLE = [
360
+ { profile_state: 'active', hide_stats: false,
361
+ row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
362
+ project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
363
+ last_active: '2026-08-01T00:00:00.000Z', disciplines: ['engineer'] } },
364
+ { profile_state: 'active', hide_stats: true,
365
+ row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
366
+ project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
367
+ last_active: '2026-08-02T00:00:00.000Z', disciplines: ['ui'] } },
368
+ { profile_state: 'provisional', hide_stats: false,
369
+ row: { github_id: '13', github_login: PROV_LOGIN, display_name: 'Provisional', avatar_url: null,
370
+ project_count: 1, total_credits: 71713, total_tasks_shipped: 1, total_karma: 0,
371
+ last_active: '2026-08-03T00:00:00.000Z', disciplines: [] } },
372
+ ];
373
+
374
+ const engine = {
375
+ calls: [],
376
+ query: async (text, params = []) => {
377
+ const sql = String(text);
378
+ engine.calls.push({ sql, params });
379
+
380
+ // listScoutingRollup — the ONE read behind GET /scouting and the hall's
381
+ // /people page. Honors exactly the predicates the query declares.
382
+ if (/AS project_count/.test(sql)) {
383
+ let rows = SCOUTING_TABLE;
384
+ if (/pa\.profile_state = 'active'/.test(sql)) rows = rows.filter((r) => r.profile_state === 'active');
385
+ if (/NOT pa\.hide_stats/.test(sql)) rows = rows.filter((r) => !r.hide_stats);
386
+ return { rows: rows.map((r) => ({ ...r.row })) };
387
+ }
388
+ // setAccountHideStats — the own-scoped write behind PATCH /me/cross-project.
389
+ if (/INSERT INTO platform_identity_accounts/.test(sql)) {
390
+ hubAccount.hide_stats = !!params[4];
391
+ return { rows: [{ github_id: String(HUB_GITHUB_ID), github_login: 'jaxri', ...hubAccount }] };
392
+ }
393
+ // getPublicProfileExtras' consent read.
394
+ if (/SELECT profile_state, hide_stats/.test(sql)) {
395
+ return { rows: [{ profile_state: hubAccount.profile_state, hide_stats: hubAccount.hide_stats }] };
396
+ }
397
+ // getRollupForBuilder's per-project rows (the client_id → origin two-hop).
398
+ if (/platform_identity_sso_clients/.test(sql)) {
399
+ return { rows: PROOF_PROJECTS.map((p) => ({ name: p.name, origin: p.origin, rank_held: p.rank_held, is_public: p.is_public, recent_ships: [] })) };
400
+ }
401
+ // getRollupForBuilder's un-fanned aggregate — summed ACROSS both projects.
402
+ if (/AS works_shipped/.test(sql)) {
403
+ return { rows: [{ projects: PROOF_PROJECTS.length, credits: sumOf('credits'),
404
+ works_shipped: sumOf('tasks_shipped'), karma: sumOf('karma'),
405
+ last_active: '2026-08-02T00:00:00.000Z', disciplines: ['engineer', 'ui'] }] };
406
+ }
407
+ throw new Error(`fake engine: unexpected query ${sql.slice(0, 60)}`);
408
+ },
409
+ };
410
+
411
+ // Pin the module doorway (ADR 0083) BEFORE platform-identity loads, so the
412
+ // module's default pool — destructured at require time — is the fake engine.
413
+ const moduleApiPath = require.resolve('../src/module-api.js');
414
+ require.cache[moduleApiPath] = {
415
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
416
+ exports: {
417
+ pool: engine,
418
+ requireBuilder: (_req, _res, next) => next(),
419
+ requirePermission: () => (_req, _res, next) => next(),
420
+ parsePagination: () => ({ limit: 100, offset: 0 }),
421
+ corsPublicGet: () => (_req, _res, next) => next(),
422
+ },
423
+ };
424
+ const pi = require('../modules/platform-identity/platform-identity.js');
425
+ const buildScoutingRoutes = require('../modules/platform-identity/routes/scouting.js');
426
+
427
+ // From here the port serves the REAL public read over the fake engine.
428
+ hubPublicRead = (githubId) => pi.getPublicProfileExtras(githubId, { pool: engine });
429
+
430
+ await t('R06.1 a two-project builder\'s COMBINED record shows to another account', async () => {
431
+ const r = await get(port, '/api/gds/builders/95/profile');
432
+ assert.equal(r.status, 200);
433
+ const cp = r.json.cross_project;
434
+ assert.ok(cp, 'the record is served');
435
+ assertCrossProjectContract(cp, r.text);
436
+ assert.equal(cp.totals.projects, 2, 'both projects count');
437
+ assert.equal(cp.totals.credits, sumOf('credits'), 'credits are the sum across BOTH projects');
438
+ assert.equal(cp.totals.works_shipped, sumOf('tasks_shipped'), 'ships are summed across both');
439
+ assert.equal(cp.totals.karma, sumOf('karma'), 'karma is summed across both');
440
+ assert.deepEqual(cp.disciplines, ['engineer', 'ui'], 'the disciplines union spans both projects');
441
+ assert.equal(cp.projects.length, 1, 'only the publicly-catalogued project is NAMED');
442
+ assert.equal(cp.projects[0].name, PROOF_PROJECTS[0].name);
443
+ assert.equal(cp.more_projects, 1, 'the other is counted, never named');
444
+ });
445
+
446
+ await t('R06.1 the record is viewer-INDEPENDENT — another account sees the same body', async () => {
447
+ const anon = await get(port, '/api/gds/builders/95/profile');
448
+ const other = await get(port, '/api/gds/builders/95/profile', { Cookie: 'gds_session=some-other-builder' });
449
+ assert.equal(other.status, 200);
450
+ assert.equal(other.text, anon.text, 'one response shape for every reader (ADR 0171 D4)');
451
+ });
452
+
453
+ await t('R06.1 the profile read takes NO auth middleware — the portfolio is shareable', () => {
454
+ // A future PR walling this behind requireBuilder would break "another account
455
+ // can see it" while every assertion above still passed, so pin the shape.
456
+ const line = sourceOf('src/bongos/routes/builders.js').split('\n')
457
+ .find((l) => l.includes("router.get('/builders/:id/profile'"));
458
+ assert.ok(line, 'the profile route registration is findable');
459
+ assert.match(line, /router\.get\('\/builders\/:id\/profile', async \(req, res\) => \{/,
460
+ 'nothing may sit between the path and the handler — that is what makes the record public');
461
+ });
462
+
463
+ await t('R06.4 NEGATIVE CONTROL: the leak guard goes red when a private field is planted', async () => {
464
+ const realRead = hubPublicRead;
465
+ const rollup = async () => pi.getPublicProfileExtras(HUB_GITHUB_ID, { pool: engine });
466
+ try {
467
+ // (a) a module-internal key on the object itself.
468
+ hubPublicRead = async () => ({ ...(await rollup()), client_id: SECRET_CLIENT_ID });
469
+ let r = await get(port, '/api/gds/builders/95/profile');
470
+ assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
471
+ /cross_project keys are exactly the contract/, 'an extra top-level key must turn the guard red');
472
+
473
+ // (b) a module-internal key on a project ROW (the is_public discriminator).
474
+ hubPublicRead = async () => {
475
+ const cp = await rollup();
476
+ return { ...cp, projects: cp.projects.map((p) => ({ ...p, is_public: true })) };
477
+ };
478
+ r = await get(port, '/api/gds/builders/95/profile');
479
+ assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
480
+ /project rows carry only the whitelisted keys/, 'an extra project-row key must turn the guard red');
481
+
482
+ // (c) a sentinel smuggled inside an ALLOWED field — key lists stay legal, so
483
+ // this isolates the raw-body scan. This is the leak the shape check cannot see.
484
+ hubPublicRead = async () => {
485
+ const cp = await rollup();
486
+ return { ...cp, projects: [{ ...cp.projects[0], name: SECRET_PROJECT_NAME }] };
487
+ };
488
+ r = await get(port, '/api/gds/builders/95/profile');
489
+ assert.deepEqual(Object.keys(r.json.cross_project).sort(), [...CP_PUBLIC_KEYS].sort(),
490
+ 'the planted leak is shape-legal — only the sentinel scan can catch it');
491
+ assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
492
+ /must not contain/, 'a sentinel in an allowed field must turn the guard red');
493
+ } finally {
494
+ hubPublicRead = realRead;
495
+ }
496
+ });
497
+
498
+ await t('R06.2 hide_stats removes the record on the IMMEDIATE next load', async () => {
499
+ const before = await get(port, '/api/gds/builders/95/profile');
500
+ assert.ok(before.json.cross_project, 'public before the flip');
501
+ assert.equal(before.headers['cache-control'], 'no-store');
502
+
503
+ // Flip it through the REAL write the own-scoped PATCH /me/cross-project uses.
504
+ const row = await pi.setAccountHideStats(
505
+ { githubId: HUB_GITHUB_ID, githubLogin: 'jaxri', hide: true }, { pool: engine });
506
+ assert.equal(row.hide_stats, true, 'the write path set the flag');
507
+
508
+ // No sleep, no second server, no cache warm-up: the very next load.
509
+ const after = await get(port, '/api/gds/builders/95/profile');
510
+ assert.equal(after.status, 200);
511
+ assert.equal(after.json.cross_project, null, 'gone on the next load');
512
+ assert.equal(after.headers['cache-control'], 'no-store', 'hide must beat the cache (ADR 0171 D5)');
513
+ assert.ok(!/max-age/.test(after.headers['cache-control'] || ''),
514
+ 'any max-age would let a cache keep serving the pre-hide body — the old public, max-age=60 failed exactly here');
515
+ });
516
+
517
+ await t('R06.5 no PARTIAL numbers survive the hide (a count beside a hidden total leaks by subtraction)', async () => {
518
+ // Set the flag here too, so this proof stands on its own rather than on
519
+ // whatever R06.2 left behind.
520
+ await pi.setAccountHideStats({ githubId: HUB_GITHUB_ID, githubLogin: 'jaxri', hide: true }, { pool: engine });
521
+ const r = await get(port, '/api/gds/builders/95/profile');
522
+ assert.equal(r.json.cross_project, null);
523
+ for (const key of ['totals', 'works_shipped', 'more_projects', 'last_active', 'rank_held']) {
524
+ assert.ok(!r.text.includes(key), `nothing derived from the rollup may survive — found "${key}"`);
525
+ }
526
+ const digits = [sumOf('credits'), sumOf('tasks_shipped'), sumOf('karma'),
527
+ ...PROOF_PROJECTS.map((p) => p.credits)].map(String);
528
+ for (const v of [...digits, ...PROOF_PROJECTS.map((p) => p.name), ...PROOF_PROJECTS.map((p) => p.origin)]) {
529
+ assert.ok(!r.text.includes(v), `no rollup value may survive a hide — found "${v}"`);
530
+ }
531
+ });
532
+
533
+ // ---- R06.3: the OTHER read paths honor hide too ---------------------------
534
+
535
+ const scoutApp = express();
536
+ scoutApp.use(attachFail);
537
+ scoutApp.use('/api/gds', buildScoutingRoutes());
538
+ const scout = await listen(scoutApp);
539
+
540
+ await t('R06.3 GET /scouting drops the hidden AND the provisional builder', async () => {
541
+ engine.calls.length = 0;
542
+ const r = await get(scout.port, '/api/gds/scouting');
543
+ assert.equal(r.status, 200);
544
+ const logins = r.json.builders.map((b) => b.github_login);
545
+ assert.deepEqual(logins, ['r06-visible'], 'only the consented, non-hidden builder is listed');
546
+ assert.equal(engine.calls.length, 1, 'exactly ONE read — nothing can re-enter via a second, unfiltered query');
547
+ for (const s of [HIDDEN_LOGIN, PROV_LOGIN, '88817', '6613', '5519', '71713']) {
548
+ assert.ok(!r.text.includes(s), `the excluded builder leaks nothing — found "${s}"`);
549
+ }
550
+ });
551
+
552
+ await t('R06.3 the /people page reads /scouting and nothing else', async () => {
553
+ // /people is the hall's render of the same endpoint, so whatever /scouting
554
+ // drops, /people drops. Pin that it has no second data source.
555
+ const endpoints = [...sourceOf('modules/hall-ui/public/people.js').matchAll(/fetch\(\s*'([^']+)'/g)].map((m) => m[1]);
556
+ assert.deepEqual(endpoints, ['/api/gds/scouting?limit=500'], '/people has exactly one data source');
557
+ });
558
+
559
+ scout.server.close();
280
560
  server.close();
281
561
 
282
562
  console.log(`\n${passed} passed, ${failed} failed`);