@bongos/core 1.19.170 → 1.19.172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.170",
6
- "core_contract": "1.19.170",
7
- "source_commit": "2fd8009b960bd910fe95b3169d504adc7fe309d1",
5
+ "core_version": "1.19.172",
6
+ "core_contract": "1.19.172",
7
+ "source_commit": "d488f4a69b30ec2fe81f95cc1ec652c25af8fc68",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-16T18:45:10.150Z",
9
+ "built_at": "2026-08-16T19:39:18.102Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 305,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1268,
14
+ "functional_verbatim": 1271,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1594,
20
- "tree_sha256": "938326e2e51b3c2bf0a7ab516c8444005ec95028f247df10cc55bedae67b099f",
19
+ "file_count": 1597,
20
+ "tree_sha256": "c7aad1a023cefa6bce6c0403ed5d0bf9c1e5a28b4e38e14a85f1cfc697d5edb3",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -347,12 +347,12 @@
347
347
  {
348
348
  "path": "clients/bongos-client/README.md",
349
349
  "mode": "0000644",
350
- "sha256": "d6d6163164f98c96a8e25dfe84825669bf8324f1f1b98adb0674781ad235ad10"
350
+ "sha256": "a13c27a7d671408579dedf417b7f28486bebc52b7a685ba3900cd4eca843957c"
351
351
  },
352
352
  {
353
353
  "path": "clients/bongos-client/bongos-client.global.js",
354
354
  "mode": "0000644",
355
- "sha256": "386706d0ef3d09c5e074bdc45bfc0de869d732eb1cb2a2280472f02f797f2b74"
355
+ "sha256": "7b6abb75150b8fdc9fba1e0d765c049979da7cdd1d6a20eeeb6f3d5e3886ebc7"
356
356
  },
