@bongos/core 1.19.421 → 1.19.423

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.421",
6
- "core_contract": "1.19.421",
7
- "source_commit": "ea6e65fb6df2e2ca09fcb0769b194b3ccb407878",
5
+ "core_version": "1.19.423",
6
+ "core_contract": "1.19.423",
7
+ "source_commit": "4766551ebf3debe1bd66da9feb7974ea29a9fc7d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-29T17:49:20.389Z",
9
+ "built_at": "2026-08-29T18:48:03.289Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 359,
13
13
  "agent_docs_stubbed": 23,
14
- "functional_verbatim": 1719,
14
+ "functional_verbatim": 1720,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2101,
20
- "tree_sha256": "9528e100c022a131a7ee286c52e98c8bd5544beec72d013af6aea2a668a8e487",
19
+ "file_count": 2102,
20
+ "tree_sha256": "e08d232a8919912b3eaa5b098b0ad5e505a24ec5c55e56f236762ef12684ba97",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1937,7 +1937,7 @@
1937
1937
  {
1938
1938
  "path": "docs/module-api-changelog.md",
1939
1939
  "mode": "0000644",
1940
- "sha256": "54aad52c9bf29c0443c656e9ccc606b886e241bd6f0f1f6ea178d6e0ff4ce46a"
1940
+ "sha256": "13dba1ab067b238b37395e6e23728b198a7f944bb15e0a4b4850ceefdfafb2b1"
1941
1941
  },
