@bongos/core 1.19.1016 → 1.19.1017
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 +78 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +6 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0334-the-recruiting-directory-lists-a-hidden-builder-by-name.md +47 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +56 -5
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +5 -3
- package/docs/copy-registry.json +22 -4
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/people.html +11 -0
- package/modules/hall-ui/public/people.js +77 -11
- package/modules/hall-ui/public/people.states.json +1 -1
- package/modules/platform-identity/account-visibility.js +33 -2
- package/modules/platform-identity/builder-search.js +153 -0
- package/modules/platform-identity/migrations/platform_identity_023_builder_search_trgm.sql +40 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/community-search.js +120 -0
- package/modules/platform-identity/routes/scouting.js +14 -7
- package/modules/platform-identity/scouting-name-only.js +47 -0
- package/modules/public-landing/public/projects-floor.states.json +9 -2
- package/modules/public-landing/public/projects.html +69 -4
- package/modules/ui-design/styles/expedition/mock.html +16 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +4 -1
- package/tests/card_apply_ui.mjs +297 -0
- package/tests/community_search_route.mjs +385 -0
- package/tests/effective_visibility_predicate.mjs +49 -1
- package/tests/people_name_only.mjs +262 -0
- package/tests/profile_route.mjs +23 -5
- package/tests/recruiter_sliver_boundary.mjs +3 -0
- package/tests/scouting_owner_access_and_reach.mjs +112 -2
- package/tests/ui_design_styles.mjs +13 -0
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**2037 strings** across 3 surfaces · 1738 distinct pieces of copy · 2037 certain, 0 heuristic · 212 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 102 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 102 | 1546 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 391 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `7926e30955d2` | 3x | ← Back to goal | modules/hall-ui/public/goals.html:199, modules/hall-ui/public/goals.html:210, modules/hall-ui/public/goals.html:222 |
|
|
79
79
|
| … | | _172 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1546 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -618,6 +618,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
618
618
|
| `a53e0cde7ff7` | certain | Link first — your rank role is set the moment you join. | `modules/hall-ui/public/settings.js:840` |
|
|
619
619
|
| `64e85f1691f9` | certain | Link your Discord account and you're added to the {{worldName}} server with your rank role already set — no in | `modules/hall-ui/public/settings.html:61` |
|
|
620
620
|
| `9a31cf5fe05b` | certain | Linking opens Discord's consent screen; unlinking applies at once. | `modules/hall-ui/public/settings.html:66` |
|
|
621
|
+
| `8e067a257eed` | certain | Listed by name only | `modules/hall-ui/public/people.html:53` |
|
|
621
622
|
| `daf84bba41b1` | certain | Loading goals… | `modules/hall-ui/public/goals.html:44` |
|
|
622
623
|
| `93ceeaa4e226` | certain | Loading idea | `modules/hall-ui/public/idea.html:32` |
|
|
623
624
|
| `2b5826012538` | certain | Loading profile… | `modules/hall-ui/public/profile.html:34` |
|
|
@@ -1114,6 +1115,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1114
1115
|
| `bcdbded93d4e` | certain | The work board failed to load. Reload to try again. | `modules/hall-ui/public/work.js:1492` |
|
|
1115
1116
|
| `25794c439321` | certain | Then copy the project onto your machine and run the one-shot bootstrap (it checks your setup, installs depende | `modules/hall-ui/public/hall-render.js:890` |
|
|
1116
1117
|
| `df316c3484de` | certain | Then drive it however you like: | `modules/hall-ui/public/hall-render.js:825` |
|
|
1118
|
+
| `48ace42c55ef` | certain | These builders are open to recruiting but keep their cross-project record private, so only who they are is sho | `modules/hall-ui/public/people.html:54` |
|
|
1117
1119
|
| `a0140098c0e0` | certain | They already have an invitation or a join-request pending on this goal. | `modules/hall-ui/public/goals-render.js:1303` |
|
|
1118
1120
|
| `5151cf635644` | certain | They have asked to connect. | `modules/hall-ui/public/profile-connections.js:203` |
|
|
1119
1121
|
| `10219b02433e` | certain | They join as a member once they accept — you can change that afterwards. Nobody is added until they do. | `modules/hall-ui/public/goals-render.js:1235` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 2037,
|
|
7
|
+
"unique_ids": 1738,
|
|
8
|
+
"certain": 2037,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 212
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 102,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1546
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -7071,6 +7071,15 @@
|
|
|
7071
7071
|
"origin": "html-text",
|
|
7072
7072
|
"confidence": "certain"
|
|
7073
7073
|
},
|
|
7074
|
+
{
|
|
7075
|
+
"id": "8e067a257eed",
|
|
7076
|
+
"surface": "builders-hall",
|
|
7077
|
+
"text": "Listed by name only",
|
|
7078
|
+
"file": "modules/hall-ui/public/people.html",
|
|
7079
|
+
"line": 53,
|
|
7080
|
+
"origin": "html-text",
|
|
7081
|
+
"confidence": "certain"
|
|
7082
|
+
},
|
|
7074
7083
|
{
|
|
7075
7084
|
"id": "daf84bba41b1",
|
|
7076
7085
|
"surface": "builders-hall",
|
|
@@ -11535,6 +11544,15 @@
|
|
|
11535
11544
|
"origin": "js-markup",
|
|
11536
11545
|
"confidence": "certain"
|
|
11537
11546
|
},
|
|
11547
|
+
{
|
|
11548
|
+
"id": "48ace42c55ef",
|
|
11549
|
+
"surface": "builders-hall",
|
|
11550
|
+
"text": "These builders are open to recruiting but keep their cross-project record private, so only who they are is shown. They are never ranked, and a role filter never finds them.",
|
|
11551
|
+
"file": "modules/hall-ui/public/people.html",
|
|
11552
|
+
"line": 54,
|
|
11553
|
+
"origin": "html-text",
|
|
11554
|
+
"confidence": "certain"
|
|
11555
|
+
},
|
|
11538
11556
|
{
|
|
11539
11557
|
"id": "a0140098c0e0",
|
|
11540
11558
|
"surface": "builders-hall",
|
package/docs/file-map.md
CHANGED
|
@@ -375,7 +375,7 @@ tests/
|
|
|
375
375
|
├── canonical_profile_url.mjs ← (task 1002804, goal 1000045 criterion C1, ADR 0214): the P08 proof — the canonical profile URL is resolved in ONE place, `handle` reaches `GET /builders/:id/profile` under the EXISTENCE predicate (so a hidden builder keeps their address), absent/provisional/private are BYTE-IDENTICAL so the numeric-id redirect cannot become an existence oracle, and the widened response still leaks no private column. Real core router over a WHERE-derived fake pool on a real socket; the real browser script in a vm above a stub DOM, run twice — as shipped (every surface dark, because `/u/<handle>` still 404s) and with the liveness constant flipped, so the three live behaviours are proved rather than deferred. Asserts its own DB-free lane membership. DB-free `unit` lane.
|
|
376
376
|
├── application_lifecycle.mjs ← (task 1002284, goal 1000045, ADR 0201 D1/D3.2/D4 + ADR 0208): the application lifecycle EXECUTED — apply, list-pending, approve, decline, RESCIND (`invited → dismissed`), idempotency on the state already held, and the two things that must never happen: a builder without `access_request.review` moving the queue, and a VOUCH buying admission. Real router, REAL `requirePermission` resolving against the real government catalog's rank seed, and the REAL `hasInvitedAccessRequest` from `src/bongos/db-kernel.js` pointed at the same in-memory `access_requests` table (a stubbed `./pool`), whose filter is built from each statement's own WHERE clause and which refuses a predicate it cannot read. The no-authority proof seeds a vouched row and asserts the approve path and the admission read are byte-identical to the unvouched twin, and that no statement on either path names `applicant_github_id`/`vouched_at`. DB-free `unit` lane.
|
|
377
377
|
├── application_echo.mjs ← (task 1002284, goal 1000045, ADR 0201 D1.3/D1.4): the hub's STATUSLESS echo of an application. Part A pins the domain functions — the read binds `APPLICATION_ECHO_WINDOW_DAYS` itself (one 30, not three), the write is an upsert on the pair, neither statement mentions a status, and a non-numeric principal is refused before any query (ADR 0171 D2). Part B runs the real `POST /my-projects/join` and `GET /my-projects` over an in-memory `platform_identity_application_echoes` table with the relay's answer scripted: only an accepted application leaves a trace — a standing membership, either door refusal, a members-only project and an unreachable project leave none — and best-effort is asserted in both directions (a failed echo write still reports the filed application; a failed echo read still lands the memberships listing). DB-free `unit` lane.
|
|
378
|
-
├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting` and the hall's `/people` (the same endpoint), returns when it opts back in, and the opt-out grants and denies no permission — an opted-out owner still reads the directory while being absent from their own copy of it. Fake pool filtered by each statement's own WHERE clause, real route, real `requirePermission` over the real government catalog, real HTTP. Mutation-checked
|
|
378
|
+
├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting` and the hall's `/people` (the same endpoint), returns when it opts back in, and the opt-out grants and denies no permission — an opted-out owner still reads the directory while being absent from their own copy of it. Since task 1002291 (R15, ADR 0334) it also proves the directory's NAME-ONLY tier: a hidden, reachable builder is listed by login, name and avatar alone, never ranked, and none of their record reaches the body. Fake pool filtered by each statement's own WHERE clause, real route, real `requirePermission` over the real government catalog, real HTTP. Mutation-checked five ways. DB-free `unit` lane. The page half is `people_name_only.mjs` (the real people.js + kit in a vm).
|
|
379
379
|
├── scouting_page_gate.mjs ← (task 1002290, goal 1000045 C5, ADR 0210 D3): the OTHER half of R14 — the `/builders/people` SHELL must admit exactly the two audiences `GET /scouting` admits. A project owner holding no permission reaches it; everyone the route refuses gets the byte-identical `?denied=project.curate` redirect; and each of six ways the `alsoAdmits` door can fail (module absent, function absent, throw, reject, non-boolean, prototype-only) fails CLOSED. Also pins that the other four page gates never consult the seam at all, and that exactly ONE gate declares a second door. Its own file because the route proof's ESM imports pull `auth.js` in before a session double could be installed — this one installs it first, the `government_page_gates.mjs` pattern. DB-free `unit` lane.
|
|
380
380
|
├── terms_acceptance.mjs ← (task 1002913, goal 1000045, ADR 0213): the platform T&C acceptance record. Proves the four properties that make it evidence rather than a claim — the /terms page carries the cross-project consent ADR 0141 §4 deferred to it and it is apex-only; acceptance is written ONLY by the hub's own /sso/authorize (driven over the real router with an attacker-controlled query string, so a forged timestamp/version/activate flag reaches nothing); it is written ONCE (the fake pool interprets the upsert's own column assignments, so COALESCE is executed, not string-matched, and an assignment shape it cannot read throws instead of reading as "unchanged"); and consent is two columns in the shared predicate, so an account the hub witnessed before the terms existed publishes nothing. Plus the projection rule — every read whose row a JS twin judges must select both consent columns, or the twin denies everyone silently. DB-free `unit` lane, asserts its own lane membership.
|
|
381
381
|
├── applicant_profile_boundary.mjs ← (task 1002972, goal 1000045 C4, privacy spec D7, ADR 0241): THE D7 BOUNDARY proof for the applicant view a reviewer sees. Whitelists read from the MODULES’ own exports — the private branch against `recruiter-sliver`’s `SLIVER_KEYS`/`SLIVER_KEYS_HIDDEN`, the public branch against the public-page payload’s key list — so a third shape (the “new disclosure class” D7 forbids) reds this file. Pins THE CONSENT SWAP in both directions on the same account: a private builder who never opted into recruiting gets an applicant view and gets `null` from `getRecruiterSliver`, so a refactor routing the port through the recruiter sliver cannot pass quietly. `hide_stats` beats the application in both branches; provisional and terms-never-accepted disclose nothing; planted sentinels asserted absent from the whole body with the avatar-embedded `github_id` pinned as the one honest exception. Plus the federated gate (ADR 0205): no echo → 404 and NO account row read (asserted on the statement log), the witness matched against the AUTHENTICATED client, an out-of-window echo stops working, and `Cache-Control: no-store`. Real router + real HTTP over in-memory tables filtered by each statement’s own WHERE clause. DB-free `unit` lane, asserted in the file itself.
|
|
@@ -2506,5 +2506,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2506
2506
|
landed since 1.19.1014 with no explicit bump. run 36078624472. (task 1002620)
|
|
2507
2507
|
1.19.1016 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2508
2508
|
landed since 1.19.1015 with no explicit bump. run 36080388706. (task 1002620)
|
|
2509
|
+
1.19.1017 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2510
|
+
landed since 1.19.1016 with no explicit bump. run 36081753087. (task 1002620)
|
|
2509
2511
|
---------------------------------------------------------------------------
|
|
2510
2512
|
```
|
|
@@ -44,6 +44,17 @@
|
|
|
44
44
|
rollup offers; people.js writes it. -->
|
|
45
45
|
<p class="ledger__note" id="people-foot" hidden></p>
|
|
46
46
|
</section>
|
|
47
|
+
|
|
48
|
+
<!-- task 1002291 (ADR 0334): builders who hid their cross-project record
|
|
49
|
+
but are still open to recruiting, listed by name alone (privacy spec
|
|
50
|
+
D2). Its own panel because they are never ranked; hidden unless the
|
|
51
|
+
response carries a row, and people.js writes it. -->
|
|
52
|
+
<section class="scroll" id="people-named-scroll" aria-labelledby="people-named-h" hidden>
|
|
53
|
+
<h2 id="people-named-h" class="scroll__h">Listed by name only<span class="count-pill" id="people-named-count" hidden></span></h2>
|
|
54
|
+
<p class="scroll__lede" id="people-named-lede">These builders are open to recruiting but keep their cross-project record private, so only who they are is shown. They are never ranked, and a role filter never finds them.</p>
|
|
55
|
+
<div id="people-named-body"></div>
|
|
56
|
+
<p class="ledger__note" id="people-named-foot" hidden></p>
|
|
57
|
+
</section>
|
|
47
58
|
</main>
|
|
48
59
|
|
|
49
60
|
<footer class="app-foot" id="app-foot"></footer>
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
// exists); then the four figures in tabular columns, the crafts as chips, and
|
|
17
17
|
// when they were last seen. Search, a sort and a craft lens are the kit's
|
|
18
18
|
// filter bar; the rows page through the kit's pager.
|
|
19
|
+
//
|
|
20
|
+
// THE SECOND TIER (task 1002291, ADR 0334). The response also carries
|
|
21
|
+
// `name_only`: builders who hid their cross-project record but are still open to
|
|
22
|
+
// recruiting, which privacy spec D2 lists "name-only". They get their own panel
|
|
23
|
+
// of identity cells because they have no record to rank, sort or filter by role:
|
|
24
|
+
// the lenses below leave them alphabetical, a search reads their name, and a
|
|
25
|
+
// role filter never matches them.
|
|
19
26
|
// All network values render through OTBKit.esc — never raw innerHTML.
|
|
20
27
|
(function () {
|
|
21
28
|
'use strict';
|
|
@@ -26,6 +33,10 @@
|
|
|
26
33
|
var sub = document.getElementById('people-sub');
|
|
27
34
|
var countPill = document.getElementById('people-count');
|
|
28
35
|
var foot = document.getElementById('people-foot');
|
|
36
|
+
var namedScroll = document.getElementById('people-named-scroll');
|
|
37
|
+
var namedCount = document.getElementById('people-named-count');
|
|
38
|
+
var namedBody = document.getElementById('people-named-body');
|
|
39
|
+
var namedFoot = document.getElementById('people-named-foot');
|
|
29
40
|
|
|
30
41
|
function fmtRecency(iso) {
|
|
31
42
|
if (!iso) return '—';
|
|
@@ -97,6 +108,7 @@
|
|
|
97
108
|
var DIRECTORY_LIMIT = 500;
|
|
98
109
|
|
|
99
110
|
var all = [];
|
|
111
|
+
var named = [];
|
|
100
112
|
var query = '';
|
|
101
113
|
var craft = '';
|
|
102
114
|
var sortBy = '';
|
|
@@ -108,6 +120,17 @@
|
|
|
108
120
|
(Array.isArray(b.disciplines) ? b.disciplines.join(' ') : '')).toLowerCase();
|
|
109
121
|
return hay.indexOf(query) !== -1;
|
|
110
122
|
}
|
|
123
|
+
// A name-only row carries no roles (they are part of the hidden record), so a
|
|
124
|
+
// role filter never matches it; a search reads the name.
|
|
125
|
+
function matchesNamed(b) {
|
|
126
|
+
if (craft) return false;
|
|
127
|
+
if (!query) return true;
|
|
128
|
+
return ((b.github_login || '') + ' ' + (b.display_name || '')).toLowerCase().indexOf(query) !== -1;
|
|
129
|
+
}
|
|
130
|
+
// A count that cannot overstate a clipped read: a full response means "at least".
|
|
131
|
+
function countText(n) {
|
|
132
|
+
return n >= DIRECTORY_LIMIT ? fmtInt(DIRECTORY_LIMIT) + '+' : fmtInt(n);
|
|
133
|
+
}
|
|
111
134
|
// The server's order IS "track record" (ADR 0141 §4 ranks the rollup); the
|
|
112
135
|
// other lenses re-order the loaded set client-side, ties keeping that order.
|
|
113
136
|
var SORTS = {
|
|
@@ -128,6 +151,8 @@
|
|
|
128
151
|
|
|
129
152
|
var pager = kit.makePager('people', 'hall-people-per-page');
|
|
130
153
|
var ledger = null;
|
|
154
|
+
var namedPager = kit.makePager('people-named', 'hall-people-named-per-page');
|
|
155
|
+
var namedLedger = null;
|
|
131
156
|
|
|
132
157
|
function rowHtml(b) {
|
|
133
158
|
return '<tr>' +
|
|
@@ -141,12 +166,45 @@
|
|
|
141
166
|
'</tr>';
|
|
142
167
|
}
|
|
143
168
|
|
|
169
|
+
// The identity cell and nothing else: every other column is the record hidden.
|
|
170
|
+
function namedRowHtml(b) {
|
|
171
|
+
return '<tr><td>' + whoHtml(b) + '</td></tr>';
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
function rerender() {
|
|
145
175
|
if (ledger) ledger.render(ordered(all.filter(matches)));
|
|
176
|
+
if (namedLedger) namedLedger.render(named.filter(matchesNamed));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function mountNamed() {
|
|
180
|
+
if (!namedScroll || !named.length) return;
|
|
181
|
+
namedScroll.hidden = false;
|
|
182
|
+
if (namedCount) {
|
|
183
|
+
namedCount.textContent = countText(named.length);
|
|
184
|
+
namedCount.hidden = false;
|
|
185
|
+
}
|
|
186
|
+
// The ranked footnote's rule, for the one lens that reads this list: a full
|
|
187
|
+
// response is clipped, and a search cannot find a builder it never loaded.
|
|
188
|
+
var namedClipped = named.length >= DIRECTORY_LIMIT;
|
|
189
|
+
if (namedFoot) {
|
|
190
|
+
namedFoot.textContent = namedClipped
|
|
191
|
+
? 'This list is clipped at the first ' + fmtInt(DIRECTORY_LIMIT) + ' by login. Search only reads what this page loaded, so a builder past that ' + fmtInt(DIRECTORY_LIMIT) + ' will not appear in it.'
|
|
192
|
+
: '';
|
|
193
|
+
namedFoot.hidden = !namedClipped;
|
|
194
|
+
}
|
|
195
|
+
namedLedger = kit.makeLedger({
|
|
196
|
+
mount: namedBody,
|
|
197
|
+
columns: ['Builder'],
|
|
198
|
+
rowHtml: namedRowHtml,
|
|
199
|
+
emptyMsg: 'No builder listed by name only matches these filters.',
|
|
200
|
+
pager: namedPager,
|
|
201
|
+
});
|
|
146
202
|
}
|
|
147
203
|
|
|
148
|
-
function mountDirectory(builders) {
|
|
204
|
+
function mountDirectory(builders, nameOnly) {
|
|
149
205
|
all = Array.isArray(builders) ? builders : [];
|
|
206
|
+
named = Array.isArray(nameOnly) ? nameOnly : [];
|
|
207
|
+
if (namedScroll) namedScroll.hidden = true;
|
|
150
208
|
// An EMPTY directory is almost never missing data — it is ADR 0171's
|
|
151
209
|
// fail-closed consent boundary doing exactly what it was ratified to do.
|
|
152
210
|
// `listScoutingRollup` filters on `profile_state = 'active'`, every account
|
|
@@ -160,7 +218,7 @@
|
|
|
160
218
|
// leaderboard; the accounts simply are not publicly listed — and that wrong
|
|
161
219
|
// explanation is why an empty directory got filed as a bug (task 1002937).
|
|
162
220
|
// Say which of the two it is, so the page is never mistaken for broken.
|
|
163
|
-
if (!all.length) {
|
|
221
|
+
if (!all.length && !named.length) {
|
|
164
222
|
notice('Nobody is publicly listed here yet. A builder joins this directory once they have '
|
|
165
223
|
+ 'signed in on the main platform site at least once — accounts stay unlisted until then, '
|
|
166
224
|
+ 'and none are listed without it. Their work still shows on each project’s own ledger.');
|
|
@@ -169,16 +227,21 @@
|
|
|
169
227
|
// A FULL PAGE IS A CLIPPED PAGE. Every sentence below has to hedge when this
|
|
170
228
|
// is true, because each of them otherwise states a total the page cannot know.
|
|
171
229
|
var clipped = all.length >= DIRECTORY_LIMIT;
|
|
230
|
+
// The second tier, named in the same sentence so the ranked count is never
|
|
231
|
+
// read as everyone the directory lists.
|
|
232
|
+
var namedPart = named.length ? '; ' + countText(named.length) + ' more by name only' : '';
|
|
172
233
|
if (countPill) {
|
|
173
234
|
countPill.textContent = clipped ? fmtInt(DIRECTORY_LIMIT) + '+' : String(all.length);
|
|
174
235
|
countPill.hidden = false;
|
|
175
236
|
}
|
|
176
237
|
if (sub) {
|
|
177
|
-
|
|
178
|
-
? 'Showing the first ' + fmtInt(DIRECTORY_LIMIT) + ' builders across hosted projects, ranked by track record — there are more than this page can load
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
238
|
+
var listed = clipped
|
|
239
|
+
? 'Showing the first ' + fmtInt(DIRECTORY_LIMIT) + ' builders across hosted projects, ranked by track record — there are more than this page can load' + namedPart
|
|
240
|
+
: all.length
|
|
241
|
+
? fmtInt(all.length) + ' builder' + (all.length === 1 ? '' : 's') + ' listed across hosted projects, ranked by track record' + namedPart
|
|
242
|
+
// Nobody ranked yet: "0 builders ranked; 2 more by name only" would read as a count of nothing.
|
|
243
|
+
: countText(named.length) + ' builder' + (named.length === 1 ? '' : 's') + ' listed across hosted projects by name only';
|
|
244
|
+
sub.textContent = listed + '. Descriptive only — cross-project numbers never carry authority here.';
|
|
182
245
|
}
|
|
183
246
|
// THE LENSES LIE HARDER THAN THE COUNT DOES, and that is worth its own
|
|
184
247
|
// sentence. Search, sort and the craft filter all re-order the set this page
|
|
@@ -214,6 +277,7 @@
|
|
|
214
277
|
craft = String(vals['people-craft'] || '');
|
|
215
278
|
sortBy = String(vals['people-sort'] || '');
|
|
216
279
|
pager.page = 1;
|
|
280
|
+
namedPager.page = 1;
|
|
217
281
|
rerender();
|
|
218
282
|
},
|
|
219
283
|
});
|
|
@@ -224,11 +288,13 @@
|
|
|
224
288
|
columns: ['Builder', { label: 'Projects', class: 'ledger__num' }, { label: 'Ships', class: 'ledger__num' },
|
|
225
289
|
{ label: currencyLabel(), class: 'ledger__num' }, { label: 'Karma', class: 'ledger__num' }, `${window.OTB.roleAxisLabel()}s`, 'Last active'],
|
|
226
290
|
rowHtml: rowHtml,
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
291
|
+
// With ranked builders, reached only when the lenses excluded them all;
|
|
292
|
+
// without, the directory lists hidden builders alone (the panel below) —
|
|
293
|
+
// the fully empty case returns above with its own explanation.
|
|
294
|
+
emptyMsg: all.length ? 'No listed builder matches that search.' : 'No builder with a public track record is listed yet.',
|
|
230
295
|
pager: pager,
|
|
231
296
|
});
|
|
297
|
+
mountNamed();
|
|
232
298
|
rerender();
|
|
233
299
|
}
|
|
234
300
|
|
|
@@ -244,7 +310,7 @@
|
|
|
244
310
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
245
311
|
return r.json();
|
|
246
312
|
})
|
|
247
|
-
.then(function (data) { mountDirectory(data && data.builders); })
|
|
313
|
+
.then(function (data) { mountDirectory(data && data.builders, data && data.name_only); })
|
|
248
314
|
.catch(function (e) {
|
|
249
315
|
// The floor is the pack's label for metic, never a hardcoded rank name —
|
|
250
316
|
// and the page used to say "Archon-only", which was wrong since ADR 0210.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "People — the cross-project directory behind requireProjectCuratePage (metic+ via project.curate, or a project owner). Descriptive only; it grants no authority, and the page says so above the fold. Written with the re-skin (task 1003307); the redesign (task 1003470) added the identity cell, the sort and craft lenses, and the `search` state. Its legacy door /scouting was deleted in task 1003440; the URL 301s here. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/scouting.json carries five builders, three with avatars).",
|
|
2
|
+
"_": "People — the cross-project directory behind requireProjectCuratePage (metic+ via project.curate, or a project owner). Descriptive only; it grants no authority, and the page says so above the fold. Written with the re-skin (task 1003307); the redesign (task 1003470) added the identity cell, the sort and craft lenses, and the `search` state. Its legacy door /scouting was deleted in task 1003440; the URL 301s here. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/scouting.json carries five builders, three with avatars, plus two listed by name only — task 1002291 added that second panel; the `search` state shows its empty case).",
|
|
3
3
|
"page": "/builders/people",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": {
|
|
@@ -32,8 +32,13 @@
|
|
|
32
32
|
// recruiterReachSql / isRecruiterDiscoverable REACH — the COALESCE, privacy
|
|
33
33
|
// spec D1. A reach predicate only: it never buys addressability.
|
|
34
34
|
// scoutingListableSql THE DIRECTORY — numbers AND reach,
|
|
35
|
-
// the
|
|
36
|
-
// 1002290). Composed, never restated.
|
|
35
|
+
// the predicate the recruiting directory's ranked tier joins (ADR 0210,
|
|
36
|
+
// task 1002290). Composed, never restated.
|
|
37
|
+
// scoutingNameOnlySql THE DIRECTORY'S NAME-ONLY TIER —
|
|
38
|
+
// existence AND hidden AND reach (ADR 0334, task 1002291; privacy spec D2:
|
|
39
|
+
// a hidden builder is "listed name-only (if not opted out)"). The same
|
|
40
|
+
// predicate as the one above with the hide term flipped, so the two tiers
|
|
41
|
+
// partition the directory: hide decides a row's SHAPE, reach its presence.
|
|
37
42
|
// recruiterSliverVisibleSql / isRecruiterSliverVisible THE PRE-ACCEPT
|
|
38
43
|
// SLIVER — consent AND reach, WITHOUT the existence axis (ADR 0215, task
|
|
39
44
|
// 1002968; privacy spec D3). The one predicate that deliberately does not
|
|
@@ -205,6 +210,31 @@ function scoutingListableSql(alias) {
|
|
|
205
210
|
return `(${rollupVisibleSql(alias)} AND ${recruiterReachSql(alias)})`;
|
|
206
211
|
}
|
|
207
212
|
|
|
213
|
+
// scoutingNameOnlySql — WHO the recruiting directory lists by NAME ALONE: a
|
|
214
|
+
// publicly visible, reachable account that hid its cross-project record (ADR
|
|
215
|
+
// 0334, task 1002291). Privacy spec D2's recruiter column lists that account
|
|
216
|
+
// "name-only (if not opted out)"; before this the directory dropped it outright,
|
|
217
|
+
// reading hide as a second opt-out while Settings told the builder recruiters
|
|
218
|
+
// would see their name and picture.
|
|
219
|
+
//
|
|
220
|
+
// It is scoutingListableSql with the hide term FLIPPED and nothing else, shaped
|
|
221
|
+
// identically so the two tiers cannot overlap or leave a gap
|
|
222
|
+
// (tests/effective_visibility_predicate.mjs holds that). It keeps the EXISTENCE
|
|
223
|
+
// floor, so a private account is never listed even by name — that is the D3
|
|
224
|
+
// sliver's subject, at the archon floor (ADR 0215/0217).
|
|
225
|
+
//
|
|
226
|
+
// DANGEROUS OUTSIDE ITS ONE READER. It admits hidden accounts by design, so a
|
|
227
|
+
// read that joined it and returned a number would hand out exactly the record
|
|
228
|
+
// hide_stats withholds. scouting-name-only.js is its only reader and selects
|
|
229
|
+
// three identity columns; the guard pins both.
|
|
230
|
+
function scoutingNameOnlySql(alias) {
|
|
231
|
+
if (alias && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(alias))) {
|
|
232
|
+
throw new Error('scoutingNameOnlySql: alias must be a bare SQL identifier');
|
|
233
|
+
}
|
|
234
|
+
const q = alias ? `${alias}.` : '';
|
|
235
|
+
return `((${accountVisibleSql(alias)} AND ${q}hide_stats) AND ${recruiterReachSql(alias)})`;
|
|
236
|
+
}
|
|
237
|
+
|
|
208
238
|
// recruiterSliverVisibleSql — THE predicate the PRE-ACCEPT recruiter sliver
|
|
209
239
|
// joins: the CONSENT floor AND the reach axis (ADR 0215, privacy spec D3; task
|
|
210
240
|
// 1002968). modules/platform-identity/recruiter-sliver.js is its one reader.
|
|
@@ -256,6 +286,7 @@ module.exports = {
|
|
|
256
286
|
isRecruiterDiscoverable,
|
|
257
287
|
recruiterReachSql,
|
|
258
288
|
scoutingListableSql,
|
|
289
|
+
scoutingNameOnlySql,
|
|
259
290
|
recruiterSliverVisibleSql,
|
|
260
291
|
isRecruiterSliverVisible,
|
|
261
292
|
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// modules/platform-identity/builder-search.js — the PUBLIC builder search (D4,
|
|
2
|
+
// docs/specs/bongos-v1-community-tab.md; task 1002979, goal 1000110 CT3). Answers
|
|
3
|
+
// "find a builder by name, optionally narrowed to a discipline" for the future
|
|
4
|
+
// /community page (tasks 1002980 / 1003363) — NOT the recruiting directory
|
|
5
|
+
// (routes/scouting.js), which stays a separate, project.curate/owner-gated
|
|
6
|
+
// surface with its own consent predicate, and NOT its archon-floor pre-accept
|
|
7
|
+
// sliver (recruiter-sliver.js). This surface reveals nothing beyond the same
|
|
8
|
+
// public-profile rules /profiles/:handle already applies (D8: no new disclosure
|
|
9
|
+
// class).
|
|
10
|
+
//
|
|
11
|
+
// TWO PREDICATES FROM account-visibility.js, ONE PER AXIS D4 NAMES:
|
|
12
|
+
// NAME MATCH — accountVisibleSql (existence: active + public). A
|
|
13
|
+
// hide_stats account is still findable BY NAME — hiding removes the
|
|
14
|
+
// rollup, not the page (the same rule /profiles/:handle already honors).
|
|
15
|
+
// DISCIPLINE MATCH — rollupVisibleSql (existence AND NOT hide_stats),
|
|
16
|
+
// because a discipline is ROLLUP-DERIVED
|
|
17
|
+
// (platform_identity_builder_project_activity.disciplines) and the rollup
|
|
18
|
+
// is exactly what hide_stats suppresses. A hidden account's disciplines are
|
|
19
|
+
// therefore never attached and never filterable — which is what keeps a
|
|
20
|
+
// discipline facet from re-exposing a hidden account's rollup by a side
|
|
21
|
+
// door. Private and provisional accounts never reach either predicate, so
|
|
22
|
+
// they are absent from every result regardless of query or filter.
|
|
23
|
+
// Both predicates are IMPORTED, never re-derived — the sole-owner rule
|
|
24
|
+
// tests/effective_visibility_predicate.mjs enforces.
|
|
25
|
+
//
|
|
26
|
+
// Matching: handle / display_name PREFIX (escaped LIKE) OR pg_trgm fuzzy (the
|
|
27
|
+
// `%` similarity operator) — the two legs D4 asks for. BOTH legs compare
|
|
28
|
+
// lower(column) against lower($1)/lower($2) — the `%` operator is
|
|
29
|
+
// case-SENSITIVE (unlike ILIKE, pg_trgm has no case-insensitive variant), so
|
|
30
|
+
// without the explicit lower() a query differing only in case would silently
|
|
31
|
+
// miss the fuzzy leg while the `sim` column (already lower()-folded) still
|
|
32
|
+
// scored it. Indexed by migration platform_identity_023_builder_search_trgm.sql
|
|
33
|
+
// — a GIN gin_trgm_ops index on lower(handle) and lower(display_name), which
|
|
34
|
+
// accelerates both legs since both now query that same expression.
|
|
35
|
+
// github_id never crosses the projection — handle is the address, exactly as
|
|
36
|
+
// every sibling public read in this module already treats it.
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const { pool: defaultPool } = require('../../src/module-api');
|
|
40
|
+
const { accountVisibleSql, rollupVisibleSql } = require('./account-visibility');
|
|
41
|
+
|
|
42
|
+
const DEFAULT_LIMIT = 20;
|
|
43
|
+
const MAX_LIMIT = 50;
|
|
44
|
+
// Deep OFFSET pagination on an unauthenticated route is a free scan-and-discard
|
|
45
|
+
// knob (?offset=999999999 still walks and throws away every matched row before
|
|
46
|
+
// the LIMIT) — capped so a caller can page a reasonable distance and no further.
|
|
47
|
+
const MAX_OFFSET = 1000;
|
|
48
|
+
// A 1-character query has no usable trigram and a near-unselective LIKE
|
|
49
|
+
// prefix, so it would materialize and sort a large share of the table before
|
|
50
|
+
// LIMIT on every anonymous request. 2 chars is the floor at which a trigram
|
|
51
|
+
// exists at all.
|
|
52
|
+
const MIN_QUERY_CHARS = 2;
|
|
53
|
+
const MAX_QUERY_CHARS = 100;
|
|
54
|
+
const MAX_DISCIPLINE_CHARS = 32; // mirrors upsertActivity's per-discipline cap
|
|
55
|
+
|
|
56
|
+
function clampLimit(limit) {
|
|
57
|
+
const n = parseInt(limit, 10);
|
|
58
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT;
|
|
59
|
+
return Math.min(n, MAX_LIMIT);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clampOffset(offset) {
|
|
63
|
+
const n = parseInt(offset, 10);
|
|
64
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
65
|
+
return Math.min(n, MAX_OFFSET);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Escape LIKE's own wildcard characters so a searcher's literal % or _ can't
|
|
69
|
+
// turn a prefix match into an unbounded pattern.
|
|
70
|
+
function escapeLikePrefix(s) {
|
|
71
|
+
return s.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// searchBuilders — the one read behind GET /community/search. `q` is required
|
|
75
|
+
// (too short/empty/oversized → no results, never a throw — the route
|
|
76
|
+
// validates and 400s before calling this; this stays defensive on its own).
|
|
77
|
+
// `discipline` is optional; '' means no filter. Returns
|
|
78
|
+
// { results, limit, offset } — `limit`/`offset` are the CLAMPED values
|
|
79
|
+
// actually used, so a caller (the route) never re-derives them to echo back.
|
|
80
|
+
// A result's `disciplines` is null for a hidden account or one with no
|
|
81
|
+
// rollup activity.
|
|
82
|
+
async function searchBuilders({ q, discipline, limit, offset, pool = defaultPool } = {}) {
|
|
83
|
+
const query = typeof q === 'string' ? q.trim() : '';
|
|
84
|
+
const lim = clampLimit(limit);
|
|
85
|
+
const off = clampOffset(offset);
|
|
86
|
+
if (query.length < MIN_QUERY_CHARS || query.length > MAX_QUERY_CHARS) {
|
|
87
|
+
return { results: [], limit: lim, offset: off };
|
|
88
|
+
}
|
|
89
|
+
const disc = typeof discipline === 'string' ? discipline.trim().slice(0, MAX_DISCIPLINE_CHARS) : '';
|
|
90
|
+
|
|
91
|
+
const { rows } = await pool.query(
|
|
92
|
+
`WITH matches AS (
|
|
93
|
+
SELECT pa.github_id, pa.handle, pa.github_login, pa.display_name, pa.avatar_url,
|
|
94
|
+
(lower(pa.handle) = lower($1)) AS exact_handle,
|
|
95
|
+
GREATEST(similarity(lower(pa.handle), lower($1)),
|
|
96
|
+
similarity(lower(COALESCE(pa.display_name, '')), lower($1))) AS sim
|
|
97
|
+
FROM platform_identity_accounts pa
|
|
98
|
+
WHERE ${accountVisibleSql('pa')}
|
|
99
|
+
AND (
|
|
100
|
+
lower(pa.handle) LIKE lower($2) ESCAPE '\\'
|
|
101
|
+
OR lower(pa.display_name) LIKE lower($2) ESCAPE '\\'
|
|
102
|
+
OR lower(pa.handle) % lower($1)
|
|
103
|
+
OR lower(pa.display_name) % lower($1)
|
|
104
|
+
)
|
|
105
|
+
), disc AS (
|
|
106
|
+
-- Scoped to the name matches only (never the whole activity table),
|
|
107
|
+
-- which is strictly smaller — though 'matches' itself is bounded only
|
|
108
|
+
-- by MIN_QUERY_CHARS' selectivity, not by a hard cap, since the
|
|
109
|
+
-- discipline filter (below) needs every match's disciplines before
|
|
110
|
+
-- LIMIT can apply.
|
|
111
|
+
SELECT x.github_id, array_agg(DISTINCT x.d) AS disciplines
|
|
112
|
+
FROM (SELECT a.github_id, unnest(a.disciplines) AS d
|
|
113
|
+
FROM platform_identity_builder_project_activity a
|
|
114
|
+
WHERE a.github_id IN (SELECT github_id FROM matches)
|
|
115
|
+
AND EXISTS (
|
|
116
|
+
SELECT 1 FROM platform_identity_accounts pa2
|
|
117
|
+
WHERE pa2.github_id = a.github_id AND ${rollupVisibleSql('pa2')})) x
|
|
118
|
+
WHERE x.d IS NOT NULL
|
|
119
|
+
GROUP BY x.github_id
|
|
120
|
+
)
|
|
121
|
+
SELECT m.handle, m.github_login, m.display_name, m.avatar_url, d.disciplines
|
|
122
|
+
FROM matches m
|
|
123
|
+
LEFT JOIN disc d ON d.github_id = m.github_id
|
|
124
|
+
WHERE ($3::text = '' OR d.disciplines @> ARRAY[$3]::text[])
|
|
125
|
+
ORDER BY m.exact_handle DESC, m.sim DESC, m.handle
|
|
126
|
+
LIMIT $4 OFFSET $5`,
|
|
127
|
+
[query, `${escapeLikePrefix(query)}%`, disc, lim, off],
|
|
128
|
+
);
|
|
129
|
+
return {
|
|
130
|
+
results: rows.map((r) => ({
|
|
131
|
+
handle: r.handle,
|
|
132
|
+
github_login: r.github_login,
|
|
133
|
+
display_name: r.display_name,
|
|
134
|
+
avatar_url: r.avatar_url,
|
|
135
|
+
disciplines: Array.isArray(r.disciplines) ? r.disciplines : null,
|
|
136
|
+
})),
|
|
137
|
+
limit: lim,
|
|
138
|
+
offset: off,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
searchBuilders,
|
|
144
|
+
clampLimit,
|
|
145
|
+
clampOffset,
|
|
146
|
+
escapeLikePrefix,
|
|
147
|
+
DEFAULT_LIMIT,
|
|
148
|
+
MAX_LIMIT,
|
|
149
|
+
MAX_OFFSET,
|
|
150
|
+
MIN_QUERY_CHARS,
|
|
151
|
+
MAX_QUERY_CHARS,
|
|
152
|
+
MAX_DISCIPLINE_CHARS,
|
|
153
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- platform_identity_023_builder_search_trgm.sql — GIN trigram indexes for the
|
|
2
|
+
-- public builder search (D4, docs/specs/bongos-v1-community-tab.md; task
|
|
3
|
+
-- 1002979). builder-search.js's searchBuilders() queries
|
|
4
|
+
-- lower(handle) LIKE lower($2) OR lower(handle) % lower($1) (and the same
|
|
5
|
+
-- pair for display_name) against platform_identity_accounts on EVERY request
|
|
6
|
+
-- to an UNAUTHENTICATED, rate-limited-but-public route — an unindexed
|
|
7
|
+
-- sequential scan there is a cost every anonymous caller can trigger.
|
|
8
|
+
--
|
|
9
|
+
-- INDEXED ON THE EXPRESSION, NOT THE BARE COLUMN. Both query legs compare
|
|
10
|
+
-- lower(handle)/lower(display_name), so a plain gin_trgm_ops index on the raw
|
|
11
|
+
-- column would never be used — Postgres only matches a functional index
|
|
12
|
+
-- against the identical expression. One index per column then serves BOTH
|
|
13
|
+
-- legs (prefix LIKE and % fuzzy), because pg_trgm's GIN opclass supports LIKE
|
|
14
|
+
-- as well as the trigram similarity operators.
|
|
15
|
+
--
|
|
16
|
+
-- pg_trgm itself is already enabled repo-wide by migrations/103_search_chunks.sql
|
|
17
|
+
-- (a core migration, so it runs before any module migration) — nothing to
|
|
18
|
+
-- create here.
|
|
19
|
+
--
|
|
20
|
+
-- WHY NOT CONCURRENTLY. Same reasoning core_222 and modules/ideas/migrations/-
|
|
21
|
+
-- ideas_004_lineage_route_index.sql recorded for their own indexes: CREATE
|
|
22
|
+
-- INDEX CONCURRENTLY cannot run inside a transaction block, and this file is
|
|
23
|
+
-- wrapped BEGIN/COMMIT so it lands atomically. Acceptable here specifically
|
|
24
|
+
-- because platform_identity_accounts is a hub-only, human-signup-paced table
|
|
25
|
+
-- (one row per platform account) — the build is a light, one-time lock against
|
|
26
|
+
-- a small table, not a write-hot one.
|
|
27
|
+
--
|
|
28
|
+
-- Additive + platform_identity_-namespaced (ADR 0083 §5). Forward-safe: index
|
|
29
|
+
-- creation only, no column/type change, so the previous release runs
|
|
30
|
+
-- unmodified against this schema. Idempotent (IF NOT EXISTS).
|
|
31
|
+
|
|
32
|
+
BEGIN;
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS platform_identity_accounts_handle_trgm
|
|
35
|
+
ON platform_identity_accounts USING gin (lower(handle) gin_trgm_ops);
|
|
36
|
+
|
|
37
|
+
CREATE INDEX IF NOT EXISTS platform_identity_accounts_display_name_trgm
|
|
38
|
+
ON platform_identity_accounts USING gin (lower(display_name) gin_trgm_ops);
|
|
39
|
+
|
|
40
|
+
COMMIT;
|
|
@@ -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"],
|
|
13
|
+
"routes": ["sso", "projects", "my-projects", "scouting", "profile", "public-profile", "connections", "community-search"],
|
|
14
14
|
"migrations": true,
|
|
15
15
|
"pollers": ["code-sweep", "visibility-pull"]
|
|
16
16
|
},
|