357
357
  {
358
358
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -362,17 +362,17 @@
362
362
  {
363
363
  "path": "clients/bongos-client/index.cjs",
364
364
  "mode": "0000644",
365
- "sha256": "7d462b5b80c9db9159981406398fc62c5ba71043c6a3b9433852d06d612bf2b0"
365
+ "sha256": "a4a864366450b849bafc096807f37cd16c8ed624470473aaf4907b24ab104778"
366
366
  },
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "4acb1dde6f894ba37cff7e27e0110f12ae30c7cb6e4d972d6e85d2877489b6d0"
370
+ "sha256": "c032e9796a9feeee86929323992511d5ed1cb448975481f98f86cee1b3790d50"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
374
374
  "mode": "0000644",
375
- "sha256": "4c7679ef258644ab12db1b8fe412b388f96f4296c6aae45d6ab637a8f3b9a037"
375
+ "sha256": "cc177720f5f3b0971edac3f03464e3dd5930b48d12811cd9ab39a7b6ceaaeb9c"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/package.json",
@@ -1347,12 +1347,12 @@
1347
1347
  {
1348
1348
  "path": "docs/api-reference.md",
1349
1349
  "mode": "0000644",
1350
- "sha256": "ccf59372e0456f36d83154d6dff4dbf5382e8d69023bee82b318b8e9cfb2c50b"
1350
+ "sha256": "d1595faad24aafba420199b3115b11ee5387de7c97c7f4fe7fe4f138f43dd79a"
1351
1351
  },
1352
1352
  {
1353
1353
  "path": "docs/api/openapi.json",
1354
1354
  "mode": "0000644",
1355
- "sha256": "4deac83a1d7c12e1a24109e856d9a6fef6b539afafdca55a525ab6786fe4e235"
1355
+ "sha256": "1435287ef26983d7088555abc9f18df97ceb4a8676be091ff49076e54a4366ab"
1356
1356
  },
1357
1357
  {
1358
1358
  "path": "docs/architecture.md",
@@ -2662,7 +2662,7 @@
2662
2662
  {
2663
2663
  "path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
2664
2664
  "mode": "0000644",
2665
- "sha256": "7d462b5b80c9db9159981406398fc62c5ba71043c6a3b9433852d06d612bf2b0"
2665
+ "sha256": "a4a864366450b849bafc096807f37cd16c8ed624470473aaf4907b24ab104778"
2666
2666
  },
2667
2667
  {
2668
2668
  "path": "modules/dev-box/app/test/unit.mjs",
@@ -3317,12 +3317,12 @@
3317
3317
  {
3318
3318
  "path": "modules/hall-ui/public/profile.html",
3319
3319
  "mode": "0000644",
3320
- "sha256": "cd3148ae6a22d7e7fff8e0b4a07aed2fe6d27b4e0b6e5b450bd2a63b3f078713"
3320
+ "sha256": "b8d5fb31d92f6d65e8f257d10c77f4ee0aa0ebec50d977d4c7d918ff2253da1e"
3321
3321
  },
3322
3322
  {
3323
3323
  "path": "modules/hall-ui/public/profile.js",
3324
3324
  "mode": "0000644",
3325
- "sha256": "be57f192368280ac1ae2b0b820f89daf59512f9f89d9ad23414c42cbf09a462e"
3325
+ "sha256": "a4cf0c4c2d088fd1fb84c41c93cc28e2acd2b656b0bc995cd46043881b3611fa"
3326
3326
  },
3327
3327
  {
3328
3328
  "path": "modules/hall-ui/public/ranks.html",
@@ -3789,6 +3789,11 @@
3789
3789
  "mode": "0000644",
3790
3790
  "sha256": "cce2f971f030aa628f9d7855f2e421595ea71cb314e90965206dbe970b7beded"
3791
3791
  },
3792
+ {
3793
+ "path": "modules/platform-identity/migrations/platform_identity_008_handle.sql",
3794
+ "mode": "0000644",
3795
+ "sha256": "f78eb8f7b6ee1ea3ddd789d6019b9715ae193ff0cb7890aeb24d8473033a911a"
3796
+ },
3792
3797
  {
3793
3798
  "path": "modules/platform-identity/module.json",
3794
3799
  "mode": "0000644",
@@ -3797,7 +3802,7 @@
3797
3802
  {
3798
3803
  "path": "modules/platform-identity/platform-identity.js",
3799
3804
  "mode": "0000644",
3800
- "sha256": "96907af1af78c472118ecf5ed9f269d6d2ce940b09535aae3b3df4de4d2337c1"
3805
+ "sha256": "115be49e667716d760610f8a9b167eeea4e0e6b61c40c7b9a439703847b61dcd"
3801
3806
  },
3802
3807
  {
3803
3808
  "path": "modules/platform-identity/pollers/code-sweep.js",
@@ -3817,7 +3822,7 @@
3817
3822
  {
3818
3823
  "path": "modules/platform-identity/routes/profile.js",
3819
3824
  "mode": "0000644",
3820
- "sha256": "6ea29b4f07417fa925b7987dd7fbe8a0262b7fec83ceffade9fccbc220289bba"
3825
+ "sha256": "29e7f6304b4215fd79c35662c48370a04a5a9ffa2c3c4ae3d1fabf277d407eb2"
3821
3826
  },
3822
3827
  {
3823
3828
  "path": "modules/platform-identity/routes/projects.js",
@@ -3837,7 +3842,7 @@
3837
3842
  {
3838
3843
  "path": "modules/platform-identity/tests/platform-identity.mjs",
3839
3844
  "mode": "0000644",
3840
- "sha256": "18bf8d0cb42d76bc5413c760b13837fe439a6e719b88a84db96f143b829e67d5"
3845
+ "sha256": "4b7f13effc33d5d16a5f87d0915ed31e107f138ef135822745774f90192ad327"
3841
3846
  },
3842
3847
  {
3843
3848
  "path": "modules/platform-identity/ui/.gitkeep",
@@ -4212,12 +4217,12 @@
4212
4217
  {
4213
4218
  "path": "package-lock.json",
4214
4219
  "mode": "0000644",
4215
- "sha256": "0b596f97bbd28b6ddfda4a9265effcb0ea8711a4bb57948c35b80d7cf79c23d9"
4220
+ "sha256": "3da96cec73d1af6c4f1470d29cc26187cc949eba5b6fb7291b89da3da4c7eae7"
4216
4221
  },
4217
4222
  {
4218
4223
  "path": "package.json",
4219
4224
  "mode": "0000644",
4220
- "sha256": "ae9625129bd03cb29845f65e1be3abd91c48259d85a9c23095370e43a0a0e8aa"
4225
+ "sha256": "dbf95e145339c9e8d52b1e532e452eb2d344c0c0f06a28f58c6e19366e256dd6"
4221
4226
  },
4222
4227
  {
4223
4228
  "path": "public-docs/index.html",
@@ -5477,7 +5482,7 @@
5477
5482
  {
5478
5483
  "path": "scripts/hall-preview/server.js",
5479
5484
  "mode": "0000644",
5480
- "sha256": "b6550cee568d8e8082c30512979256872d71d0d7f1810d061f5dbffad8a76979"
5485
+ "sha256": "1cd66475d610b107b0f3b7f19688133e7c482b41b034e2cef6009d552df31cb2"
5481
5486
  },
5482
5487
  {
5483
5488
  "path": "scripts/migrate.sh",
@@ -5822,7 +5827,7 @@
5822
5827
  {
5823
5828
  "path": "src/module-api.js",
5824
5829
  "mode": "0000644",
5825
- "sha256": "28605499539ca63e000ac2fc52af1a3d75dbb2fd947846426619a189a2afd92b"
5830
+ "sha256": "bc1f9966a0040b5239664c1a5d993c302b1fa9c56d2080c97b168115a4da847a"
5826
5831
  },
5827
5832
  {
5828
5833
  "path": "src/module-loader/catalog.js",
@@ -6904,6 +6909,11 @@
6904
6909
  "mode": "0000644",
6905
6910
  "sha256": "82bf1cc68150c247c108308d360d6408511f31a7af11d5e7a4721e7ea282f2bb"
6906
6911
  },
6912
+ {
6913
+ "path": "tests/handle_machinery.mjs",
6914
+ "mode": "0000644",
6915
+ "sha256": "755f49404e97c60c8c264f5f4c9d563a7c4ee4f51b43afe96fd10acce52ebe7b"
6916
+ },
6907
6917
  {
6908
6918
  "path": "tests/helpers.mjs",
6909
6919
  "mode": "0000644",
@@ -7389,6 +7399,11 @@
7389
7399
  "mode": "0000644",
7390
7400
  "sha256": "3b3045a6143d67c963516ed5703b5dabb9b8945a392118721e6a0a56258f1732"
7391
7401
  },
7402
+ {
7403
+ "path": "tests/profile_ui_cross_project.mjs",
7404
+ "mode": "0000644",
7405
+ "sha256": "6a7a223a678b56333befaed017429e5b9d8a81e4405efae72a23355902517d9d"
7406
+ },
7392
7407
  {
7393
7408
  "path": "tests/project_stats.mjs",
7394
7409
  "mode": "0000644",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 273 operations across 45 resource groups
8
+ - 274 operations across 45 resource groups
9
9
 
10
10
  ## Install
11
11
 
@@ -423,6 +423,8 @@ function createClient(opts = {}) {
423
423
  getMeEventSoundPrefs: (args) => request("GET", "/me/event-sound-prefs", { hasBody: false }, args),
424
424
  // PATCH /me/event-sound-prefs — rank: any-builder — PATCH /me/event-sound-prefs
425
425
  patchMeEventSoundPrefs: (args) => request("PATCH", "/me/event-sound-prefs", { hasBody: true }, args),
426
+ // PATCH /me/handle — rank: any-builder — PATCH /me/handle
427
+ patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
426
428
  // GET /me/interaction — rank: any-builder — GET /me/interaction
427
429
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
428
430
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -422,6 +422,8 @@ function createClient(opts = {}) {
422
422
  getMeEventSoundPrefs: (args) => request("GET", "/me/event-sound-prefs", { hasBody: false }, args),
423
423
  // PATCH /me/event-sound-prefs — rank: any-builder — PATCH /me/event-sound-prefs
424
424
  patchMeEventSoundPrefs: (args) => request("PATCH", "/me/event-sound-prefs", { hasBody: true }, args),
425
+ // PATCH /me/handle — rank: any-builder — PATCH /me/handle
426
+ patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
425
427
  // GET /me/interaction — rank: any-builder — GET /me/interaction
426
428
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
427
429
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -51,7 +51,7 @@ export interface GetInboxResponse { ideas: unknown; open_count: unknown }
51
51
  export interface GetLeaderboardResponse { leaderboard: unknown; page: unknown }
52
52
  export interface GetLearningsResponse { learnings: unknown }
53
53
  export interface GetMeAchievementsResponse { unlocked: unknown }
54
- export interface GetMeCrossProjectResponse { hide_stats: boolean; profile_state: unknown; cross_project: unknown }
54
+ export interface GetMeCrossProjectResponse { hide_stats: boolean; profile_state: unknown; handle: unknown; cross_project: unknown }
55
55
  export interface GetMeDisplayNameResponse { display_name: unknown; max_length: unknown }
56
56
  export interface GetMeEventSoundPrefsResponse { prefs: unknown; effective: unknown; effective_wav: unknown; events: unknown }
57
57
  export interface GetMeRecentShipsResponse { ships: unknown }
@@ -131,6 +131,8 @@ export interface PatchMeDisciplinesRequest { preferred_disciplines: string[] }
131
131
  export interface PatchMeDisciplinesResponse { builder: unknown; onboarding: unknown }
132
132
  export interface PatchMeDisplayNameResponse { display_name: unknown; max_length: unknown }
133
133
  export interface PatchMeEventSoundPrefsResponse { prefs: unknown; effective: unknown; effective_wav: unknown; events: unknown }
134
+ export interface PatchMeHandleRequest { handle: string }
135
+ export interface PatchMeHandleResponse { handle: unknown; previous_handle: unknown }
134
136
  export interface PatchMeSkillPrefsResponse { prefs: unknown; effective: unknown; defaults: unknown; known_skills: unknown; allowed_models: unknown }
135
137
  export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
136
138
  export interface PatchProjectsIdFeaturedRequest { featured: boolean }
@@ -662,6 +664,8 @@ export interface BongosClient {
662
664
  getMeEventSoundPrefs(args?: RequestArgs): Promise<GetMeEventSoundPrefsResponse>;
663
665
  /** PATCH /me/event-sound-prefs — rank: any-builder */
664
666
  patchMeEventSoundPrefs(args?: RequestArgs): Promise<PatchMeEventSoundPrefsResponse>;
667
+ /** PATCH /me/handle — rank: any-builder */
668
+ patchMeHandle(args: RequestArgs & { body: PatchMeHandleRequest }): Promise<PatchMeHandleResponse>;
665
669
  /** GET /me/interaction — rank: any-builder */
666
670
  getMeInteraction(args?: RequestArgs): Promise<ApiResponse>;
667
671
  /** PATCH /me/interaction — rank: any-builder */
@@ -419,6 +419,8 @@ export function createClient(opts = {}) {
419
419
  getMeEventSoundPrefs: (args) => request("GET", "/me/event-sound-prefs", { hasBody: false }, args),
420
420
  // PATCH /me/event-sound-prefs — rank: any-builder — PATCH /me/event-sound-prefs
421
421
  patchMeEventSoundPrefs: (args) => request("PATCH", "/me/event-sound-prefs", { hasBody: true }, args),
422
+ // PATCH /me/handle — rank: any-builder — PATCH /me/handle
423
+ patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
422
424
  // GET /me/interaction — rank: any-builder — GET /me/interaction
423
425
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
424
426
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -6575,6 +6575,55 @@
6575
6575
  ]
6576
6576
  }
6577
6577
  },
6578
+ "/me/handle": {
6579
+ "patch": {
6580
+ "operationId": "patch_me_handle",
6581
+ "tags": [
6582
+ "me"
6583
+ ],
6584
+ "summary": "PATCH /me/handle",
6585
+ "description": "rename the caller's OWN handle (own data, keyed on the authenticated github_id). Rules live in setAccountHandle: canonical form, reserved words refused, one rename per window, previous handle kept for the 301 window. Upsert-first (the setAccountHideStats precedent) so a hub builder who never federated can still claim a handle — never demotes consent.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
6586
+ "x-rank": "any-builder",
6587
+ "x-source": "modules/platform-identity/routes/profile.js",
6588
+ "requestBody": {
6589
+ "required": true,
6590
+ "content": {
6591
+ "application/json": {
6592
+ "schema": {
6593
+ "$ref": "#/components/schemas/PatchMeHandleRequest"
6594
+ }
6595
+ }
6596
+ },
6597
+ "x-validated": true
6598
+ },
6599
+ "responses": {
6600
+ "200": {
6601
+ "description": "Success.",
6602
+ "content": {
6603
+ "application/json": {
6604
+ "schema": {
6605
+ "$ref": "#/components/schemas/PatchMeHandleResponse"
6606
+ }
6607
+ }
6608
+ }
6609
+ },
6610
+ "400": {
6611
+ "$ref": "#/components/responses/ValidationFailed"
6612
+ },
6613
+ "401": {
6614
+ "$ref": "#/components/responses/Unauthorized"
6615
+ },
6616
+ "403": {
6617
+ "$ref": "#/components/responses/Forbidden"
6618
+ }
6619
+ },
6620
+ "security": [
6621
+ {
6622
+ "builderSession": []
6623
+ }
6624
+ ]
6625
+ }
6626
+ },
6578
6627
  "/me/interaction": {
6579
6628
  "get": {
6580
6629
  "operationId": "get_me_interaction",
@@ -12942,11 +12991,13 @@
12942
12991
  "type": "boolean"
12943
12992
  },
12944
12993
  "profile_state": {},
12994
+ "handle": {},
12945
12995
  "cross_project": {}
12946
12996
  },
12947
12997
  "required": [
12948
12998
  "hide_stats",
12949
12999
  "profile_state",
13000
+ "handle",
12950
13001
  "cross_project"
12951
13002
  ]
12952
13003
  },
@@ -13984,6 +14035,31 @@
13984
14035
  "events"
13985
14036
  ]
13986
14037
  },
14038
+ "PatchMeHandleRequest": {
14039
+ "type": "object",
14040
+ "properties": {
14041
+ "handle": {
14042
+ "type": "string",
14043
+ "minLength": 1,
14044
+ "maxLength": 39
14045
+ }
14046
+ },
14047
+ "required": [
14048
+ "handle"
14049
+ ],
14050
+ "additionalProperties": false
14051
+ },
14052
+ "PatchMeHandleResponse": {
14053
+ "type": "object",
14054
+ "properties": {
14055
+ "handle": {},
14056
+ "previous_handle": {}
14057
+ },
14058
+ "required": [
14059
+ "handle",
14060
+ "previous_handle"
14061
+ ]
14062
+ },
13987
14063
  "PatchMeSkillPrefsResponse": {
13988
14064
  "type": "object",
13989
14065
  "properties": {
@@ -17222,9 +17298,9 @@
17222
17298
  "description": "A required dependency/feature is not configured or is temporarily down."
17223
17299
  }
17224
17300
  },
17225
- "x-endpoint-count": 273,
17226
- "x-schema-count": 306,
17301
+ "x-endpoint-count": 274,
17302
+ "x-schema-count": 308,
17227
17303
  "x-undocumented-bodies": 8,
17228
- "x-response-schemas": 207,
17304
+ "x-response-schemas": 208,
17229
17305
  "x-generated-by": "scripts/gds/gen-api-docs.js"
17230
17306
  }
@@ -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. 273 endpoints across 45 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 274 endpoints across 45 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/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -282,7 +282,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
282
282
  | POST | `/api/bongos/llm-cache/lookup` | `metic+archon` | `cache_key`, `tokens_saved_est` | POST /llm-cache/lookup — hit/miss + atomic hit_count bump. |
283
283
  | POST | `/api/bongos/llm-cache/store` | `metic+archon` | `spec`, `result_stdout`, `result_cost_usd` | POST /llm-cache/store — upsert a freshly-produced result. |
284
284
 
285
- ## `me` (23)
285
+ ## `me` (24)
286
286
 
287
287
  | Method | Path | Rank | Body | Description |
288
288
  |---|---|---|---|---|
@@ -296,6 +296,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
296
296
  | PATCH | `/api/bongos/me/display-name` | `any-builder` | _undocumented_ | PATCH the display name. |
297
297
  | GET | `/api/bongos/me/event-sound-prefs` | `any-builder` | — | Per-builder per-event sound variant selection (task 582, migration 164). |
298
298
  | PATCH | `/api/bongos/me/event-sound-prefs` | `any-builder` | _undocumented_ | PATCH a partial set of event sound overrides. |
299
+ | PATCH | `/api/bongos/me/handle` | `any-builder` | `handle` | rename the caller's OWN handle (own data, keyed on the authenticated github_id). |
299
300
  | GET | `/api/bongos/me/interaction` | `any-builder` | — | Per-builder INTERACTION PROFILE (task 2010, shares the behavior_prefs jsonb / migration 119). |
300
301
  | PATCH | `/api/bongos/me/interaction` | `any-builder` | _undocumented_ | PATCH the interaction profile. |
301
302
  | GET | `/api/bongos/me/model-allocation` | `any-builder` | — | |
@@ -422,6 +422,8 @@ function createClient(opts = {}) {
422
422
  getMeEventSoundPrefs: (args) => request("GET", "/me/event-sound-prefs", { hasBody: false }, args),
423
423
  // PATCH /me/event-sound-prefs — rank: any-builder — PATCH /me/event-sound-prefs
424
424
  patchMeEventSoundPrefs: (args) => request("PATCH", "/me/event-sound-prefs", { hasBody: true }, args),
425
+ // PATCH /me/handle — rank: any-builder — PATCH /me/handle
426
+ patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
425
427
  // GET /me/interaction — rank: any-builder — GET /me/interaction
426
428
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
427
429
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -40,6 +40,14 @@
40
40
  </div>
41
41
  </section>
42
42
 
43
+ <!-- Cross-project record — hub-only (ADR 0171). profile.js reveals this
44
+ only when the profile response carries a cross_project key; on a
45
+ vanilla instance the key is absent and the section never renders. -->
46
+ <section class="scroll" id="xp-scroll" aria-labelledby="xp-h" hidden>
47
+ <h2 id="xp-h" class="scroll__h">Cross-project record</h2>
48
+ <div id="xp-body"></div>
49
+ </section>
50
+
43
51
  <!-- Achievements — the builder's earned shelf. -->
44
52
  <section class="scroll scroll--bare" id="ach-scroll" aria-labelledby="ach-h">
45
53
  <h2 id="ach-h" class="scroll__h">Achievements</h2>
@@ -66,6 +74,10 @@
66
74
  </tbody>
67
75
  </table>
68
76
  </section>
77
+
78
+ <!-- Reserved slots — connections, guilds, and actions (profile-page
79
+ spec sections 6–8) mount here in later phases; keeping the order
80
+ fixed now means the page is not re-laid-out twice (Decision 5). -->
69
81
  </main>
70
82
 
71
83
  <footer class="app-foot" id="app-foot"></footer>
@@ -83,6 +95,6 @@
83
95
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
84
96
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
85
97
  <script src="/builders/brand-apply.js?v=2026-06-27-r75"></script>
86
- <script src="/builders/profile.js?v=2026-08-04-reward1"></script>
98
+ <script src="/builders/profile.js?v=2026-08-16-r04"></script>
87
99
  </body>
88
100
  </html>
@@ -160,6 +160,82 @@
160
160
  }).join('');
161
161
  }
162
162
 
163
+ // ---- render: cross-project record (hub-only, ADR 0171 / task 1002280) ----
164
+ //
165
+ // The section reveals only when the response carries a cross_project KEY —
166
+ // present iff the hub port resolved; on a vanilla instance the key is absent
167
+ // and the section stays hidden (degrade, don't 500). A null value means "no
168
+ // public cross-project record" — hidden, provisional, or none, deliberately
169
+ // indistinguishable — so all three render the same private line and NOTHING
170
+ // derived from the rollup (no partial numbers; a count plus a hidden total is
171
+ // a leak by subtraction). The response is viewer-independent (ADR 0171 D4):
172
+ // self is detected client-side by comparing the /me id, and the self "toggle"
173
+ // is a link to the real control in Settings (R03). No caching assumptions —
174
+ // hub profile responses are no-store (ADR 0171 D5).
175
+
176
+ function xpProjectRow(p) {
177
+ const name = escapeHtml(p.name || 'Project');
178
+ // origin is third-party-reported; link only a well-formed https URL.
179
+ const origin = typeof p.origin === 'string' && /^https:\/\//.test(p.origin) ? p.origin : null;
180
+ const nameCell = origin ? `<a href="${escapeHtml(origin)}" rel="noopener">${name}</a>` : name;
181
+ // rank_held is THAT project's standing as it reported it — plain text, never
182
+ // the local rank badge or the instance's own rank labels (spec Decision 1).
183
+ const held = typeof p.rank_held === 'string' && p.rank_held
184
+ ? escapeHtml(p.rank_held.charAt(0).toUpperCase() + p.rank_held.slice(1))
185
+ : '—';
186
+ return `<tr><td>${nameCell}</td><td>${held}</td></tr>`;
187
+ }
188
+
189
+ function renderCrossProject(data, selfId) {
190
+ const section = $('#xp-scroll');
191
+ const body = $('#xp-body');
192
+ if (!section || !body) return;
193
+ if (!data || !('cross_project' in data)) {
194
+ section.hidden = true;
195
+ return;
196
+ }
197
+ // ids may serialize as strings (pg bigint) — compare canonicalized.
198
+ const isSelf = selfId != null && data.builder && String(data.builder.id) === String(selfId);
199
+ const manage = isSelf
200
+ ? `<p class="scroll__lede">This is you — <a href="/settings#cross-project">manage its visibility in Settings → Cross-project record</a>.</p>`
201
+ : '';
202
+ const cp = data.cross_project;
203
+ if (!cp) {
204
+ body.innerHTML = `<p class="scroll__lede">This builder keeps their track record private.</p>${manage}`;
205
+ section.hidden = false;
206
+ return;
207
+ }
208
+ const t = cp.totals || {};
209
+ const tiles =
210
+ `<div class="stat-row">` +
211
+ tile('Projects', fmtNum(t.projects)) +
212
+ tile('Works shipped', fmtNum(t.works_shipped)) +
213
+ tile('Credits', fmtNum(t.credits)) +
214
+ tile('Karma', fmtNum(t.karma)) +
215
+ `</div>`;
216
+ const discs = Array.isArray(cp.disciplines) ? cp.disciplines.filter((d) => typeof d === 'string') : [];
217
+ const metaBits = [];
218
+ if (discs.length) metaBits.push(`Disciplines: ${discs.join(', ')}`);
219
+ if (cp.last_active) metaBits.push(`Last active ${fmtDate(cp.last_active)}`);
220
+ const meta = metaBits.length ? `<p class="scroll__lede">${escapeHtml(metaBits.join(' · '))}</p>` : '';
221
+ const named = Array.isArray(cp.projects) ? cp.projects : [];
222
+ const moreCount = Number(cp.more_projects) || 0;
223
+ const projectsTable = named.length
224
+ ? `<table class="ledger">
225
+ <thead><tr><th>Project</th><th title="That project's own standing, as reported by that project.">Standing there</th></tr></thead>
226
+ <tbody>${named.map(xpProjectRow).join('')}</tbody>
227
+ </table>`
228
+ : '';
229
+ const more = moreCount > 0
230
+ ? `<p class="scroll__lede">+ ${fmtNum(moreCount)} more project${moreCount === 1 ? '' : 's'} not publicly listed.</p>`
231
+ : '';
232
+ const none = !named.length && !moreCount
233
+ ? `<p class="scroll__lede">No cross-project activity reported yet.</p>`
234
+ : '';
235
+ body.innerHTML = tiles + meta + projectsTable + more + none + manage;
236
+ section.hidden = false;
237
+ }
238
+
163
239
  // ---- states -------------------------------------------------------------
164
240
 
165
241
  function showMessage(text) {
@@ -167,13 +243,14 @@
167
243
  if (sub) sub.textContent = text;
168
244
  }
169
245
 
170
- function renderProfile(data) {
246
+ function renderProfile(data, selfId) {
171
247
  const builder = data.builder || {};
172
248
  const name = builder.display_name || builder.github_login || 'Builder';
173
249
  const shipped = Number(data.shipped_count) || 0;
174
250
  showMessage(`${name} — ${rankLabel(normalizeRank(builder.rank))} · ${fmtNum(shipped)} work${shipped === 1 ? '' : 's'} shipped`);
175
251
  renderHead(builder);
176
252
  renderNumbers(data.shipped_count, data.achievements);
253
+ renderCrossProject(data, selfId);
177
254
  renderAchievements(data.achievements);
178
255
  renderWorks(data.recent_ships);
179
256
  }
@@ -197,23 +274,39 @@
197
274
  const ach = $('#ach-grid'); if (ach) ach.innerHTML = '';
198
275
  const works = $('#works-table tbody'); if (works) works.innerHTML = '';
199
276
  const worksLede = $('#works-lede'); if (worksLede) worksLede.hidden = true;
277
+ const xp = $('#xp-scroll'); if (xp) xp.hidden = true;
200
278
  }
201
279
 
202
- async function fetchProfile(id) {
280
+ async function fetchProfile(id, selfId) {
203
281
  const res = await api.request('GET', `${API}/builders/${encodeURIComponent(id)}/profile`);
204
282
  if (res.status === 404) {
205
283
  showMessage('No builder found with that id.');
206
284
  return;
207
285
  }
208
286
  if (!res.ok) throw new Error(`profile → ${res.status}`);
209
- renderProfile(res.data || {});
287
+ renderProfile(res.data || {}, selfId);
288
+ }
289
+
290
+ // The viewer's own builder id, or null when signed out / unresolvable. Used
291
+ // ONLY for client-side self-detection (ADR 0171 D4 — the profile response is
292
+ // viewer-independent); a failure here just means no self link renders.
293
+ async function resolveSelfId() {
294
+ try {
295
+ const me = await api.request('GET', `${API}/me`);
296
+ if (!me.ok) return null;
297
+ const builder = (me.data && me.data.builder) ? me.data.builder : me.data;
298
+ return (builder && builder.id) || null;
299
+ } catch (err) {
300
+ console.error('[profile] /me self-detect failed', err);
301
+ return null;
302
+ }
210
303
  }
211
304
 
212
305
  async function load() {
213
306
  const idParam = new URLSearchParams(location.search).get('id');
214
307
  try {
215
308
  if (idParam) {
216
- await fetchProfile(idParam);
309
+ await fetchProfile(idParam, await resolveSelfId());
217
310
  return;
218
311
  }
219
312
  // No id — soft-resolve the viewer's OWN id from /me (no hard redirect; a
@@ -230,7 +323,7 @@
230
323
  renderEmpty();
231
324
  return;
232
325
  }
233
- await fetchProfile(selfId);
326
+ await fetchProfile(selfId, selfId);
234
327
  } catch (err) {
235
328
  console.error('[profile] failed to load', err);
236
329
  showMessage('Could not load this profile. Return to the hall and try again.');