1942
1942
  {
1943
1943
  "path": "docs/modules-contract.md",
@@ -5497,12 +5497,12 @@
5497
5497
  {
5498
5498
  "path": "package-lock.json",
5499
5499
  "mode": "0000644",
5500
- "sha256": "c735a709aeb39ba8d543a0d1a116c6c4c99398fdee629b719228b21dc351e931"
5500
+ "sha256": "c6703f3be312d7ab5830f5f4aceda03cb1e5689ac3d82568f835d8a9ee3eeb4a"
5501
5501
  },
5502
5502
  {
5503
5503
  "path": "package.json",
5504
5504
  "mode": "0000644",
5505
- "sha256": "82dbf5b00684863b19d987b3aed3abbf668b38f2aa8133081993a3dc6ec37b57"
5505
+ "sha256": "5fca13b9621fcb29a90b4c4ea608a0083a9fc3574dfeecf8b35fed342d147491"
5506
5506
  },
5507
5507
  {
5508
5508
  "path": "public-docs/index.html",
@@ -5742,7 +5742,7 @@
5742
5742
  {
5743
5743
  "path": "public-landing/assets/cosmos.css",
5744
5744
  "mode": "0000644",
5745
- "sha256": "85797e1405425d852572a796f56fde0928b596af49b04214b576c7e4067fbfcd"
5745
+ "sha256": "282ec017ecdd9ecde6ef3c80df5b7874b392e2b4d97ae29b4222a87c2ed51f3a"
5746
5746
  },
5747
5747
  {
5748
5748
  "path": "public-landing/assets/verro-medallion.png",
@@ -5962,7 +5962,7 @@
5962
5962
  {
5963
5963
  "path": "public-landing/projects.html",
5964
5964
  "mode": "0000644",
5965
- "sha256": "8f74c9a2d3eda52bc91d04c0b1b13fc4752688fdf864bcc0ae8cb9e242c7b8d3"
5965
+ "sha256": "4174cf9209dd56e38944462d62f178bf684a7fb593d3c20b81620f004e64a57b"
5966
5966
  },
5967
5967
  {
5968
5968
  "path": "public-landing/projects.probes.json",
@@ -6842,7 +6842,7 @@
6842
6842
  {
6843
6843
  "path": "scripts/gds/run-unit-tests.js",
6844
6844
  "mode": "0000644",
6845
- "sha256": "29ee6bd76bdfb65522170808b6e636032079a145df4f995570901fb4f05b6035"
6845
+ "sha256": "9a173b5e307f5c1396f1d72b88d0b05534cccbff53af0a39a620001df69bf1a8"
6846
6846
  },
6847
6847
  {
6848
6848
  "path": "scripts/gds/sandbox-stage.js",
@@ -7632,7 +7632,7 @@
7632
7632
  {
7633
7633
  "path": "src/module-api.js",
7634
7634
  "mode": "0000644",
7635
- "sha256": "1dda5c82cffdacd548fa5f46dace55a9967f8b43b3ddd3eff267ddf9002f0020"
7635
+ "sha256": "187cad1863a33d5e9c7db35bad173525124dbc8534a87ecf2b760397fab37d12"
7636
7636
  },
7637
7637
  {
7638
7638
  "path": "src/module-loader/catalog.js",
@@ -9654,6 +9654,11 @@
9654
9654
  "mode": "0000644",
9655
9655
  "sha256": "a7f70fa612494593438565f4bd1e3d454a3dbca862c8396a7dcb252d38e3e489"
9656
9656
  },
9657
+ {
9658
+ "path": "tests/profile_privacy_owner_view.mjs",
9659
+ "mode": "0000644",
9660
+ "sha256": "7ccf39b7557fdb7f906210a8d2e58079b92c860d25bc906d8243b8477b02a02a"
9661
+ },
9657
9662
  {
9658
9663
  "path": "tests/profile_rollup_consent.mjs",
9659
9664
  "mode": "0000644",
@@ -9682,12 +9687,12 @@
9682
9687
  {
9683
9688
  "path": "tests/projects_hub.mjs",
9684
9689
  "mode": "0000644",
9685
- "sha256": "27a10d25acbf114ba0939325bf225399503b54149d3c24f25592c82d7bbe5f85"
9690
+ "sha256": "b40e405b673524ad0d33c041730771d1ebf9c95198062ebf307aed5fa1e7d501"
9686
9691
  },
9687
9692
  {
9688
9693
  "path": "tests/projects_hub_pre_uat.mjs",
9689
9694
  "mode": "0000644",
9690
- "sha256": "ecf1a225759d8d8475bac145727c006fe7341c4b945690ffd47a620e42e8140e"
9695
+ "sha256": "48b314b096c67793dba8f5faeec5b03d043c42a17a38466e072e6bf9c7ae9ec5"
9691
9696
  },
9692
9697
  {
9693
9698
  "path": "tests/promote_goal_id.mjs",
@@ -1279,5 +1279,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1279
1279
  landed since 1.19.419 with no explicit bump. run 33266034679. (task 1002620)
1280
1280
  1.19.421 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1281
1281
  landed since 1.19.420 with no explicit bump. run 33266605270. (task 1002620)
1282
+ 1.19.422 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1283
+ landed since 1.19.421 with no explicit bump. run 33267393283. (task 1002620)
1284
+ 1.19.423 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1285
+ landed since 1.19.422 with no explicit bump. run 33269205170. (task 1002620)
1282
1286
  ---------------------------------------------------------------------------
1283
1287
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.421",
3
+ "version": "1.19.423",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.421",
9
+ "version": "1.19.423",
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.421",
3
+ "version": "1.19.423",
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",
@@ -903,17 +903,30 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
903
903
  .tbl td.right{text-align:right;}
904
904
  /* Nothing in an action or status cell may wrap: "Manage →" breaking after the
905
905
  space and the chip's recency dropping to a second line both read as a broken
906
- control rather than a tight column. Those two cells claim their width and the
907
- name cell absorbs the slack. */
906
+ control rather than a tight column. */
908
907
  .tbl .btn,.tbl .chip{white-space:nowrap;}
909
- .tbl .colStatus{width:1%;white-space:nowrap;}
910
- .tbl .colGo{width:1%;white-space:nowrap;padding-left:8px;}
911
- .tbl .colName{width:auto;}
912
- .tbl .colType{width:1%;white-space:nowrap;}
908
+ .tbl .colStatus,.tbl .colGo,.tbl .colType{white-space:nowrap;}
909
+ .tbl .colGo{padding-left:8px;}
910
+ /* ── FIXED tracks (task 1003348; the owner: "the columns don't look aligned").
911
+ An auto table sized every column by its content, so the status column hugged
912
+ its widest chip and the headers drifted off their cells. table-layout:fixed
913
+ takes the widths from the header row — one set of tracks for every row, each
914
+ column edge one vertical line — and the name column absorbs the slack. The
915
+ Project header sits over the NAME (past the 10px dot and its 12px gap), and a
916
+ right-aligned column's header is right-aligned too. Below the table's minimum
917
+ the wrap scrolls (#sub-table-body), never the page. ── */
918
+ .tbl{table-layout:fixed;min-width:860px;}
919
+ .tbl th.colName{padding-left:22px;}
920
+ .tbl th.right{text-align:right;}
921
+ .tbl th.colType{width:88px;}
922
+ .tbl th.colBuilders{width:96px;}
923
+ .tbl th.colTags{width:168px;}
924
+ .tbl th.colStatus{width:224px;}
925
+ .tbl th.colGo{width:112px;}
913
926
  /* the declared-type tag (task 1002772) — its own vocabulary: not the relationship
914
927
  .pill, not the liveness .chip. Two different facts never wear the same chrome. */
915
928
  .typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--rule);border-radius:999px;padding:2px 9px;white-space:nowrap;}
916
- @media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}}
929
+ @media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}.tbl{min-width:600px;}}
917
930
  @media (max-width:900px){
918
931
  .tblMast{gap:16px;padding:22px 0 20px;}
919
932
  .tblTitle{font-size:clamp(28px,7.4vw,34px);}
@@ -1065,7 +1078,6 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1065
1078
  .rowHall{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:10px;
1066
1079
  vertical-align:middle;color:var(--accent-deep);border-radius:var(--radius-pill);}
1067
1080
  .rowHall:hover{background:var(--glass);}
1068
- .tbl .colGo{white-space:nowrap;}
1069
1081
  @media (max-width:640px){
1070
1082
  .projCard h3{font-size:26px;padding:0 20px;}
1071
1083
  .projCard .cardTag,.projCard .cardRel,.projCard .cardFaces,.projCard .cardFigs,.projCard .cardMeta,
@@ -1154,8 +1166,10 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1154
1166
  structure, the project's own planet as the row's object, the strip drawn
1155
1167
  as the lifecycle rail, the doors as hairline pills with the accent only on
1156
1168
  the one that acts. One set of FIXED tracks across every row, so the list
1157
- reads as one ledger (task 1003348). Dark only (ADR 0204); every colour a
1158
- token; halos are outlines, never box-shadow (DESIGN.md).
1169
+ reads as one ledger — the same column grammar the every-project table takes
1170
+ in its `.tbl` rules (table-layout:fixed, the header row's widths; task
1171
+ 1003348, both halves pinned together in tests/projects_hub.mjs). Dark only
1172
+ (ADR 0204); every colour a token; halos are outlines, never box-shadow.
1159
1173
  ═══════════════════════════════════════════════════════════════════════ */
1160
1174
  #view-mine .wrap,#view-manage .wrap{max-width:1180px;}
1161
1175
  #view-mine .card,#view-manage .card{background:none;border-radius:0;box-shadow:none;padding:0;margin:0;}
@@ -489,7 +489,7 @@ summary{min-height:24px;padding:3px 0;}
489
489
  <tr>
490
490
  <th class="colName" data-sort="name" aria-sort="ascending"><button type="button" class="thSort">Project</button></th>
491
491
  <th class="colType">Type</th>
492
- <th class="right" data-sort="builders"><button type="button" class="thSort">Builders</button></th>
492
+ <th class="right colBuilders" data-sort="builders"><button type="button" class="thSort">Builders</button></th>
493
493
  <th class="colTags">Runs</th>
494
494
  <th class="right colStatus">Status</th>
495
495
  <th class="colGo"></th>
@@ -106,7 +106,14 @@ const INTEGRATION = new Set([
106
106
  // the DISTINCT, and that a deactivated builder or a machine principal still
107
107
  // never governs under the widened predicate.
108
108
  'government_board_membership_db',
109
- 'government_fold_bench', 'government_live_proof', 'linkify_refs', 'memory_isolation',
109
+ 'government_fold_bench', 'government_live_proof',
110
+ // task 1002806: these three declare "in the INTEGRATION set" in their own headers
111
+ // and were never in it. Every one boots a real Postgres and self-skips without one,
112
+ // so in the DB-free lane they ran, skipped, exited 0 and asserted NOTHING — while
113
+ // the --integration lane never saw them either, because it runs only this set. A
114
+ // test that cannot fail in either lane is a file that looks like a gate.
115
+ 'handle_machinery', 'hub_profile_leak', 'profile_fields',
116
+ 'linkify_refs', 'memory_isolation',
110
117
  'memory_pull', 'needs_rebase', 'parallel-load', 'profile_rollup_consent',
111
118
  // task 1003145: whether the PLANNER uses the migration 225 index for the recall
112
119
  // freshness probe. The DB-free sibling (recall_corpus_health) pins the query
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.421'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.423'; // 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');
@@ -0,0 +1,477 @@
1
+ // tests/profile_privacy_owner_view.mjs — P10 (task 1002806, goal 1000045):
2
+ // "a private account's profile 404s to a stranger and RENDERS TO ITS OWNER",
3
+ // and a stats-hidden account renders NO PARTIAL NUMBERS.
4
+ //
5
+ // WHAT THIS FILE IS, AND WHAT IT DELIBERATELY IS NOT. The 404 half of P10 was
6
+ // already landed by task 1002967 in tests/effective_visibility_predicate.mjs:
7
+ // private / provisional / private-discoverable / private-previous-handle all
8
+ // 404 on GET /profiles/:handle, never 403, byte-identically for an anonymous
9
+ // viewer, a signed-in stranger and the owner. Re-asserting that here would buy
10
+ // nothing. This file is the OTHER half — the two things that proof left open:
11
+ //
12
+ // 1. THE OWNER'S PAGE, OVER REAL HTTP. 1002967 proves the own-scope side door
13
+ // at the FUNCTION level (pi.getOwnProfileExtras). But the page shell reads
14
+ // GET /me/cross-project, and that route is what must hand it the banner
15
+ // input (account_visibility='private') alongside the owner's own numbers.
16
+ // A function that behaves is not a route that behaves.
17
+ //
18
+ // 2. THE BYPASS THAT WOULD NULLIFY EVERY 404. The public read is viewer-
19
+ // independent precisely because the owner gets their page from the
20
+ // own-scope door instead. That door is therefore load-bearing for the
21
+ // privacy model: if it ever answered a CALLER-NAMED github_id instead of
22
+ // the session's, every 404 in 1002967 becomes a speed bump — ask the side
23
+ // door for the private account instead. Nothing tested that. This does.
24
+ //
25
+ // 3. NO PARTIAL NUMBERS, PROVED BY SUBTRACTION. 1002967 asserts the hidden
26
+ // account's body matches /"cross_project":null/. That is necessary and not
27
+ // sufficient: a future response that added `projects_count: 7` beside a
28
+ // null cross_project still matches it. So here every rollup-derived value
29
+ // is a PLANTED SENTINEL and the assertion is that none of them appears
30
+ // ANYWHERE in the hidden account's response text — with two controls that
31
+ // stop it passing vacuously (a public account's body DOES carry its
32
+ // sentinels; the hidden account's own numbers DO exist behind the door).
33
+ //
34
+ // THE RULE THIS FOLLOWS IS THE CODE'S, NOT THE PROFILE-PAGE SPEC'S. The spec
35
+ // docs/specs/bongos-v1-builder-profile-page.md says 404 "to everyone but the
36
+ // account itself", which reads as a viewer-dependent public response. The
37
+ // AUTHORITATIVE model — the privacy spec (task 1002785) D2 — reconciles it the
38
+ // other way and the code implements the reconciliation: the PUBLIC read 404s to
39
+ // everyone INCLUDING the owner, and the owner's page is composed from the
40
+ // own-scope endpoint. Both halves are asserted below.
41
+ //
42
+ // MECHANISM (tests/connections_ownership.mjs + tests/own_session_revoke.mjs):
43
+ // an in-memory accounts + activity table whose row filter is built FROM EACH
44
+ // STATEMENT'S OWN WHERE CLAUSE, refusing any statement whose placeholders or
45
+ // literal comparisons it did not consume — so a predicate the fake cannot read
46
+ // is a loud error rather than a silently absent filter. Everything above the
47
+ // pool is real: the real routers, the real domain module, real HTTP.
48
+ //
49
+ // DB-free by construction. Runs in the lane that actually executes (the last
50
+ // test in this file proves that).
51
+ //
52
+ // Run: node tests/profile_privacy_owner_view.mjs
53
+
54
+ import { strict as assert } from 'node:assert';
55
+ import { test, after } from 'node:test';
56
+ import { createRequire } from 'node:module';
57
+ import http from 'node:http';
58
+ import fs from 'node:fs';
59
+ import { fileURLToPath } from 'node:url';
60
+ import { dirname, join } from 'node:path';
61
+
62
+ const require = createRequire(import.meta.url);
63
+ const express = require('express');
64
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
65
+
66
+ // ===========================================================================
67
+ // The fixture: four accounts, and a sentinel for every rollup-derived value
68
+ // ===========================================================================
69
+ //
70
+ // Sentinels are per-account so a cross-wired response is caught too, and they
71
+ // are improbable literals so "absent from the body" means absent, not merely
72
+ // "did not collide with a 0 or a 1".
73
+
74
+ const OCTO = '4242'; // active · public · shown — the CONTROL
75
+ const GHOST = '5150'; // active · PRIVATE — the owner-renders subject
76
+ const SHY = '6060'; // active · public · HIDDEN — the no-partial-numbers subject
77
+ const NOSY = '7777'; // active · public — the stranger at the side door
78
+
79
+ const ACCOUNTS = [
80
+ { id: 1, github_id: OCTO, github_login: 'octo', display_name: 'Octo', avatar_url: null,
81
+ handle: 'octo', previous_handle: null, handle_changed_at: null, bio: null, links: null,
82
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
83
+ profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
84
+ { id: 2, github_id: GHOST, github_login: 'ghost', display_name: 'Ghost', avatar_url: null,
85
+ handle: 'ghost', previous_handle: null, handle_changed_at: null, bio: 'ghost-bio-515007', links: null,
86
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
87
+ profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
88
+ { id: 3, github_id: SHY, github_login: 'shy', display_name: 'Shy', avatar_url: null,
89
+ handle: 'shy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
90
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
91
+ profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
92
+ { id: 4, github_id: NOSY, github_login: 'nosy', display_name: 'Nosy', avatar_url: null,
93
+ handle: 'nosy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
94
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
95
+ profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
96
+ ];
97
+
98
+ // Every number and name the cross-project rollup can produce, per account.
99
+ const ROLLUP = {
100
+ [OCTO]: {
101
+ agg: { projects: 811008, credits: 811001, works_shipped: 811002, karma: 811003, last_active: 'octo-active-811007', disciplines: ['octo-craft-811004'] },
102
+ projects: [{ name: 'Octo-Project-811005', origin: 'https://octo.example', rank_held: 'metic', recent_ships: ['Octo-Ship-811006'], is_public: true }],
103
+ },
104
+ [GHOST]: {
105
+ agg: { projects: 515008, credits: 515001, works_shipped: 515002, karma: 515003, last_active: 'ghost-active-515007', disciplines: ['ghost-craft-515004'] },
106
+ projects: [{ name: 'Ghost-Project-515005', origin: 'https://ghost.example', rank_held: 'metic', recent_ships: ['Ghost-Ship-515006'], is_public: true }],
107
+ },
108
+ [SHY]: {
109
+ agg: { projects: 606008, credits: 606001, works_shipped: 606002, karma: 606003, last_active: 'shy-active-606007', disciplines: ['shy-craft-606004'] },
110
+ projects: [{ name: 'Shy-Project-606005', origin: 'https://shy.example', rank_held: 'metic', recent_ships: ['Shy-Ship-606006'], is_public: true }],
111
+ },
112
+ [NOSY]: {
113
+ agg: { projects: 777008, credits: 777001, works_shipped: 777002, karma: 777003, last_active: 'nosy-active-777007', disciplines: ['nosy-craft-777004'] },
114
+ projects: [{ name: 'Nosy-Project-777005', origin: 'https://nosy.example', rank_held: 'metic', recent_ships: ['Nosy-Ship-777006'], is_public: true }],
115
+ },
116
+ };
117
+
118
+ // Every sentinel belonging to one account, as strings — the "did any of this
119
+ // leak?" alphabet. Numbers are included as their serialized form.
120
+ function sentinelsOf(githubId) {
121
+ const r = ROLLUP[githubId];
122
+ return [
123
+ String(r.agg.projects),
124
+ String(r.agg.credits), String(r.agg.works_shipped), String(r.agg.karma),
125
+ r.agg.last_active, ...r.agg.disciplines,
126
+ ...r.projects.flatMap((p) => [p.name, p.origin, ...p.recent_ships]),
127
+ ];
128
+ }
129
+
130
+ // ===========================================================================
131
+ // The in-memory tables + their statement interpreter
132
+ // ===========================================================================
133
+
134
+ // The columns this interpreter can filter on. A literal comparison against
135
+ // anything else throws — a WHERE it cannot read must never degrade to no WHERE.
136
+ const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'handle', 'previous_handle']);
137
+
138
+ /**
139
+ * Build a row test from a statement's own WHERE text. Literal comparisons carry
140
+ * as much weight as placeholders here: the effective-visibility predicate is
141
+ * made ENTIRELY of literal comparisons, so a fake that modelled only $N filters
142
+ * would score the privacy terms as absent and pass however the code changed.
143
+ */
144
+ function accountFilter(where, params) {
145
+ const tests = [];
146
+ const consumed = new Set();
147
+
148
+ if (/\bprofile_state = 'active'/.test(where)) tests.push((r) => r.profile_state === 'active');
149
+ if (/\baccount_visibility = 'public'/.test(where)) tests.push((r) => r.account_visibility === 'public');
150
+ if (/\bNOT (?:[a-z_]+\.)?hide_stats\b/.test(where)) tests.push((r) => !r.hide_stats);
151
+
152
+ const byId = /\b(?:[a-z_]+\.)?github_id = \$(\d+)/.exec(where);
153
+ if (byId) {
154
+ consumed.add(byId[1]);
155
+ const v = params[Number(byId[1]) - 1];
156
+ tests.push((r) => String(r.github_id) === String(v));
157
+ }
158
+
159
+ const byHandle = /lower\(handle\) = \$(\d+) OR lower\(previous_handle\) = \$(\d+)/.exec(where);
160
+ if (byHandle) {
161
+ consumed.add(byHandle[1]); consumed.add(byHandle[2]);
162
+ const v = String(params[Number(byHandle[1]) - 1]);
163
+ tests.push((r) => String(r.handle || '').toLowerCase() === v || String(r.previous_handle || '').toLowerCase() === v);
164
+ }
165
+
166
+ for (const m of where.matchAll(/\$(\d+)/g)) {
167
+ if (!consumed.has(m[1])) {
168
+ throw new Error(`fake pool: unread predicate on $${m[1]} in "${where}" — a filter this interpreter cannot read must never degrade to no filter`);
169
+ }
170
+ }
171
+ for (const m of where.matchAll(/\b([a-z_]+)\s*(?:=|<>|!=)\s*'/g)) {
172
+ if (!KNOWN_COLUMNS.has(m[1])) {
173
+ throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
174
+ }
175
+ }
176
+ return (r) => tests.every((fn) => fn(r));
177
+ }
178
+
179
+ // The github_id an activity statement is scoped to, read from its own WHERE.
180
+ function activityScope(sql, params) {
181
+ const m = /\ba\.github_id = \$(\d+)/.exec(sql);
182
+ if (!m) throw new Error(`fake pool: an activity read with no github_id scope — ${sql.slice(0, 120)}`);
183
+ return String(params[Number(m[1]) - 1]);
184
+ }
185
+
186
+ let calls = [];
187
+ const pool = {
188
+ query: async (text, params = []) => {
189
+ const sql = String(text).replace(/\s+/g, ' ').trim();
190
+ calls.push({ sql, params });
191
+
192
+ if (/FROM platform_identity_accounts/.test(sql)) {
193
+ const where = sql.includes(' WHERE ')
194
+ ? sql.slice(sql.indexOf(' WHERE ') + 7).replace(/ ORDER BY .*| LIMIT .*/g, '')
195
+ : '';
196
+ const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
197
+ let rows = ACCOUNTS.filter(accountFilter(where, params));
198
+ if (/is_current/.test(cols)) {
199
+ // resolveHandle asks the row whether the handle it matched is the CURRENT
200
+ // one — the flag that forks 200 from 301. Derived, never assumed.
201
+ const asked = String(params[0]).toLowerCase();
202
+ rows = rows
203
+ .map((r) => ({ ...r, is_current: String(r.handle || '').toLowerCase() === asked }))
204
+ .sort((a, b) => Number(b.is_current) - Number(a.is_current))
205
+ .slice(0, /LIMIT 1/.test(sql) ? 1 : rows.length);
206
+ }
207
+ return { rows: rows.map((r) => ({ ...r })), rowCount: rows.length };
208
+ }
209
+
210
+ // The rollup aggregate — one row, always (an aggregate with no GROUP BY).
211
+ if (/WITH agg AS/.test(sql) && /platform_identity_builder_project_activity/.test(sql)) {
212
+ const who = ROLLUP[activityScope(sql, params)];
213
+ const agg = who ? who.agg : { projects: 0, credits: 0, works_shipped: 0, karma: 0, last_active: null, disciplines: null };
214
+ return { rows: [{ ...agg }], rowCount: 1 };
215
+ }
216
+
217
+ // The per-project rows the rollup names (or collapses into more_projects).
218
+ if (/platform_identity_builder_project_activity/.test(sql)) {
219
+ const who = ROLLUP[activityScope(sql, params)];
220
+ const rows = who ? who.projects.map((p) => ({ ...p })) : [];
221
+ return { rows, rowCount: rows.length };
222
+ }
223
+
224
+ throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 140)}`);
225
+ },
226
+ };
227
+
228
+ // ===========================================================================
229
+ // The real routers over those tables
230
+ // ===========================================================================
231
+
232
+ // The viewer the stubbed session middleware presents. Real requireBuilder reads
233
+ // it from the session; the route body under test is untouched, and the route
234
+ // body is the whole point — a route that reached for req.query.github_id would
235
+ // be just as wrong with real auth in front of it.
236
+ let viewer = null;
237
+
238
+ const moduleApiPath = require.resolve('../src/module-api.js');
239
+ const helpers = require('../src/bongos/routes/_helpers.js');
240
+ require.cache[moduleApiPath] = {
241
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
242
+ exports: {
243
+ pool,
244
+ validateOrRespond: helpers.validateOrRespond,
245
+ // The public route's CORS wrapper is not under test; a pass-through keeps
246
+ // the doorway honest without dragging core config in.
247
+ corsPublicGet: () => (_req, _res, next) => next(),
248
+ requireBuilder: (req, res, next) => {
249
+ if (!viewer) return res.fail('unauthenticated', 401);
250
+ req.builder = { ...viewer };
251
+ next();
252
+ },
253
+ },
254
+ };
255
+
256
+ const pi = require('../modules/platform-identity/platform-identity.js');
257
+ const buildProfileRoutes = require('../modules/platform-identity/routes/profile.js');
258
+ const buildPublicProfileRoutes = require('../modules/platform-identity/routes/public-profile.js');
259
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
260
+
261
+ const app = express();
262
+ app.use(express.json());
263
+ app.use(attachFail);
264
+ app.use('/api/bongos', buildProfileRoutes());
265
+ app.use('/api/bongos', buildPublicProfileRoutes());
266
+ const server = app.listen(0, '127.0.0.1');
267
+ await new Promise((r) => server.once('listening', r));
268
+ const port = server.address().port;
269
+ after(() => server.close());
270
+
271
+ function get(path_, headers = {}) {
272
+ calls = [];
273
+ return new Promise((resolve, reject) => {
274
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: path_, headers }, (res) => {
275
+ const chunks = [];
276
+ res.on('data', (c) => chunks.push(c));
277
+ res.on('end', () => {
278
+ const text = Buffer.concat(chunks).toString('utf8');
279
+ let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
280
+ resolve({ status: res.statusCode, text, json });
281
+ });
282
+ });
283
+ req.on('error', reject);
284
+ req.end();
285
+ });
286
+ }
287
+
288
+ const asAccount = (githubId) => {
289
+ const a = ACCOUNTS.find((x) => x.github_id === githubId);
290
+ viewer = { github_id: a.github_id, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url };
291
+ };
292
+
293
+ // ===========================================================================
294
+ // 0. The instrument is calibrated before it is trusted
295
+ // ===========================================================================
296
+
297
+ test('the interpreter refuses a predicate it cannot read, rather than ignoring it', () => {
298
+ assert.throws(
299
+ () => accountFilter('github_id = $1 AND some_new_flag = $2', [OCTO, true]),
300
+ /unread predicate on \$2/,
301
+ 'a filter the fake cannot model must be a loud error — otherwise a rewritten WHERE silently widens',
302
+ );
303
+ assert.throws(
304
+ () => accountFilter("github_id = $1 AND shadow_banned = 'yes'", [OCTO]),
305
+ /unrecognised literal comparison on shadow_banned/,
306
+ 'the privacy predicate is made of LITERAL comparisons — an unmodelled one must not read as absent',
307
+ );
308
+ });
309
+
310
+ test('the interpreter really does narrow on the visibility literals', () => {
311
+ const gated = ACCOUNTS.filter(accountFilter("(profile_state = 'active' AND account_visibility = 'public') AND (lower(handle) = $1 OR lower(previous_handle) = $1)", ['ghost']));
312
+ const ungated = ACCOUNTS.filter(accountFilter('(lower(handle) = $1 OR lower(previous_handle) = $1)', ['ghost']));
313
+ assert.equal(gated.length, 0, 'with the predicate, the private account is not reachable by handle');
314
+ assert.equal(ungated.length, 1, 'without it the SAME shape finds them — so dropping it genuinely changes this fixture');
315
+ });
316
+
317
+ // ===========================================================================
318
+ // 1. No partial numbers: the hidden account leaks nothing by subtraction
319
+ // ===========================================================================
320
+
321
+ test('CONTROL: a public account really does serve every rollup sentinel', async () => {
322
+ // Without this, "no sentinels in the hidden body" could just mean the rollup
323
+ // never ran at all, and every assertion below would be vacuous.
324
+ viewer = null;
325
+ const res = await get('/api/bongos/profiles/octo');
326
+ assert.equal(res.status, 200);
327
+ for (const s of sentinelsOf(OCTO)) {
328
+ assert.ok(res.text.includes(s), `a public account's body must carry its rollup value ${s} — otherwise the leak test proves nothing`);
329
+ }
330
+ assert.ok(res.json.cross_project && res.json.cross_project.totals, 'and the payload really is the rollup shape');
331
+ });
332
+
333
+ test('CONTROL: the hidden account HAS numbers — they exist, they are withheld', async () => {
334
+ // The other vacuity guard: prove the sentinels are real data behind the door,
335
+ // not an empty fixture that trivially cannot appear in any response.
336
+ const own = await pi.getOwnProfileExtras(SHY, { pool });
337
+ assert.equal(own.totals.credits, 606001, 'the hidden account has real numbers');
338
+ assert.equal(own.totals.works_shipped, 606002);
339
+ assert.deepEqual(own.projects.map((p) => p.name), ['Shy-Project-606005']);
340
+ });
341
+
342
+ test('a stats-hidden account renders NO PARTIAL NUMBERS — not one sentinel survives', async () => {
343
+ viewer = null;
344
+ const res = await get('/api/bongos/profiles/shy');
345
+ assert.equal(res.status, 200, 'hide removes the ROLLUP, not the page (spec D2) — the identity band still resolves');
346
+
347
+ // The whole point of P10: a count beside a hidden total is a leak by
348
+ // subtraction, so the test is not "cross_project is null" (which a stray
349
+ // sibling key would satisfy) but "none of these values is anywhere".
350
+ for (const s of sentinelsOf(SHY)) {
351
+ assert.ok(!res.text.includes(s), `the hidden account's ${s} appears in the response — a count beside a hidden total is a leak by subtraction`);
352
+ }
353
+
354
+ // ...and the withholding is TOTAL, not a hollowed-out object. An empty
355
+ // {totals:{}} would tell a reader the rollup exists and is being suppressed.
356
+ assert.equal(res.json.cross_project, null, 'cross_project must be exactly null, never a partial or emptied object');
357
+ assert.ok(!('totals' in res.json), 'no rollup key may be hoisted to the top level to survive the null');
358
+ assert.deepEqual(Object.keys(res.json).sort(), ['account', 'cross_project'],
359
+ 'the hidden response must carry no extra key — a new sibling key is exactly how a partial number arrives');
360
+
361
+ // The identity band is the other place a stray number can dock. The route
362
+ // whitelists it, and this pins that whitelist: a field added here reaches the
363
+ // world, so it must be a deliberate act that fails a test first.
364
+ assert.deepEqual(Object.keys(res.json.account).sort(),
365
+ ['avatar_url', 'bio', 'display_name', 'github_login', 'handle', 'links', 'member_since'],
366
+ 'a new key on the identity band of a HIDDEN account is how a count arrives beside a hidden total');
367
+ });
368
+
369
+ test('the withholding is indistinguishable from having no numbers at all', async () => {
370
+ // A distinct "hidden" marker would be the same disclosure as a 403: it tells
371
+ // a stranger the account has a track record it is choosing not to show.
372
+ viewer = null;
373
+ const shy = await get('/api/bongos/profiles/shy');
374
+ assert.doesNotMatch(shy.text, /hidden|hide_stats|private|withheld|suppressed/i,
375
+ 'the response must not name WHY the numbers are absent — that is itself the disclosure');
376
+ });
377
+
378
+ // ===========================================================================
379
+ // 2. Renders to its owner: the own-scope door, over real HTTP
380
+ // ===========================================================================
381
+
382
+ test('the private account gets its OWN page from the own-scope door', async () => {
383
+ // The half the public 404 exists to make safe. GET /me/cross-project is what
384
+ // the page shell reads when the viewer is the account.
385
+ asAccount(GHOST);
386
+ const res = await get('/api/bongos/me/cross-project');
387
+ assert.equal(res.status, 200, 'a private account is still a full participant in its own scope');
388
+
389
+ // The banner input: the shell renders "private" from this field, and it must
390
+ // be the STORED value, not a default.
391
+ assert.equal(res.json.account_visibility, 'private',
392
+ 'without this the owner cannot be told their page is private — the banner has no input');
393
+ assert.equal(res.json.handle, 'ghost', 'and it is their own record, identified');
394
+ assert.equal(res.json.bio, 'ghost-bio-515007');
395
+
396
+ // Their numbers are theirs: the side door is deliberately flag-blind.
397
+ assert.ok(res.json.cross_project, 'a private account must be able to read its own track record');
398
+ assert.equal(res.json.cross_project.totals.credits, 515001);
399
+ assert.equal(res.json.cross_project.totals.works_shipped, 515002);
400
+ assert.deepEqual(res.json.cross_project.projects.map((p) => p.name), ['Ghost-Project-515005']);
401
+
402
+ // ...and the reason it is a SIDE door: no visibility predicate is applied.
403
+ assert.ok(!calls.some((c) => /account_visibility = '|profile_state = '/.test(c.sql)),
404
+ 'the self view must apply no visibility predicate — that is what makes it a side door, not a second public read');
405
+ });
406
+
407
+ test('BOTH HALVES AT ONCE: the same account, same instant — 404 public, 200 own', async () => {
408
+ // P10 stated as one assertion. The public response is viewer-independent, so
409
+ // the owner sees 404 there too (privacy spec D2, reconciling the profile-page
410
+ // spec's looser "404 to everyone but the account itself"); the page they
411
+ // actually see is composed from the own-scope door.
412
+ asAccount(GHOST);
413
+ const publicRead = await get('/api/bongos/profiles/ghost');
414
+ assert.equal(publicRead.status, 404, 'the PUBLIC read 404s even for the owner — it takes no viewer, so it cannot vary by one');
415
+ assert.notEqual(publicRead.status, 403, 'a 403 would confirm the handle exists, which is the disclosure the 404 rule prevents');
416
+ assert.ok(!publicRead.text.includes('Ghost'), 'and nothing about the account rides in the 404 body');
417
+ assert.ok(!publicRead.text.includes('ghost-bio-515007'));
418
+
419
+ const ownRead = await get('/api/bongos/me/cross-project');
420
+ assert.equal(ownRead.status, 200, 'and the very same account, in the very same state, renders to itself');
421
+ assert.equal(ownRead.json.handle, 'ghost');
422
+ });
423
+
424
+ test('a HIDDEN owner still sees their own numbers — hide is a public rule, not a self rule', async () => {
425
+ asAccount(SHY);
426
+ const res = await get('/api/bongos/me/cross-project');
427
+ assert.equal(res.status, 200);
428
+ assert.equal(res.json.hide_stats, true, 'the toggle reads back, so Settings can render its state');
429
+ assert.equal(res.json.cross_project.totals.credits, 606001,
430
+ 'ADR 0171 D2: a hidden builder inspects their numbers HERE, which is why the public route may return null');
431
+ });
432
+
433
+ // ===========================================================================
434
+ // 3. The bypass that would nullify every 404
435
+ // ===========================================================================
436
+
437
+ test('the own-scope door answers the SESSION, never a caller-named account', async () => {
438
+ // If this door ever honoured a caller-named id, every 404 the public read
439
+ // returns becomes a speed bump: ask the side door for the private account
440
+ // instead. It is the single most load-bearing assertion in this file.
441
+ asAccount(NOSY);
442
+ const attempts = [
443
+ ['/api/bongos/me/cross-project?github_id=' + GHOST, {}],
444
+ ['/api/bongos/me/cross-project?id=' + GHOST, {}],
445
+ ['/api/bongos/me/cross-project?handle=ghost', {}],
446
+ ['/api/bongos/me/cross-project?github_login=ghost', {}],
447
+ ['/api/bongos/me/cross-project', { 'x-github-id': GHOST }],
448
+ ['/api/bongos/me/cross-project', { 'x-builder-id': GHOST, 'x-github-login': 'ghost' }],
449
+ ];
450
+ for (const [path_, headers] of attempts) {
451
+ const res = await get(path_, headers);
452
+ assert.equal(res.status, 200, `${path_} must still serve the caller`);
453
+ assert.equal(res.json.handle, 'nosy', `${path_} returned someone else's account — the own-scope door is not own-scoped`);
454
+ assert.equal(res.json.account_visibility, 'public', `${path_} leaked another account's privacy state`);
455
+ for (const s of sentinelsOf(GHOST)) {
456
+ assert.ok(!res.text.includes(s), `${path_} leaked the private account's ${s}`);
457
+ }
458
+ }
459
+ });
460
+
461
+ test('the own-scope door is closed to an unauthenticated caller', async () => {
462
+ viewer = null;
463
+ const res = await get('/api/bongos/me/cross-project');
464
+ assert.equal(res.status, 401, 'no session, no self — otherwise "own scope" means "anyone\'s scope"');
465
+ });
466
+
467
+ // ===========================================================================
468
+ // 4. Lane proof — a test that never runs is not a gate (task 1002282)
469
+ // ===========================================================================
470
+
471
+ test('this proof runs in the DB-free lane that executes on every PR', () => {
472
+ const runner = fs.readFileSync(join(ROOT, 'scripts', 'gds', 'run-unit-tests.js'), 'utf8');
473
+ assert.ok(
474
+ !/profile_privacy_owner_view/.test(runner),
475
+ 'named in run-unit-tests.js means parked into INTEGRATION or KNOWN_FAILING — where CI has no Postgres, this file would self-skip forever and gate nothing',
476
+ );
477
+ });
@@ -1655,3 +1655,24 @@ test('the kit proves the field path: the fixture carries a curated template and
1655
1655
  assert.ok(attrs.some((a) => a.includes('amazonprimea.com') && a.endsWith('data-template bead-wash')), 'the curated template reaches the orb');
1656
1656
  assert.ok(attrs.some((a) => a.includes('demo.cloudbongos.com') && a.endsWith('data-material gold')), 'the material reaches the orb as its own attribute');
1657
1657
  });
1658
+
1659
+ /* ── the table's columns on FIXED tracks (task 1003348; the owner, 2026-08-29: "the
1660
+ columns in that projects table view dont look aligned") ── */
1661
+ test('one column grammar: the table lays every row on one set of fixed tracks (each header over its cells), and the mine list shares the rule', () => {
1662
+ assert.match(COSMOS, /\.tbl\{table-layout:fixed;min-width:860px;\}/, 'the header row\'s widths are the table\'s — no per-row content sizing');
1663
+ // the task's other half — the Your projects list — landed with task 1003345 and is pinned above
1664
+ // ('the mine list lays every row on one set of FIXED tracks'); it is asserted here too so the two
1665
+ // halves of the grammar can never drift apart: fixed planet + doors, ratio tracks with no content minimum
1666
+ assert.match(COSMOS, /#view-mine \.proj\{display:grid;grid-template-columns:72px minmax\(0,1fr\);/, 'the mine row: the planet track is a width');
1667
+ assert.match(COSMOS, /#view-mine \.projBody\{display:grid;grid-template-columns:minmax\(0,1\.1fr\) minmax\(0,1\.3fr\) minmax\(0,1fr\) 232px;/, 'the mine row: name, facts and rail by ratio with no content minimum, the doors a width');
1668
+ assert.match(COSMOS, /the same column grammar the every-project table takes/, 'the mine block names the table as its twin');
1669
+ for (const [col, w] of [['colType', 88], ['colBuilders', 96], ['colTags', 168], ['colStatus', 224], ['colGo', 112]]) {
1670
+ assert.match(COSMOS, new RegExp(`\\.tbl th\\.${col}\\{width:${w}px;\\}`), `${col} is a width, not a 1% hug`);
1671
+ }
1672
+ assert.doesNotMatch(COSMOS, /\.tbl \.col\w+\{width:1%/, 'no column hugs its widest cell any more');
1673
+ assert.match(COSMOS, /\.tbl th\.colName\{padding-left:22px;\}/, 'the Project header sits over the name, past the dot and its gap');
1674
+ assert.match(COSMOS, /\.tbl th\.right\{text-align:right;\}/, 'a right-aligned column\'s header is right-aligned too');
1675
+ assert.match(HUB, /<th class="right colBuilders" data-sort="builders">/, 'the builders header names its column so its width can be set');
1676
+ assert.match(COSMOS, /@media \(max-width:780px\)\{\.tbl \.colTags,\.tbl \.colType\{display:none;\}\.tbl\{min-width:600px;\}\}/, 'a phone scrolls the table inside its wrap');
1677
+ assert.match(COSMOS, /#sub-table-body\{overflow-x:auto;\}/, 'the wrap scrolls, never the page');
1678
+ });
@@ -560,7 +560,7 @@ test('the done panel’s progress region is stable, change-only, and says each s
560
560
 
561
561
  test('the map table sorts from the keyboard and an owned row’s name is a real link', () => {
562
562
  assert.match(HUB, /<th class="colName" data-sort="name" aria-sort="ascending"><button type="button" class="thSort">Project<\/button><\/th>/);
563
- assert.match(HUB, /<th class="right" data-sort="builders"><button type="button" class="thSort">Builders<\/button><\/th>/);
563
+ assert.match(HUB, /<th class="right colBuilders" data-sort="builders"><button type="button" class="thSort">Builders<\/button><\/th>/);
564
564
  assert.match(SCRIPT, /var btn = th\.querySelector\('button'\);\n\s*if \(!btn\) return;\n\s*btn\.addEventListener\('click'/, 'the listener sits on the button, not the th');
565
565
  assert.doesNotMatch(SCRIPT, /th\.style\.cursor/, 'no JS-set cursor on the th');
566
566
  assert.match(COSMOS, /\.tbl th button\{all:unset;cursor:pointer;\}/);