@bongos/core 1.19.426 → 1.19.428

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.
Files changed (30) hide show
  1. package/.bongos-core.json +65 -30
  2. package/docs/adr/0212-connections-on-the-profile-surface-and-addressing.md +61 -0
  3. package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +71 -0
  4. package/docs/adr/README.md +2 -0
  5. package/docs/copy-inventory.md +34 -18
  6. package/docs/copy-registry.json +179 -25
  7. package/docs/file-map.md +2 -1
  8. package/docs/module-api-changelog.md +4 -0
  9. package/modules/hall-ui/public/profile-connections.js +304 -0
  10. package/modules/hall-ui/public/profile.html +16 -1
  11. package/modules/hall-ui/public/profile.js +5 -0
  12. package/modules/hall-ui/public/style.css +38 -0
  13. package/modules/platform-identity/account-visibility.js +31 -12
  14. package/modules/platform-identity/connections.js +4 -3
  15. package/modules/platform-identity/migrations/platform_identity_019_terms_acceptance.sql +47 -0
  16. package/modules/platform-identity/platform-identity.js +12 -10
  17. package/modules/platform-identity/routes/connections.js +4 -3
  18. package/modules/platform-identity/terms-acceptance.js +97 -0
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/public-landing/terms.html +8 -0
  22. package/scripts/hall-preview/server.js +3 -0
  23. package/src/module-api.js +1 -1
  24. package/tests/connections_api.mjs +13 -8
  25. package/tests/connections_ownership.mjs +6 -1
  26. package/tests/effective_visibility_predicate.mjs +89 -14
  27. package/tests/profile_connections_ui.mjs +889 -0
  28. package/tests/profile_privacy_owner_view.mjs +31 -4
  29. package/tests/profile_route.mjs +15 -8
  30. package/tests/terms_acceptance.mjs +573 -0
@@ -76,23 +76,28 @@ const GHOST = '5150'; // active · PRIVATE — the owner-renders subjec
76
76
  const SHY = '6060'; // active · public · HIDDEN — the no-partial-numbers subject
77
77
  const NOSY = '7777'; // active · public — the stranger at the side door
78
78
 
