@bongos/core 1.19.1016 → 1.19.1018
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 +98 -48
- 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/0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md +1 -0
- package/docs/adr/0333-the-application-vouch-is-a-third-token-spent-once-and-bound-to-its-login.md +86 -0
- package/docs/adr/0334-the-recruiting-directory-lists-a-hidden-builder-by-name.md +47 -0
- package/docs/adr/README.md +2 -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 +9 -0
- package/migrations/core_254_access_requests_applicant_handle.sql +23 -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/onboarding/routes/access-requests.js +31 -4
- package/modules/platform-identity/account-visibility.js +33 -2
- package/modules/platform-identity/builder-search.js +153 -0
- package/modules/platform-identity/hub-keys.js +51 -32
- 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/my-projects.js +33 -1
- 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/bongos/auth-idp.js +93 -48
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +7 -1
- package/tests/account_privacy_flags.mjs +4 -1
- package/tests/application_echo.mjs +100 -1
- package/tests/application_lifecycle.mjs +145 -6
- 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/idp_device_exchange.mjs +174 -0
- package/tests/module_api.mjs +1 -0
- 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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 0334 — The recruiting directory lists a hidden builder by name, and hide changes a row's shape, not whether it exists
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-24
|
|
5
|
+
- **Task:** [#1002291](https://cloudbongos.com/builders#/task/1002291) (R15, owner recruiting UI; goal 1000045 C5, now folded into goal 1000110)
|
|
6
|
+
- **Relates to:** `docs/specs/<redacted>.md` D2 (owner-signed 2026-08-16), [ADR 0171](0171-profile-rollup-join-and-consent.md) D2, [ADR 0210](0210-scouting-graduates-on-an-instance-asserted-owner-claim.md) D4, [ADR 0215](0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md), [ADR 0217](0217-rank-gates-inviting-not-viewing.md) D1/D2
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
R15 asks for an owner recruiting surface with search, a role filter and a sort by track record, and says it must honour the hide flag and the recruiting opt-out. Most of that already existed when the task was picked up. The hall's `/people` page (tasks 1002577, 1003470, 1003110) searches, filters by role and sorts. R14 (task 1002290, ADR 0210) opened it to project owners and made the opt-out remove a builder. The one thing that did not match the owner-signed spec was what "honour hide" does.
|
|
11
|
+
|
|
12
|
+
The privacy spec's D2 matrix has a row for an account that is active, public and has hidden its cross-project stats. In the recruiter column it says **"listed name-only (if not opted out)"**. The directory did something else: `listScoutingRollup` joins `rollupVisibleSql`, which excludes `hide_stats`, so a hidden builder was dropped completely. In practice hide worked as a second opt-out.
|
|
13
|
+
|
|
14
|
+
Two things made that a bug and not a harmless extra caution:
|
|
15
|
+
|
|
16
|
+
- **The Settings copy said the opposite.** A hidden builder's "Discoverable by recruiters" row says *"Because your cross-project record is hidden, they will see your name and your picture, and nothing else"*, and that turning the switch off *"removes you from … the People directory"*. For that builder the second sentence was false, because they were never in the directory.
|
|
17
|
+
- **Absence came from ADR 0171 D2, which predates the spec.** ADR 0171 dropped hidden builders from `/people` and `/scouting` before a privacy model existed, and it said *"if 1002785 lands a different privacy rule, its model wins"*. Spike 1002785 then produced the D2 matrix.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
**The directory answers in two tiers, and the hide flag decides a builder's tier, not whether they are listed.**
|
|
22
|
+
|
|
23
|
+
| | Who | Carries |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `builders`, the ranked tier | visible AND NOT hidden AND reachable (`scoutingListableSql`, unchanged) | the full rollup row, ranked on track record |
|
|
26
|
+
| `name_only` | visible AND hidden AND reachable (`scoutingNameOnlySql`, new) | `github_login`, `display_name`, `avatar_url` and nothing else |
|
|
27
|
+
|
|
28
|
+
1. **One predicate, flipped.** `scoutingNameOnlySql` is `scoutingListableSql` with the hide term reversed, built the same way. A test proves the two predicates differ only in that term, so no builder can be in both tiers or in neither. Only the recruiting opt-out removes a builder from the directory. It lives in `account-visibility.js` like every other visibility rule.
|
|
29
|
+
2. **The existence floor stays.** A private account is never listed, even by name, and even when it opted in to recruiting. That account is the D3 sliver's subject, and the sliver is gated at the archon floor, which the owner kept (ADR 0215 D4, ADR 0217 D2). This ADR does not move that gate.
|
|
30
|
+
3. **Presence must not depend on activity.** The ranked tier is built from the activity table, so only builders with a record appear in it. The name-only tier reads the accounts table alone. If it listed only hidden builders who had shipped something, appearing in the list would reveal that fact, and that fact comes from the hidden record.
|
|
31
|
+
4. **The only sort key is the login.** The ranked tier is sorted by the record itself, so a hidden row placed among ranked rows would reveal roughly where its record would rank. Name-only rows sit in their own panel in alphabetical order. No sort lens reorders them, a search matches their name, and a role filter never matches them because the role list is part of the hidden record.
|
|
32
|
+
5. **The row is a literal whitelist, and the predicate has exactly one reader.** `scouting-name-only.js` builds each row key by key from a statement that selects three identity columns. `scoutingNameOnlySql` admits hidden accounts by design, so a read that joined it and returned a number would leak the very record hide withholds. `tests/effective_visibility_predicate.mjs` therefore pins the predicate to that one file, that file to that one statement, and the statement to those three columns and no rollup table.
|
|
33
|
+
|
|
34
|
+
These three fields are already public. A hidden builder's public profile page (`GET /profiles/:handle`) shows their handle, login, display name and avatar to anyone. The directory's audience is `project.curate` or a project owner, which is narrower. The only new fact the directory reveals is that the builder has not opted out of recruiting, and recruiters are the audience that setting exists for.
|
|
35
|
+
|
|
36
|
+
## Consequences
|
|
37
|
+
|
|
38
|
+
- A hidden builder who is still discoverable now appears on `/people` by name and picture. Recruiters were already promised this, and the Settings copy is now true without any wording change. Turning off "Discoverable by recruiters" removes them from both tiers. `tests/account_privacy_flags.mjs` now requires all three recruiting predicates to include the reach term.
|
|
39
|
+
- `GET /scouting` gains a response key and changes nothing that already existed. `builders` still has the same rows and the same shape, and an older client that ignores `name_only` behaves as before. Under the hood the route now runs two reads instead of one. The page still makes a single request.
|
|
40
|
+
- Tests that asserted a hidden builder was *absent* from the directory now assert that the builder is *listed by name only*, and that none of their numbers appear anywhere in the response (`tests/profile_route.mjs` R06.3). The real-database proof `tests/profile_rollup_consent.mjs` calls `listScoutingRollup` directly, which is the ranked tier, and is unchanged.
|
|
41
|
+
- The creation wizard's invite suggestions (`invite-suggestions.js`, ADR 0251) still read the ranked tier only. A hidden builder has no record to rank, and adding them there would be a new decision, which this ADR does not make.
|
|
42
|
+
|
|
43
|
+
## Rejected alternatives
|
|
44
|
+
|
|
45
|
+
- **Keep the exclusion and just fix the copy.** This contradicts the owner-signed matrix row, and it keeps two switches doing the same job. Whether a builder can be found is the reach setting's decision alone.
|
|
46
|
+
- **Mix hidden builders into the ranked ledger as trailing rows.** Their position below everyone else would read as a ranking, a role filter would need special cases, and the count, pager and clipping logic would all have to account for two kinds of row.
|
|
47
|
+
- **Also list private, discoverable accounts by name.** That is the D3 sliver, held at the archon floor by an owner decision. The directory's door is wider than that, and widening it is exactly what ADR 0217 D2 declined to do.
|
package/docs/adr/README.md
CHANGED
|
@@ -423,5 +423,7 @@ This keeps the decision history honest and traceable.
|
|
|
423
423
|
| 0329 | [**The rendered fit check is a kit floor and an advisory ship-check row, not a grader worker** ([task 1004203](https://cloudbongos.com/builders#/task/1004203), goal 1000095 — UI quality). Nothing in the pipeline rendered a page, so titles clipped by character count ran past their cards (task 1003480, task 1004204) and a second `.mSec` child would have landed on the first (task 1003078), each found only in a browser. Two new kit floors, `text-fit` (text measured word by word past its box; SVG text past its card rect) and `sibling-overlap` (in-flow, block-level, text-bearing siblings), run in every `render.js` render; `scripts/gds/render-check.js` renders the pages a branch changed at 1280 + 390 and `/ship-check` shows them as an ADVISORY row that never flips its exit (ADR 0162). Not a grader worker: that would put a browser, fixtures and a harness on the grader host for signal the builder already saw; the artist gate (ADR 0241) stays the human look before deploy.](0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md) | ui-design / kit + ship-check |
|
|
424
424
|
| 0330 | [**An ask is answered where it was asked** ([task 1004216](https://cloudbongos.com/builders#/task/1004216), goal 1000095 — BONGOS-V2), amending [ADR 0187](0187-collab-four-decisions.md) §4a. `lifecycle_004` let a builder ASK for help and let the **asker** mark their own row answered; the person being **asked** had no write at all, so the answer happened somewhere Bongos cannot see and the row sat open until its author remembered it — six asks addressed to the owner had been open between four and thirteen days on 2026-09-23, every one of them answerable. That was not an oversight: §4a deferred conversation to the Agora and a test named *"there is deliberately NO reply thread"* enforced it. **The deferral lost its addressee** — task 1002946 is ABANDONED and goal 1000064 ARCHIVED (verified against the live DB), so the rule had stopped meaning "wait for the Agora" and started meaning "an ask stays unanswerable forever to keep a promise to something that does not exist". **Decisions:** (1) an ask carries a thread of replies, APPEND-ONLY in the SCHEMA rather than by convention — no `updated_at`, `edited_by`, `deleted_at` or `parent_reply_id`, no reactions, no mentions, and a trigger that refuses UPDATE and DELETE outright, because a thing you can edit, nest, react to and address at people is a chat and that is what §4a declined; any builder may reply, since the board read is already project-wide and the good case is somebody you did not ask knowing the answer; (2) the **ADDRESSEE** may close an ask (`answered`), widening an asker-only UPDATE that was coherent only while nothing could be said on the row — but `withdrawn` does NOT widen, since "I no longer need this" is the asker's sentence alone and widening it would be a way to make an ask vanish unanswered; `settled_by` records which of the two closed it, and ADR 0187 §3's consent posture is untouched — closing ends a question, it does not accept work; (3) `/collab-review` posts the reply and **leaves the ask open** (owner decision), since an answer the reader has not read is exactly what an unattended run must halt on. **Consequences:** a reply cannot be taken back, by design; the FK **RESTRICTS rather than cascades** — the first draft cascaded, which reads right and was wrong, because a cascade is a plain DELETE the trigger refuses, so deleting an answered ask failed outright (caught by applying the migration to a real Postgres, not by reading it); the old test was REPLACED, not deleted, so the record that the restraint was deliberate survives; and replies inherit the pull-not-push posture, so nothing still tells an asker they were answered. Rejected: a single `answer` column (cheapest and unambiguously not a chat, but a follow-up would have nowhere to go except a second ask, splitting one conversation across two rows); waiting for the Agora (abandoned — waiting means forever); letting anyone close any ask (an open question is somebody's, and a stranger ending it should not be one click away).](0330-an-ask-is-answered-where-it-was-asked.md) | collab / lifecycle / schema |
|
|
425
425
|
| 0331 | [**The sky is the ideator's home, and the project sky takes the goal map's page** ([task 1004229](https://cloudbongos.com/builders#/task/1004229), goal 1000095 — BONGOS-V2), amending [ADR 0227](0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md) and decision 11 of `docs/specs/<redacted>.md`. The owner reviewed an interactive prototype of the ideator sky and the project sky over five rounds on 2026-09-24 and adopted it AS IS — copy, layout, colours, tour text, toggles, interactions — which contradicts two things already written down, so they are rewritten rather than leaving the build graded against a contract nobody means any more. **Decisions:** (1) the sky is the ideator's HOME surface — it replaces *Your thinking*, and the project sky replaces the goal map page — so "never load-bearing" and "arrives last" are DROPPED as a sequencing hedge from before there was a design to look at; the rule that survives at full force is the accessibility half, that the sky is never the ONLY path to anything it shows (the list drawer — the room — is one key away, and every drawn thing is a row in it); (2) ADR 0227 is AMENDED, not reversed — its measured refusal of a whole-corpus task map stands, and the project sky is §1's own prescription rather than its refusal: the goal graph is the entry point, tasks are drawn only INSIDE one goal past a zoom threshold (the per-goal drill-down §10.5 measured readable at 85.2px pitch worst case), below that they are dust density on the rings with NO identities, and the goal map's table survives as the sky's ledger drawer (§6's table equivalent kept); the nav slot the goal map was deliberately denied is GRANTED to the project sky, under Goals (owner, 2026-09-24), so part 4 changes the page and the test asserting no slot together; (3) the five states and the two channels are UNTOUCHED — criterion `ideator-sky-tells-the-truth` names a model, not a picture, and this amendment changes the picture's standing only; (4) the two HEURISTICS in the prototype — an idea's origin guessed from its text, and "born of a thought" found by scanning text for idea ids — must not ship as ledger facts: parts 3 and 4 hide both layers, part 5 replaces them with a `filed_via` column and a real born-of read. **Consequences:** `docs/design/mocks/sky/sky.template.html` is the PIXEL CONTRACT for parts 2–5, and every deviation a gate or the file ratchet forces is listed in that part's ship notes; no snapshot JSON or built page is ever committed (it carries real task and idea titles and this repo is public); ADR 0264's deleted goal "Living project sky" is superseded by this work under Working area 3. Rejected: shipping the sky as an extra and keeping "never load-bearing" (makes the adopted home surface unroutable); reversing ADR 0227 (its refusal is measured and still correct — the sky needs its drill-down shape, not its repeal); shipping the heuristics behind a disclaimer (a labelled guess is still a guess in a picture whose promise is that it cannot be faked).](0331-the-sky-is-the-ideators-home.md) | design / ideator / sky |
|
|
426
|
+
| 0333 | [**The application vouch is a third token kind, spent once, bound to the login it names, and carries a handle only past the consent floor** ([task 1002285](https://cloudbongos.com/builders#/task/1002285), goal 1000045 — criterion C4), resolving [ADR 0208](0208-application-lifecycle-the-handle-waits-and-the-vouch-has-no-minter.md) D1 + D2. The hub's join relay now carries a signed application vouch in `<redacted>`, so the ADR 0260 live-record path is no longer dormant. `mintHubApplicationVouch` / `<redacted>` share one envelope with the login and logout tokens, and each verifier demands what the other two lack; `verifyHubAssertion` now refuses any `purpose`, closing the device-login replay. The jti is a chosen, separate single-use store; a vouch must carry a jti and an audience, and must name the login the request names, or the row is written unvouched (never refused); a vouched re-application takes over an unvouched pending row for its login. The vouch carries its login as `applicant_login`, so even an un-upgraded core refuses it as a login. `applicant_handle` (`core_254`) is identity, not record, and rides only past the consent floor.](0333-the-application-vouch-is-a-third-token-spent-once-and-bound-to-its-login.md) |
|
|
427
|
+
| 0334 | [**The recruiting directory lists a hidden builder by name, and hide changes a row's shape, not whether it exists** ([task 1002291](https://cloudbongos.com/builders#/task/1002291), R15 — goal 1000110). Privacy spec D2 (owner-signed) lists an active, public, hidden account on the recruiter surface "name-only (if not opted out)"; the directory dropped it outright, making hide a second opt-out while Settings told that builder recruiters would see their name and picture. `GET /scouting` now answers two tiers: `builders` (ranked, `scoutingListableSql`, unchanged) and `name_only` (`scoutingNameOnlySql` = the same predicate with the hide term flipped, so the tiers partition the directory and only the opt-out removes anyone), projected to `github_login`, `display_name`, `avatar_url` by `scouting-name-only.js`. The existence floor stays (a private account is the archon-floor D3 sliver's, never the directory's); presence never depends on activity; the only order is the login; the predicate is pinned to that one reader and its three columns. The hall's `/people` renders the tier as its own panel, never ranked, found by name, never by a role filter. Rejected: fixing only the copy; trailing rows in the ranked ledger; listing private discoverable accounts by name.](0334-the-recruiting-directory-lists-a-hidden-builder-by-name.md) | privacy / recruiting / hall |
|
|
426
428
|
| 0335 | [**An invite is a pre-approved row on the project's own instance, and `project.recruit` says who may send one** ([task 1002813](https://cloudbongos.com/builders#/task/1002813), R1 of the project admin console wave, goal 1000110 — BONGOS-V2). Supersedes ADR 0141 §5's "Invitations (deferred slice)" paragraph only. Formalises spike task 1002786's three decisions and reconciles them with six weeks of shipping: the instance invite route (task 1003044), the PATCH rescind (ADR 0201 D4.2) and the composed join door (ADR 0247) already exist, and the hub invite's dead end is now reachable from the creation wizard. **Decisions:** (D1) authority lives on the project's own instance, and the instance IS the project; the hub is identity, relay, echo, notification and links, and never writes a project's admission row; (D2) a recruit invite is an `access_requests` row with `status='invited'`, so signing in is the acceptance act and the gate is untouched; the row gains `kind ∈ (application, invite)` and `created_by` (the inviter, which a rescind would otherwise overwrite), reuses `note`, backfills exactly (`created_at = resolved_at`), and neither column ever enters an admission read; (D3) one new grantable atom, `project.recruit` (`system:false`, floor archon, widened per instance through the existing governance tab), guards the invite, the outstanding list, the invite rescind and the queue read, while `access_request.review` keeps the verdict; because inviting is admitting, a recruiter without the review atom may not approve or re-admit an APPLICANT through the invite route (409 to the queue); the hub sliver moves onto the atom, and the hub directory and hub invite KEEP `project.curate`, revising ADR 0210/0215/0248, since swapping the directory would raise a gate on a viewing surface (ADR 0217). **Task map:** R2 (task 1002814) = the migration, the invite writes and the sender list, still on `access_request.review`; R3 (task 1002815) = mint the atom, move the gates, the 409 clause, the sliver swap and the pins, and it lands after R2; R5 (task 1002817) is already delivered, so close it into task 1002331; task 1003362 folds into R3. Rejected: a new invitations table; the hub pending row as the invite; widening `access_request.review` instead of minting an atom; a per-project settings row for the floor; letting a recruiter's invite approve applications; `system:true`; swapping `project.curate` on the hub directory.](0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md) | governance / onboarding / platform-identity |
|
|
427
429
|
| 0336 | [**A guild is a hub-side group whose members each consent twice, and its collective record is only ever the sum of what those members already show** ([task 1002299](https://cloudbongos.com/builders#/task/1002299), goal 1000110 — the R23 design spike for goal 1000045's C7/C8/C9). No platform guild existed (every "guild" in the tree is Discord's). Decisions: (1) three hub tables in `platform-identity`, in their own `guilds.js` (the 1,500-line ratchet), distinct from instance-local `goal_members`: guilds (slug, name, `visibility ∈ public|unlisted` with **no default**), members (`membership_kind ∈ owner|member`, never `role` per ADR 0174; a partial unique index holds exactly one owner; a per-member `shown_publicly`), and requests; (2) two consents to join, no self-join, through ADR 0202's connections lifecycle verbatim (silent decline, 90-day swallowed re-ask, 30-day read-time expiry); `accountActiveSql` floors the feature; an **unlisted guild 404s to everyone on the public read, members included**, and takes invites only, because a slug is guessable; (3) the public roster is `shown_publicly AND accountVisibleSql`, and widening a guild to public changes no member's flag but the flipping owner's, so consent given to a small audience is never reused for a large one; (4) **the privacy resolution, pending the owner's ratification:** a member is counted iff shown AND `rollupVisibleSql`, read-time, `no-store`, never stored, and N and M are both lengths of lists the reader can see. The guild record is therefore always a sum of numbers already on members' own public profiles, so nothing can be subtracted out. All-or-nothing is rejected (one member blanks everyone and hiding becomes costly), as is per-member consent to count hidden numbers (it breaks "hide_stats beats everything"). The trade-off: guilds of privacy-minded builders look smaller and rank lower. A union count of all members' projects is refused because it reveals shared private projects; (5) the namespace is `/g/<slug>`, `g` is reserved in the SEO spec's project-slug list, the slug is fixed in V1, and `tests/guild_namespace_reserved.mjs` pins the two documents together; (6) a guild engages a project only through the individual door: a guild application is each member's own vouched application, labelled through ADR 0260's live applicant-profile read (no new instance field); inviting a guild fans out ordinary individual invites to its public roster at that moment, **never a guild-level pre-approval** (that would let a guild owner decide admission to someone else's project); (7) the map ranks public guilds by size and counted works shipped, read-time, with a slug tiebreak, and R29 narrows to the visual. Scope changes for R24–R37 are tabled in the ADR, including R28's dependency on the abandoned R11 and R29/R31's archived orb goals.](0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md) | platform identity / privacy / social graph |
|
package/docs/api/openapi.json
CHANGED
|
@@ -89,6 +89,10 @@
|
|
|
89
89
|
"name": "closeness",
|
|
90
90
|
"description": "Endpoints under `/api/bongos/closeness`."
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
"name": "community",
|
|
94
|
+
"description": "Endpoints under `/api/bongos/community`."
|
|
95
|
+
},
|
|
92
96
|
{
|
|
93
97
|
"name": "connections",
|
|
94
98
|
"description": "Endpoints under `/api/bongos/connections`."
|
|
@@ -4391,6 +4395,34 @@
|
|
|
4391
4395
|
]
|
|
4392
4396
|
}
|
|
4393
4397
|
},
|
|
4398
|
+
"/community/search": {
|
|
4399
|
+
"get": {
|
|
4400
|
+
"operationId": "get_community_search",
|
|
4401
|
+
"tags": [
|
|
4402
|
+
"community"
|
|
4403
|
+
],
|
|
4404
|
+
"summary": "GET /community/search",
|
|
4405
|
+
"description": "a name/discipline search under EXACTLY the public-profile visibility rules (D4): a hide_stats account is findable by name but drops out of a discipline-filtered result (disciplines are rollup-derived, and the rollup is what hide_stats suppresses); private and provisional accounts never appear. no-store: the same hide-must-beat-the-cache posture as /profiles/:handle (ADR 0171 D5) — this reads the identical predicate and a flip must be reflected on the very next request.\n\n**Rank:** `public` — No authentication — any caller.",
|
|
4406
|
+
"x-rank": "public",
|
|
4407
|
+
"x-source": "modules/platform-identity/routes/community-search.js",
|
|
4408
|
+
"responses": {
|
|
4409
|
+
"200": {
|
|
4410
|
+
"description": "Success.",
|
|
4411
|
+
"content": {
|
|
4412
|
+
"application/json": {
|
|
4413
|
+
"schema": {
|
|
4414
|
+
"$ref": "#/components/schemas/GetCommunitySearchResponse"
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
},
|
|
4419
|
+
"400": {
|
|
4420
|
+
"$ref": "#/components/responses/BadRequest"
|
|
4421
|
+
}
|
|
4422
|
+
},
|
|
4423
|
+
"security": []
|
|
4424
|
+
}
|
|
4425
|
+
},
|
|
4394
4426
|
"/connections": {
|
|
4395
4427
|
"get": {
|
|
4396
4428
|
"operationId": "get_connections",
|
|
@@ -19808,6 +19840,23 @@
|
|
|
19808
19840
|
"why"
|
|
19809
19841
|
]
|
|
19810
19842
|
},
|
|
19843
|
+
"GetCommunitySearchResponse": {
|
|
19844
|
+
"type": "object",
|
|
19845
|
+
"properties": {
|
|
19846
|
+
"query": {},
|
|
19847
|
+
"discipline": {},
|
|
19848
|
+
"count": {},
|
|
19849
|
+
"results": {},
|
|
19850
|
+
"page": {}
|
|
19851
|
+
},
|
|
19852
|
+
"required": [
|
|
19853
|
+
"query",
|
|
19854
|
+
"discipline",
|
|
19855
|
+
"count",
|
|
19856
|
+
"results",
|
|
19857
|
+
"page"
|
|
19858
|
+
]
|
|
19859
|
+
},
|
|
19811
19860
|
"GetConnectionsDegreeHandleResponse": {
|
|
19812
19861
|
"type": "object",
|
|
19813
19862
|
"properties": {
|
|
@@ -20934,10 +20983,12 @@
|
|
|
20934
20983
|
"GetScoutingResponse": {
|
|
20935
20984
|
"type": "object",
|
|
20936
20985
|
"properties": {
|
|
20937
|
-
"builders": {}
|
|
20986
|
+
"builders": {},
|
|
20987
|
+
"name_only": {}
|
|
20938
20988
|
},
|
|
20939
20989
|
"required": [
|
|
20940
|
-
"builders"
|
|
20990
|
+
"builders",
|
|
20991
|
+
"name_only"
|
|
20941
20992
|
]
|
|
20942
20993
|
},
|
|
20943
20994
|
"GetSecurityDocsResponse": {
|
|
@@ -26418,9 +26469,9 @@
|
|
|
26418
26469
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
26419
26470
|
}
|
|
26420
26471
|
},
|
|
26421
|
-
"x-endpoint-count":
|
|
26422
|
-
"x-schema-count":
|
|
26472
|
+
"x-endpoint-count": 417,
|
|
26473
|
+
"x-schema-count": 454,
|
|
26423
26474
|
"x-undocumented-bodies": 9,
|
|
26424
|
-
"x-response-schemas":
|
|
26475
|
+
"x-response-schemas": 309,
|
|
26425
26476
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
26426
26477
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 417 endpoints across 71 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -187,6 +187,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
187
187
|
| GET | `/api/bongos/closeness/weights` | `any-builder` | — | the MODEL is not private, and publishing it is the whole point of the weights being data. |
|
|
188
188
|
| GET | `/api/bongos/closeness/with/:builderId` | `any-builder` | — | One pair — "why am I close to THEM", asked directly rather than by paging the graph. |
|
|
189
189
|
|
|
190
|
+
## `community` (1)
|
|
191
|
+
|
|
192
|
+
| Method | Path | Rank | Body | Description |
|
|
193
|
+
|---|---|---|---|---|
|
|
194
|
+
| GET | `/api/bongos/community/search` | `public` | — | a name/discipline search under EXACTLY the public-profile visibility rules (D4): a hide_stats account is findable by name but drops out o… |
|
|
195
|
+
|
|
190
196
|
## `connections` (7)
|
|
191
197
|
|
|
192
198
|
| Method | Path | Rank | Body | Description |
|
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,14 @@ 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)
|
|
2511
|
+
(next carrier) — readApplicationVouch re-exported (task 1002285, goal 1000110;
|
|
2512
|
+
ADR 0201 D2). The public POST /access-requests' check of the hub's signed
|
|
2513
|
+
application vouch → { githubId, handle } or null. Core supplies the hub
|
|
2514
|
+
public key and this instance's client_id, so the onboarding module passes
|
|
2515
|
+
only a token and a login — the <redacted> precedent.
|
|
2516
|
+
1.19.1018 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2517
|
+
landed since 1.19.1017 with no explicit bump. run 36083406390. (task 1002620)
|
|
2509
2518
|
---------------------------------------------------------------------------
|
|
2510
2519
|
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- core_254_access_requests_applicant_handle.sql — the platform handle the hub's
|
|
2
|
+
-- application vouch carried, beside the vouched github id (ADR 0333 D5; the column
|
|
3
|
+
-- core_230 deferred to its writer, ADR 0208 D1).
|
|
4
|
+
--
|
|
5
|
+
-- Identity, not the record: the applicant's profile is still read live at review
|
|
6
|
+
-- and never stored (ADR 0260 D4). NULL = unvouched, or the account had not cleared
|
|
7
|
+
-- the consent floor, or the hub's account read failed. A snapshot — not re-synced
|
|
8
|
+
-- on rename. The vouch MARK stays `applicant_github_id IS NOT NULL`; no admission
|
|
9
|
+
-- path reads this column (ADR 0016).
|
|
10
|
+
--
|
|
11
|
+
-- Purely additive + idempotent; the previous release never names the column, so a
|
|
12
|
+
-- replay or rollback is a no-op. access_requests is CORE-owned, so this is a core_
|
|
13
|
+
-- migration (ADR 0108 §5); migrate.sh records the stem.
|
|
14
|
+
|
|
15
|
+
BEGIN;
|
|
16
|
+
|
|
17
|
+
ALTER TABLE access_requests
|
|
18
|
+
ADD COLUMN IF NOT EXISTS applicant_handle text;
|
|
19
|
+
|
|
20
|
+
COMMENT ON COLUMN access_requests.applicant_handle IS
|
|
21
|
+
'The platform handle the hub application vouch carried at apply time (ADR 0201 D2.6, task 1002285): identity, never the record, which is read live at review. NULL on an unvouched row, or when the account had not cleared the consent floor. A snapshot — not re-synced on rename. DESCRIPTIVE ONLY — no admission path reads it (ADR 0016).';
|
|
22
|
+
|
|
23
|
+
COMMIT;
|
|
@@ -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": {
|