79
+ // ADR 0213 (task 1002913): consent is profile_state AND an acceptance of the
80
+ // platform terms, both written by the same statement — so a consented fixture
81
+ // carries both, and one carrying only the first is a row from before the terms.
82
+ const ACCEPTED_AT = '2026-08-29T00:00:00.000Z';
83
+
79
84
  const ACCOUNTS = [
80
85
  { id: 1, github_id: OCTO, github_login: 'octo', display_name: 'Octo', avatar_url: null,
81
86
  handle: 'octo', previous_handle: null, handle_changed_at: null, bio: null, links: null,
82
87
  display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
83
- profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
88
+ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
84
89
  { id: 2, github_id: GHOST, github_login: 'ghost', display_name: 'Ghost', avatar_url: null,
85
90
  handle: 'ghost', previous_handle: null, handle_changed_at: null, bio: 'ghost-bio-515007', links: null,
86
91
  display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
87
- profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
92
+ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
88
93
  { id: 3, github_id: SHY, github_login: 'shy', display_name: 'Shy', avatar_url: null,
89
94
  handle: 'shy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
90
95
  display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
91
- profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
96
+ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
92
97
  { id: 4, github_id: NOSY, github_login: 'nosy', display_name: 'Nosy', avatar_url: null,
93
98
  handle: 'nosy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
94
99
  display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
95
- profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
100
+ profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
96
101
  ];
97
102
 
98
103
  // Every number and name the cross-project rollup can produce, per account.
@@ -135,6 +140,10 @@ function sentinelsOf(githubId) {
135
140
  // anything else throws — a WHERE it cannot read must never degrade to no WHERE.
136
141
  const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'handle', 'previous_handle']);
137
142
 
143
+ // Columns this interpreter can answer an IS [NOT] NULL test on — a different
144
+ // syntax from the literal comparisons above, hence its own set (ADR 0213).
145
+ const KNOWN_NULL_TESTS = new Set(['terms_accepted_at']);
146
+
138
147
  /**
139
148
  * Build a row test from a statement's own WHERE text. Literal comparisons carry
140
149
  * as much weight as placeholders here: the effective-visibility predicate is
@@ -144,8 +153,18 @@ const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stat
144
153
  function accountFilter(where, params) {
145
154
  const tests = [];
146
155
  const consumed = new Set();
156
+ // NULL-test terms actually turned into a filter, as opposed to column names
157
+ // merely recognised. The final sweep compares the two (ADR 0213).
158
+ const modelled = new Set();
147
159
 
148
160
  if (/\bprofile_state = 'active'/.test(where)) tests.push((r) => r.profile_state === 'active');
161
+ // The ADR 0213 consent half. It is a NULL test, not a literal comparison, so
162
+ // neither sweep below would see it — an unmodelled IS NOT NULL degrades to no
163
+ // filter exactly like an unread $N, which is what those sweeps exist to refuse.
164
+ if (/\bterms_accepted_at IS NOT NULL/.test(where)) {
165
+ modelled.add('terms_accepted_at');
166
+ tests.push((r) => r.terms_accepted_at != null);
167
+ }
149
168
  if (/\baccount_visibility = 'public'/.test(where)) tests.push((r) => r.account_visibility === 'public');
150
169
  if (/\bNOT (?:[a-z_]+\.)?hide_stats\b/.test(where)) tests.push((r) => !r.hide_stats);
151
170
 
@@ -173,6 +192,14 @@ function accountFilter(where, params) {
173
192
  throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
174
193
  }
175
194
  }
195
+ for (const m of where.matchAll(/\b([a-z_]+)\s+IS\s+(?:NOT\s+)?NULL\b/gi)) {
196
+ if (!KNOWN_NULL_TESTS.has(m[1])) {
197
+ throw new Error(`fake pool: unrecognised NULL test on ${m[1]} in "${where}"`);
198
+ }
199
+ if (!modelled.has(m[1])) {
200
+ throw new Error(`fake pool: NULL test on ${m[1]} in "${where}" was recognised but never MODELLED — the clause is present and this interpreter is ignoring it`);
201
+ }
202
+ }
176
203
  return (r) => tests.every((fn) => fn(r));
177
204
  }
178
205
 
@@ -354,25 +354,28 @@ const PROOF_PROJECTS = [
354
354
  const sumOf = (k) => PROOF_PROJECTS.reduce((n, p) => n + p[k], 0);
355
355
 
356
356
  // The one account row the reads consult. setAccountHideStats mutates it.
357
- const hubAccount = { profile_state: 'active', account_visibility: 'public', hide_stats: false, handle: 'r06-handle' };
357
+ // ADR 0213 (task 1002913): consent is profile_state AND an acceptance of the
358
+ // platform terms, both stamped by the same upsertAccount branch.
359
+ const ACCEPTED_AT = '2026-08-29T00:00:00.000Z';
360
+ const hubAccount = { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false, handle: 'r06-handle' };
358
361
 
359
362
  // The scouting "table": one visible builder plus the THREE that consent and
360
363
  // visibility exclude (the private row joined at task 1002967 — the same axis,
361
364
  // enforced by the same shared predicate).
362
365
  const SCOUTING_TABLE = [
363
- { profile_state: 'active', account_visibility: 'public', hide_stats: false,
366
+ { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
364
367
  row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
365
368
  project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
366
369
  last_active: '2026-08-01T00:00:00.000Z', disciplines: ['engineer'] } },
367
- { profile_state: 'active', account_visibility: 'public', hide_stats: true,
370
+ { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true,
368
371
  row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
369
372
  project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
370
373
  last_active: '2026-08-02T00:00:00.000Z', disciplines: ['ui'] } },
371
- { profile_state: 'provisional', account_visibility: 'public', hide_stats: false,
374
+ { profile_state: 'provisional', terms_accepted_at: null, account_visibility: 'public', hide_stats: false,
372
375
  row: { github_id: '13', github_login: PROV_LOGIN, display_name: 'Provisional', avatar_url: null,
373
376
  project_count: 1, total_credits: 71713, total_tasks_shipped: 1, total_karma: 0,
374
377
  last_active: '2026-08-03T00:00:00.000Z', disciplines: [] } },
375
- { profile_state: 'active', account_visibility: 'private', hide_stats: false,
378
+ { profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false,
376
379
  row: { github_id: '14', github_login: PRIVATE_LOGIN, display_name: 'Private', avatar_url: null,
377
380
  project_count: 3, total_credits: 63127, total_tasks_shipped: 4409, total_karma: 2287,
378
381
  last_active: '2026-08-04T00:00:00.000Z', disciplines: ['engineer'] } },
@@ -389,6 +392,7 @@ const engine = {
389
392
  if (/AS project_count/.test(sql)) {
390
393
  let rows = SCOUTING_TABLE;
391
394
  if (/pa\.profile_state = 'active'/.test(sql)) rows = rows.filter((r) => r.profile_state === 'active');
395
+ if (/pa\.terms_accepted_at IS NOT NULL/.test(sql)) rows = rows.filter((r) => r.terms_accepted_at != null);
392
396
  if (/pa\.account_visibility = 'public'/.test(sql)) rows = rows.filter((r) => r.account_visibility === 'public');
393
397
  if (/NOT pa\.hide_stats/.test(sql)) rows = rows.filter((r) => !r.hide_stats);
394
398
  return { rows: rows.map((r) => ({ ...r.row })) };
@@ -398,10 +402,13 @@ const engine = {
398
402
  hubAccount.hide_stats = !!params[4];
399
403
  return { rows: [{ github_id: String(HUB_GITHUB_ID), github_login: 'jaxri', ...hubAccount }] };
400
404
  }
401
- // getPublicProfileExtras' consent read — three columns since task 1002967
402
- // wired the existence axis in (the shared rollupIsVisible port reads them).
403
- if (/SELECT profile_state, account_visibility, hide_stats/.test(sql)) {
405
+ // getPublicProfileExtras' consent read. Matched on the columns it must
406
+ // carry rather than on the exact SELECT list: pinning the full list meant
407
+ // that adding terms_accepted_at (ADR 0213) silently unhooked this branch and
408
+ // the read fell through to the wrong answer with every other test green.
409
+ if (/SELECT profile_state,.*hide_stats/.test(sql) && /FROM platform_identity_accounts/.test(sql)) {
404
410
  return { rows: [{ profile_state: hubAccount.profile_state,
411
+ terms_accepted_at: hubAccount.terms_accepted_at,
405
412
  account_visibility: hubAccount.account_visibility, hide_stats: hubAccount.hide_stats }] };
406
413
  }
407
414
  // getRollupForBuilder's per-project rows (the client_id → origin two-hop).