@bongos/core 1.21.127 → 1.21.128

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.21.127",
6
- "core_contract": "1.21.127",
7
- "source_commit": "ea6cb74e9eba8805eb016b8ac0420841e429c290",
5
+ "core_version": "1.21.128",
6
+ "core_contract": "1.21.128",
7
+ "source_commit": "31d87d7e4b4e2c97040083251efcd43c9c97c935",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-15T15:58:58.689Z",
9
+ "built_at": "2026-10-15T16:35:02.176Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 606,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3138,
14
+ "functional_verbatim": 3141,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3773,
20
- "tree_sha256": "2af5f5b320f78e843791c36c370aa8f198a28c9bd3b085c1875f39869558c2cd",
19
+ "file_count": 3776,
20
+ "tree_sha256": "63aa30ff5172b9c025bc2092a63f9109f261f2a5d46b47638378c0505428b3f4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -317,12 +317,12 @@
317
317
  {
318
318
  "path": "clients/bongos-client/README.md",
319
319
  "mode": "0000644",
320
- "sha256": "8ec09fb234db73a4cb92ad78c4da3532d5717f48106dd97fbb860fa17b1b1b5b"
320
+ "sha256": "9aea69205101517f33e85bd80593333deb8a015d6c98ae55d73ddc59c3067d12"
321
321
  },
322
322
  {
323
323
  "path": "clients/bongos-client/bongos-client.global.js",
324
324
  "mode": "0000644",
325
- "sha256": "2f89099d91db14e37dc9d67ef68a81ba410740446758e639cb50de51a6942d79"
325
+ "sha256": "23208de4f2aa8f0c1c5ec2f7939e637224280c3cf520958148549e2f79c02c9d"
326
326
  },
327
327
  {
328
328
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -332,17 +332,17 @@
332
332
  {
333
333
  "path": "clients/bongos-client/index.cjs",
334
334
  "mode": "0000644",
335
- "sha256": "14f0fa81adc576c014950856f70d6721bb796104e65b3fecadb7d5b1370010b2"
335
+ "sha256": "25dd808eae862cd02ed9a494f5e0ce2ee8d31bb664f78a23c151a95dc1f0c25f"
336
336
  },
337
337
  {
338
338
  "path": "clients/bongos-client/index.d.ts",
339
339
  "mode": "0000644",
340
- "sha256": "c13884fb0bd175f8bc9745150b2ca5ed254dd0ad14bcaa63511cf892cdbaec36"
340
+ "sha256": "d98b7b69f7e2db071b9b10e5e039afdf95262b7196450ef56c73c0acac8717a7"
341
341
  },
342
342
  {
343
343
  "path": "clients/bongos-client/index.mjs",
344
344
  "mode": "0000644",
345
- "sha256": "b77e22573f2d8c9c9f2f1b3a1642e9258cd4fec394d1416ae816c6bf56a3295d"
345
+ "sha256": "c3a776cd09ddde2c4d1d3ed8b3f90daed304c6cc8d459a688585526360c2b5e5"
346
346
  },
347
347
  {
348
348
  "path": "clients/bongos-client/package.json",
@@ -2377,12 +2377,12 @@
2377
2377
  {
2378
2378
  "path": "docs/api-reference.md",
2379
2379
  "mode": "0000644",
2380
- "sha256": "cda3b2d273704ab0bebe67efe46f0101e1809ec8e8beef2ad3614b6fc5845a31"
2380
+ "sha256": "9ec7dd570500edd3705cd871886734d89780b92df6f185cdc86002288fe7aa7d"
2381
2381
  },
2382
2382
  {
2383
2383
  "path": "docs/api/openapi.json",
2384
2384
  "mode": "0000644",
2385
- "sha256": "cd89b6776167172503ef68a77b1cfa68a00bf886d8f586c456f67e3cfef84fba"
2385
+ "sha256": "319fce626080497fdf50a50064f0faa56951b1aa021e97edd6ad481d8ee5c9b9"
2386
2386
  },
2387
2387
  {
2388
2388
  "path": "docs/architecture.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "de498901b8143d15779aafd30051f97e04ae01f32c880902da3a8234a6e262e7"
2925
+ "sha256": "f5e9ba961f5fad8945bbcba2e6daefdde31efba3e6e6b41dde6d52aa06b07065"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -7934,10 +7934,20 @@
7934
7934
  "mode": "0000644",
7935
7935
  "sha256": "18984c946d746da66ff7678379e9f49c9cfdcdb8f70d2acd852ab29c820baad0"
7936
7936
  },
7937
+ {
7938
+ "path": "modules/onboarding/howto-state.js",
7939
+ "mode": "0000644",
7940
+ "sha256": "9b6d361568feaa4f9a9f299bddcd4bc649861cf79a9a6045ea00389bd86cc01c"
7941
+ },
7942
+ {
7943
+ "path": "modules/onboarding/migrations/onboarding_001_howto_reads.sql",
7944
+ "mode": "0000644",
7945
+ "sha256": "cb5a3a58b4db75c028ccc53ea3316bb733fda650218b16b494d69de58c84e982"
7946
+ },
7937
7947
  {
7938
7948
  "path": "modules/onboarding/module.json",
7939
7949
  "mode": "0000644",
7940
- "sha256": "5ec905fe4a387bbb26ec157ac8d67a05bd02986fc661aa2621b84df260f45774"
7950
+ "sha256": "363a44b85c87bfbc2bb473656319099c39e7817e47294f3ceb4a33ee6bc85421"
7941
7951
  },
7942
7952
  {
7943
7953
  "path": "modules/onboarding/newcomer-restock.js",
@@ -7967,7 +7977,7 @@
7967
7977
  {
7968
7978
  "path": "modules/onboarding/routes/onboarding.js",
7969
7979
  "mode": "0000644",
7970
- "sha256": "385deea4dfc4e737f756dc56c1cd6c8891ebdb202e66cf15e235e6b10e498788"
7980
+ "sha256": "011dfc781cbe8c7a17956fa2020e0327cf0e46f537a9057cf2b4911752ba813a"
7971
7981
  },
7972
7982
  {
7973
7983
  "path": "modules/page-notes/CLAUDE.md",
@@ -10482,12 +10492,12 @@
10482
10492
  {
10483
10493
  "path": "package-lock.json",
10484
10494
  "mode": "0000644",
10485
- "sha256": "efe64aff087c9fac228bb0502df35fdb706ea66215ba9a7b20a16575b4568a6d"
10495
+ "sha256": "b7d2956b1155f633fbfbbfafe4211befef0d545b02671ce8cd9096ee22a1f734"
10486
10496
  },
10487
10497
  {
10488
10498
  "path": "package.json",
10489
10499
  "mode": "0000644",
10490
- "sha256": "65dbc0fd1642caa7dd0907f8ca5f8d6e3808087637f31aaff0b5b3b2e22da1da"
10500
+ "sha256": "4cbf0ddd56ffbee1d0d4cdd8388793c2590a243737608cf18556e0400e946fed"
10491
10501
  },
10492
10502
  {
10493
10503
  "path": "public-docs/index.html",
@@ -10507,7 +10517,7 @@
10507
10517
  {
10508
10518
  "path": "release-notes.json",
10509
10519
  "mode": "0000644",
10510
- "sha256": "b3c808cc70f6d027e4dc58fad374f2dff423b73416eee5c5a96e8779ddd2d04f"
10520
+ "sha256": "f5b81f238e17e854f85e2af52d9c3661acf519c8bd9634aaa95206e8f107bc5b"
10511
10521
  },
10512
10522
  {
10513
10523
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12857,7 +12867,7 @@
12857
12867
  {
12858
12868
  "path": "src/module-api.js",
12859
12869
  "mode": "0000644",
12860
- "sha256": "8ff71d0200dd2b11afa13be91155669caee7ba000a537df94e37426a6485e7f1"
12870
+ "sha256": "638e7c6e9ff2e460bacc69abc98b8f6721b6d2ade29ba79aa166920e1f71f7e7"
12861
12871
  },
12862
12872
  {
12863
12873
  "path": "src/module-loader/catalog.js",
@@ -15639,6 +15649,11 @@
15639
15649
  "mode": "0000644",
15640
15650
  "sha256": "8e9b206fdde738f289bc11e204852595e9128b523cfc20cd67e3e83fb60a2fac"
15641
15651
  },
15652
+ {
15653
+ "path": "tests/howto_state.mjs",
15654
+ "mode": "0000644",
15655
+ "sha256": "8b8d3eeeffd1bb7d0deaec51c91165b06302737b72b1a710547340c7ecadcec6"
15656
+ },
15642
15657
  {
15643
15658
  "path": "tests/html_comment_nesting.mjs",
15644
15659
  "mode": "0000644",
@@ -16722,7 +16737,7 @@
16722
16737
  {
16723
16738
  "path": "tests/own_scope_guard.mjs",
16724
16739
  "mode": "0000644",
16725
- "sha256": "e9ad80ce9837076d550dbb4b77a4c0f01f087ad3e515f8af8a672221af995ca4"
16740
+ "sha256": "cdd964e66b9ebe74c504a3916eb3a10b36497af4f5b94924534d2d73f136801a"
16726
16741
  },
16727
16742
  {
16728
16743
  "path": "tests/own_session_revoke.mjs",
@@ -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
- - 555 operations across 76 resource groups
8
+ - 558 operations across 76 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -735,6 +735,12 @@ function createClient(opts = {}) {
735
735
  getMeGuilds: (args) => request("GET", "/me/guilds", { hasBody: false }, args),
736
736
  // PATCH /me/handle — rank: any-builder — PATCH /me/handle
737
737
  patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
738
+ // GET /me/howto — rank: any-builder — GET /me/howto
739
+ getMeHowto: (args) => request("GET", "/me/howto", { hasBody: false }, args),
740
+ // POST /me/howto/read — rank: any-builder — POST /me/howto/read
741
+ postMeHowtoRead: (args) => request("POST", "/me/howto/read", { hasBody: true }, args),
742
+ // POST /me/howto/reset — rank: any-builder — POST /me/howto/reset
743
+ postMeHowtoReset: (args) => request("POST", "/me/howto/reset", { hasBody: true }, args),
738
744
  // GET /me/interaction — rank: any-builder — GET /me/interaction
739
745
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
740
746
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -734,6 +734,12 @@ function createClient(opts = {}) {
734
734
  getMeGuilds: (args) => request("GET", "/me/guilds", { hasBody: false }, args),
735
735
  // PATCH /me/handle — rank: any-builder — PATCH /me/handle
736
736
  patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
737
+ // GET /me/howto — rank: any-builder — GET /me/howto
738
+ getMeHowto: (args) => request("GET", "/me/howto", { hasBody: false }, args),
739
+ // POST /me/howto/read — rank: any-builder — POST /me/howto/read
740
+ postMeHowtoRead: (args) => request("POST", "/me/howto/read", { hasBody: true }, args),
741
+ // POST /me/howto/reset — rank: any-builder — POST /me/howto/reset
742
+ postMeHowtoReset: (args) => request("POST", "/me/howto/reset", { hasBody: true }, args),
737
743
  // GET /me/interaction — rank: any-builder — GET /me/interaction
738
744
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
739
745
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -429,6 +429,8 @@ export interface PostMeAnthropicLinkRequest { anthropic_email: string; anthropic
429
429
  export interface PostMeAnthropicLinkResponse { linked: unknown }
430
430
  export interface PostMeFilterViewsRequest { page: string; name: string; values: Record<string, unknown>; is_default?: boolean }
431
431
  export interface PostMeFilterViewsResponse { page: unknown; view: unknown; views: unknown }
432
+ export interface PostMeHowtoReadRequest { steps?: unknown[]; all?: boolean; how?: string }
433
+ export interface PostMeHowtoResetRequest { steps?: unknown[]; all?: boolean }
432
434
  export interface PostMeSessionsRevokeAllResponse { ok: boolean; revoked: unknown }
433
435
  export interface PostMeSessionsRevokeRequest { session_id?: StringifiableId }
434
436
  export interface PostMeSessionsRevokeResponse { ok: boolean; revoked: unknown }
@@ -1260,6 +1262,12 @@ export interface BongosClient {
1260
1262
  getMeGuilds(args?: RequestArgs): Promise<GetMeGuildsResponse>;
1261
1263
  /** PATCH /me/handle — rank: any-builder */
1262
1264
  patchMeHandle(args: RequestArgs & { body: PatchMeHandleRequest }): Promise<PatchMeHandleResponse>;
1265
+ /** GET /me/howto — rank: any-builder */
1266
+ getMeHowto(args?: RequestArgs): Promise<ApiResponse>;
1267
+ /** POST /me/howto/read — rank: any-builder */
1268
+ postMeHowtoRead(args?: RequestArgs & { body?: PostMeHowtoReadRequest }): Promise<ApiResponse>;
1269
+ /** POST /me/howto/reset — rank: any-builder */
1270
+ postMeHowtoReset(args?: RequestArgs & { body?: PostMeHowtoResetRequest }): Promise<ApiResponse>;
1263
1271
  /** GET /me/interaction — rank: any-builder */
1264
1272
  getMeInteraction(args?: RequestArgs): Promise<ApiResponse>;
1265
1273
  /** PATCH /me/interaction — rank: any-builder */
@@ -731,6 +731,12 @@ export function createClient(opts = {}) {
731
731
  getMeGuilds: (args) => request("GET", "/me/guilds", { hasBody: false }, args),
732
732
  // PATCH /me/handle — rank: any-builder — PATCH /me/handle
733
733
  patchMeHandle: (args) => request("PATCH", "/me/handle", { hasBody: true }, args),
734
+ // GET /me/howto — rank: any-builder — GET /me/howto
735
+ getMeHowto: (args) => request("GET", "/me/howto", { hasBody: false }, args),
736
+ // POST /me/howto/read — rank: any-builder — POST /me/howto/read
737
+ postMeHowtoRead: (args) => request("POST", "/me/howto/read", { hasBody: true }, args),
738
+ // POST /me/howto/reset — rank: any-builder — POST /me/howto/reset
739
+ postMeHowtoReset: (args) => request("POST", "/me/howto/reset", { hasBody: true }, args),
734
740
  // GET /me/interaction — rank: any-builder — GET /me/interaction
735
741
  getMeInteraction: (args) => request("GET", "/me/interaction", { hasBody: false }, args),
736
742
  // PATCH /me/interaction — rank: any-builder — PATCH /me/interaction
@@ -13518,6 +13518,121 @@
13518
13518
  ]
13519
13519
  }
13520
13520
  },
13521
+ "/me/howto": {
13522
+ "get": {
13523
+ "operationId": "get_me_howto",
13524
+ "tags": [
13525
+ "me"
13526
+ ],
13527
+ "summary": "GET /me/howto",
13528
+ "description": "the caller's own read state.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
13529
+ "x-rank": "any-builder",
13530
+ "x-source": "modules/onboarding/routes/onboarding.js",
13531
+ "responses": {
13532
+ "200": {
13533
+ "description": "Success."
13534
+ },
13535
+ "400": {
13536
+ "$ref": "#/components/responses/BadRequest"
13537
+ },
13538
+ "401": {
13539
+ "$ref": "#/components/responses/Unauthorized"
13540
+ },
13541
+ "403": {
13542
+ "$ref": "#/components/responses/Forbidden"
13543
+ }
13544
+ },
13545
+ "security": [
13546
+ {
13547
+ "builderSession": []
13548
+ }
13549
+ ]
13550
+ }
13551
+ },
13552
+ "/me/howto/read": {
13553
+ "post": {
13554
+ "operationId": "post_me_howto_read",
13555
+ "tags": [
13556
+ "me"
13557
+ ],
13558
+ "summary": "POST /me/howto/read",
13559
+ "description": "marks the caller's own steps read ({ steps }) or skips the guide ({ all: true, how: 'skipped' }).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
13560
+ "x-rank": "any-builder",
13561
+ "x-source": "modules/onboarding/routes/onboarding.js",
13562
+ "requestBody": {
13563
+ "required": false,
13564
+ "content": {
13565
+ "application/json": {
13566
+ "schema": {
13567
+ "$ref": "#/components/schemas/PostMeHowtoReadRequest"
13568
+ }
13569
+ }
13570
+ },
13571
+ "x-validated": true
13572
+ },
13573
+ "responses": {
13574
+ "200": {
13575
+ "description": "Success."
13576
+ },
13577
+ "400": {
13578
+ "$ref": "#/components/responses/ValidationFailed"
13579
+ },
13580
+ "401": {
13581
+ "$ref": "#/components/responses/Unauthorized"
13582
+ },
13583
+ "403": {
13584
+ "$ref": "#/components/responses/Forbidden"
13585
+ }
13586
+ },
13587
+ "security": [
13588
+ {
13589
+ "builderSession": []
13590
+ }
13591
+ ]
13592
+ }
13593
+ },
13594
+ "/me/howto/reset": {
13595
+ "post": {
13596
+ "operationId": "post_me_howto_reset",
13597
+ "tags": [
13598
+ "me"
13599
+ ],
13600
+ "summary": "POST /me/howto/reset",
13601
+ "description": "forgets the caller's own marks ({ steps }, or everything when none are named).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
13602
+ "x-rank": "any-builder",
13603
+ "x-source": "modules/onboarding/routes/onboarding.js",
13604
+ "requestBody": {
13605
+ "required": false,
13606
+ "content": {
13607
+ "application/json": {
13608
+ "schema": {
13609
+ "$ref": "#/components/schemas/PostMeHowtoResetRequest"
13610
+ }
13611
+ }
13612
+ },
13613
+ "x-validated": true
13614
+ },
13615
+ "responses": {
13616
+ "200": {
13617
+ "description": "Success."
13618
+ },
13619
+ "400": {
13620
+ "$ref": "#/components/responses/ValidationFailed"
13621
+ },
13622
+ "401": {
13623
+ "$ref": "#/components/responses/Unauthorized"
13624
+ },
13625
+ "403": {
13626
+ "$ref": "#/components/responses/Forbidden"
13627
+ }
13628
+ },
13629
+ "security": [
13630
+ {
13631
+ "builderSession": []
13632
+ }
13633
+ ]
13634
+ }
13635
+ },
13521
13636
  "/me/interaction": {
13522
13637
  "get": {
13523
13638
  "operationId": "get_me_interaction",
@@ -32403,6 +32518,36 @@
32403
32518
  "views"
32404
32519
  ]
32405
32520
  },
32521
+ "PostMeHowtoReadRequest": {
32522
+ "type": "object",
32523
+ "properties": {
32524
+ "steps": {
32525
+ "type": "array",
32526
+ "maxItems": 100
32527
+ },
32528
+ "all": {
32529
+ "type": "boolean"
32530
+ },
32531
+ "how": {
32532
+ "type": "string",
32533
+ "maxLength": 16
32534
+ }
32535
+ },
32536
+ "additionalProperties": false
32537
+ },
32538
+ "PostMeHowtoResetRequest": {
32539
+ "type": "object",
32540
+ "properties": {
32541
+ "steps": {
32542
+ "type": "array",
32543
+ "maxItems": 100
32544
+ },
32545
+ "all": {
32546
+ "type": "boolean"
32547
+ }
32548
+ },
32549
+ "additionalProperties": false
32550
+ },
32406
32551
  "PostMeSessionsRevokeAllResponse": {
32407
32552
  "type": "object",
32408
32553
  "properties": {
@@ -35469,8 +35614,8 @@
35469
35614
  "description": "A required dependency/feature is not configured or is temporarily down."
35470
35615
  }
35471
35616
  },
35472
- "x-endpoint-count": 555,
35473
- "x-schema-count": 592,
35617
+ "x-endpoint-count": 558,
35618
+ "x-schema-count": 594,
35474
35619
  "x-undocumented-bodies": 12,
35475
35620
  "x-response-schemas": 390,
35476
35621
  "x-generated-by": "scripts/gds/gen-api-docs.js"
@@ -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. 555 endpoints across 95 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 558 endpoints across 95 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`.
@@ -489,7 +489,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
489
489
  | POST | `/api/bongos/llm-cache/lookup` | `metic+archon` | `cache_key`, `tokens_saved_est` | POST /llm-cache/lookup — hit/miss + atomic hit_count bump. |
490
490
  | POST | `/api/bongos/llm-cache/store` | `metic+archon` | `spec`, `result_stdout`, `result_cost_usd` | POST /llm-cache/store — upsert a freshly-produced result. |
491
491
 
492
- ## `me` (39)
492
+ ## `me` (42)
493
493
 
494
494
  | Method | Path | Rank | Body | Description |
495
495
  |---|---|---|---|---|
@@ -512,6 +512,9 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
512
512
  | GET | `/api/bongos/me/guild-requests` | `any-builder` | — | MY queue: invites addressed to me, each carrying the card (the guild's current visibility, which an accept must echo back), the join requ… |
513
513
  | GET | `/api/bongos/me/guilds` | `any-builder` | — | MY guilds, each with my own two switches. |
514
514
  | PATCH | `/api/bongos/me/handle` | `any-builder` | `handle` | rename the caller's OWN handle (own data, keyed on the authenticated github_id). |
515
+ | GET | `/api/bongos/me/howto` | `any-builder` | — | the caller's own read state. |
516
+ | POST | `/api/bongos/me/howto/read` | `any-builder` | `steps`, `all`, `how` | marks the caller's own steps read ({ steps }) or skips the guide ({ all: true, how: 'skipped' }). |
517
+ | POST | `/api/bongos/me/howto/reset` | `any-builder` | `steps`, `all` | forgets the caller's own marks ({ steps }, or everything when none are named). |
515
518
  | GET | `/api/bongos/me/interaction` | `any-builder` | — | Per-builder INTERACTION PROFILE (task 2010, shares the behavior_prefs jsonb / migration 119). |
516
519
  | PATCH | `/api/bongos/me/interaction` | `any-builder` | _undocumented_ | PATCH the interaction profile. |
517
520
  | GET | `/api/bongos/me/model-allocation` | `any-builder` | — | |
@@ -3091,5 +3091,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3091
3091
  landed since 1.21.125 with no explicit bump. run 38761052564. (task 1002620)
3092
3092
  1.21.127 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3093
3093
  landed since 1.21.126 with no explicit bump. run 38764985266. (task 1002620)
3094
+ 1.21.128 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3095
+ landed since 1.21.127 with no explicit bump. run 38770118067. (task 1002620)
3094
3096
  ---------------------------------------------------------------------------
3095
3097
  ```
@@ -0,0 +1,108 @@
1
+ // modules/onboarding/howto-state.js — each person's read/unread state for the how-to steps (task 1004634)
2
+ //
3
+ // WHY. The how-to guide (howto-guide.js, task 1004627) is shown one step at a time
4
+ // at the moment it is useful, and a step once read must never be shown again — on
5
+ // this computer or any other, in Claude or another AI tool. So the read marks live in
6
+ // Bongos, per person (owner decision 2026-10-08), in `onboarding_howto_reads`
7
+ // (migrations/onboarding_001_howto_reads.sql): one row per step read, unread = no row.
8
+ //
9
+ // Three operations, served by routes/onboarding.js under /me/howto:
10
+ // • readState — every step with read / skipped / unread, the count, and the next unread
11
+ // • markRead — mark steps read (or skipped); `all: true` skips the whole guide
12
+ // • reset — forget some steps, or all of them, so they show again
13
+ // Step ids are checked against the guide: an id it does not name is refused, so a
14
+ // typo cannot store a mark nothing will ever read. Marking is idempotent and keeps the
15
+ // FIRST read time (a re-mark does not move it), except that reading a step you had
16
+ // skipped upgrades it to read.
17
+ //
18
+ // Pure of core require(): the pool comes from the doorway, or is injected (tests).
19
+
20
+ 'use strict';
21
+
22
+ const guide = require('./howto-guide');
23
+
24
+ const HOWS = Object.freeze(['read', 'skipped']);
25
+
26
+ function poolFor(deps) {
27
+ if (deps && deps.pool) return deps.pool;
28
+ return require('../../src/module-api').pool;
29
+ }
30
+
31
+ // Validate a list of step ids against the guide. Returns { ok, ids } or { ok:false, unknown }.
32
+ function checkSteps(ids) {
33
+ const list = [...new Set((Array.isArray(ids) ? ids : [ids]).map((s) => String(s || '').trim()).filter(Boolean))];
34
+ const unknown = list.filter((id) => !guide.stepById(id));
35
+ return unknown.length ? { ok: false, unknown } : { ok: true, ids: list };
36
+ }
37
+
38
+ // Pure: shape stored rows into the API answer, in guide order.
39
+ function shapeState(rows) {
40
+ const byId = new Map((rows || []).map((r) => [r.step_id, r]));
41
+ const steps = guide.STEPS.map((s) => {
42
+ const r = byId.get(s.id);
43
+ return {
44
+ id: s.id, moment: s.moment, title: s.title,
45
+ state: r ? (r.how === 'skipped' ? 'skipped' : 'read') : 'unread',
46
+ read_at: r ? new Date(r.read_at).toISOString() : null,
47
+ };
48
+ });
49
+ const done = steps.filter((s) => s.state !== 'unread');
50
+ const next = guide.nextUnread(done.map((s) => s.id));
51
+ return {
52
+ steps,
53
+ total: steps.length,
54
+ read: steps.filter((s) => s.state === 'read').length,
55
+ skipped: steps.filter((s) => s.state === 'skipped').length,
56
+ unread: steps.length - done.length,
57
+ next_unread: next ? next.id : null,
58
+ };
59
+ }
60
+
61
+ async function readState(builderId, deps = {}) {
62
+ const pool = poolFor(deps);
63
+ const { rows } = await pool.query(
64
+ 'SELECT step_id, how, read_at FROM onboarding_howto_reads WHERE builder_id = $1',
65
+ [builderId],
66
+ );
67
+ return shapeState(rows);
68
+ }
69
+
70
+ // { steps: [...] } or { all: true }; how = 'read' | 'skipped'.
71
+ async function markRead(builderId, { steps, all = false, how = 'read' } = {}, deps = {}) {
72
+ if (!HOWS.includes(how)) return { ok: false, reason: 'bad_how' };
73
+ let ids;
74
+ if (all) ids = guide.STEPS.map((s) => s.id);
75
+ else {
76
+ const c = checkSteps(steps);
77
+ if (!c.ok) return { ok: false, reason: 'unknown_step', unknown: c.unknown };
78
+ if (!c.ids.length) return { ok: false, reason: 'no_steps' };
79
+ ids = c.ids;
80
+ }
81
+ const pool = poolFor(deps);
82
+ // One statement: first read time is kept; a 'skipped' row becomes 'read' when the
83
+ // step is read for real, never the other way round.
84
+ await pool.query(
85
+ `INSERT INTO onboarding_howto_reads (builder_id, step_id, how)
86
+ SELECT $1, s, $3 FROM unnest($2::text[]) AS s
87
+ ON CONFLICT (builder_id, step_id) DO UPDATE
88
+ SET how = 'read'
89
+ WHERE onboarding_howto_reads.how = 'skipped' AND EXCLUDED.how = 'read'`,
90
+ [builderId, ids, how],
91
+ );
92
+ return { ok: true, state: await readState(builderId, deps) };
93
+ }
94
+
95
+ // { steps: [...] } forgets those; no steps (or all: true) forgets every mark.
96
+ async function reset(builderId, { steps, all = false } = {}, deps = {}) {
97
+ const pool = poolFor(deps);
98
+ if (all || steps == null || (Array.isArray(steps) && steps.length === 0)) {
99
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id = $1', [builderId]);
100
+ } else {
101
+ const c = checkSteps(steps);
102
+ if (!c.ok) return { ok: false, reason: 'unknown_step', unknown: c.unknown };
103
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id = $1 AND step_id = ANY($2::text[])', [builderId, c.ids]);
104
+ }
105
+ return { ok: true, state: await readState(builderId, deps) };
106
+ }
107
+
108
+ module.exports = { HOWS, checkSteps, shapeState, readState, markRead, reset };
@@ -0,0 +1,33 @@
1
+ -- onboarding_001_howto_reads.sql — which how-to steps each person has read (task 1004634).
2
+ --
3
+ -- The in-session how-to guide (modules/onboarding/howto-guide.js, task 1004627) is
4
+ -- walked one step at a time, and a step once read must never be shown again. The
5
+ -- owner's call (2026-10-08): the read state is PER PERSON and lives in Bongos, so it
6
+ -- follows them across computers and AI tools.
7
+ --
8
+ -- One row per (builder, step) that has been read. Unread = no row, so a step added to
9
+ -- the guide later is unread for everyone with no backfill, and a reset is a DELETE.
10
+ -- `step_id` is the guide's stable id; it is not constrained here because the guide is
11
+ -- code — the API refuses an id the guide does not name, and a row for a step the guide
12
+ -- later retires is simply ignored on read. `how` records whether the person read the
13
+ -- step or skipped it (task 1004636's "skip the tutorial"), so progress can tell them
14
+ -- apart.
15
+ --
16
+ -- ADDITIVE, module-owned (ADR 0083 §Decision #5). Idempotent: safe to re-run.
17
+
18
+ BEGIN;
19
+
20
+ CREATE TABLE IF NOT EXISTS onboarding_howto_reads (
21
+ builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE CASCADE,
22
+ step_id text NOT NULL,
23
+ how text NOT NULL DEFAULT 'read',
24
+ read_at timestamptz NOT NULL DEFAULT now(),
25
+ PRIMARY KEY (builder_id, step_id),
26
+ CONSTRAINT onboarding_howto_reads_how_chk CHECK (how IN ('read', 'skipped')),
27
+ CONSTRAINT onboarding_howto_reads_step_chk CHECK (step_id ~ '^[a-z][a-z0-9_]{2,39}$')
28
+ );
29
+
30
+ INSERT INTO schema_migrations (version) VALUES ('onboarding_001_howto_reads')
31
+ ON CONFLICT (version) DO NOTHING;
32
+
33
+ COMMIT;
@@ -6,7 +6,7 @@
6
6
  "coreVersion": "^1.19.532",
7
7
  "default": true,
8
8
  "maintenance": { "status": "core-maintained" },
9
- "contributes": { "routes": ["onboarding"] },
9
+ "contributes": { "routes": ["onboarding"], "migrations": true },
10
10
  "provides": ["onboarding"],
11
11
  "consumes": []
12
12
  }
@@ -37,6 +37,7 @@ const buildAccessRequestsRouter = require('./access-requests');
37
37
  const foundingBuilders = require('../founding-builders');
38
38
  const genesis = require('../genesis-home');
39
39
  const foundingBirth = require('../founding-birth');
40
+ const howtoState = require('../howto-state');
40
41
 
41
42
  // Register the `onboarding` kernel PORT. See the header + port.js for the full
42
43
  // consumer list and the graceful-degradation contract.
@@ -58,15 +59,61 @@ module.exports = function buildOnboardingRouter() {
58
59
  // ranks page and the home's founding wall; an empty list on a project that was
59
60
  // never founded, so those surfaces render exactly as before. Signed-in, like the
60
61
  // hall pages that read it; it carries nothing the roster does not already show.
61
- router.get('/founding/builders', api.requireBuilder, async (req, res) => {
62
+ router.get('/founding/builders', api.requireBuilder, async (_req, res) => {
62
63
  const builders = await foundingBuilders.foundingBuildersFor();
63
64
  res.set('Cache-Control', 'private, max-age=60');
64
65
  res.json({ builders });
65
66
  });
66
67
  mountGenesisRoutes(router);
68
+ mountHowtoRoutes(router);
67
69
  return router;
68
70
  };
69
71
 
72
+ // ── the how-to read state (task 1004634) ─────────────────────────────────────
73
+ // Each person's own read/unread marks for the in-session how-to guide. Self-scoped:
74
+ // every route reads and writes only req.builder's rows, so any signed-in builder may
75
+ // call them (the /me/wandering posture). The AI marks a step read after walking the
76
+ // person through it (task 1004635); reopen / continue / skip use the same three calls
77
+ // (task 1004636).
78
+ const HOWTO_REFUSALS = { unknown_step: 422, no_steps: 422, bad_how: 422 };
79
+
80
+ function mountHowtoRoutes(router) {
81
+ const handle = (fn) => async (req, res) => {
82
+ try {
83
+ const r = await fn(req);
84
+ if (r && r.ok === false) {
85
+ return res.status(HOWTO_REFUSALS[r.reason] || 422).json({
86
+ error: { code: r.reason, message: r.unknown ? `not a how-to step: ${r.unknown.join(', ')}` : r.reason },
87
+ });
88
+ }
89
+ res.set('Cache-Control', 'no-store');
90
+ res.json(r && r.state ? r.state : r);
91
+ } catch (err) {
92
+ api.logger('onboarding').error('[onboarding] howto', err);
93
+ res.status(500).json({ error: { code: 'howto_failed', message: 'internal error' } });
94
+ }
95
+ };
96
+ // rank: any-builder — the caller's own read state.
97
+ router.get('/me/howto', api.requireBuilder, handle((req) => howtoState.readState(req.builder.id)));
98
+ // rank: any-builder — marks the caller's own steps read ({ steps }) or skips the guide ({ all: true, how: 'skipped' }).
99
+ router.post('/me/howto/read', api.requireBuilder, (req, res, next) => {
100
+ if (api.validateOrRespond(req, res, {
101
+ steps: { type: 'array', maxItems: 100 },
102
+ all: { type: 'boolean' },
103
+ how: { type: 'string', maxLength: 16 },
104
+ })) return;
105
+ next();
106
+ }, handle((req) => howtoState.markRead(req.builder.id, req.body || {})));
107
+ // rank: any-builder — forgets the caller's own marks ({ steps }, or everything when none are named).
108
+ router.post('/me/howto/reset', api.requireBuilder, (req, res, next) => {
109
+ if (api.validateOrRespond(req, res, {
110
+ steps: { type: 'array', maxItems: 100 },
111
+ all: { type: 'boolean' },
112
+ })) return;
113
+ next();
114
+ }, handle((req) => howtoState.reset(req.builder.id, req.body || {})));
115
+ }
116
+
70
117
  // ── the genesis home (task 1004423) ──────────────────────────────────────────
71
118
  // Read by any signed-in builder of a founding project (everyone sees where genesis
72
119
  // is); every write is the project owner's, behind the same atom that guards the
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.127",
3
+ "version": "1.21.128",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.127",
9
+ "version": "1.21.128",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.127",
3
+ "version": "1.21.128",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9760,5 +9760,11 @@
9760
9760
  "id": "1004414",
9761
9761
  "text": "Each hosted project now has a memory and CPU limit, so one runaway project can no longer slow down every other project or the hub."
9762
9762
  }
9763
+ ],
9764
+ "1.21.128": [
9765
+ {
9766
+ "id": "1004634",
9767
+ "text": "Bongos now remembers, for each person, which how-to steps they have already seen, so the AI never shows the same tip twice, on any computer."
9768
+ }
9763
9769
  ]
9764
9770
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.127'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.128'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,178 @@
1
+ // tests/howto_state.mjs — per-person read/unread state for the how-to steps (task 1004634)
2
+ //
3
+ // Two halves. The DB-free half drives howto-state.js against an in-memory pool that
4
+ // honours the three statements it issues, so the contract runs everywhere: unread by
5
+ // default, mark read, skip all, a read upgrades a skip (never the reverse), the first
6
+ // read time is kept, reset some or all, and an id the guide does not name is refused.
7
+ // The DB half applies the real migration and runs the same calls against Postgres; it
8
+ // skips when no Postgres is reachable (DATABASE_URL), like copy_desk_flags_db.mjs.
9
+ //
10
+ // Run: node tests/howto_state.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import { createRequire } from 'node:module';
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import url from 'node:url';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const hs = require('../modules/onboarding/howto-state.js');
20
+ const guide = require('../modules/onboarding/howto-guide.js');
21
+ const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
22
+
23
+ let passed = 0;
24
+ let failed = 0;
25
+ async function test(name, fn) {
26
+ try { await fn(); passed++; console.log(` ok ${name}`); }
27
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
28
+ }
29
+
30
+ // An in-memory stand-in for the three statements howto-state.js runs.
31
+ function fakePool() {
32
+ const rows = new Map(); // `${b}|${step}` → { builder_id, step_id, how, read_at }
33
+ let clock = 0;
34
+ return {
35
+ rows,
36
+ async query(sql, params) {
37
+ if (/^SELECT step_id/.test(sql)) {
38
+ return { rows: [...rows.values()].filter((r) => r.builder_id === params[0]) };
39
+ }
40
+ if (/^INSERT INTO onboarding_howto_reads/.test(sql)) {
41
+ const [b, ids, how] = params;
42
+ for (const s of ids) {
43
+ const k = `${b}|${s}`;
44
+ const cur = rows.get(k);
45
+ if (!cur) rows.set(k, { builder_id: b, step_id: s, how, read_at: new Date(1e12 + ++clock * 1000) });
46
+ else if (cur.how === 'skipped' && how === 'read') cur.how = 'read';
47
+ }
48
+ return { rows: [] };
49
+ }
50
+ if (/^DELETE FROM onboarding_howto_reads WHERE builder_id = \$1 AND/.test(sql)) {
51
+ for (const s of params[1]) rows.delete(`${params[0]}|${s}`);
52
+ return { rows: [] };
53
+ }
54
+ if (/^DELETE FROM onboarding_howto_reads WHERE builder_id = \$1$/.test(sql)) {
55
+ for (const [k, r] of rows) if (r.builder_id === params[0]) rows.delete(k);
56
+ return { rows: [] };
57
+ }
58
+ throw new Error(`unexpected SQL: ${sql}`);
59
+ },
60
+ };
61
+ }
62
+
63
+ async function contract(label, deps, A, B) {
64
+ const first = guide.STEPS[0].id;
65
+ const second = guide.STEPS[1].id;
66
+
67
+ await test(`${label}: a new person has every step unread, next is the first`, async () => {
68
+ const s = await hs.readState(A, deps);
69
+ assert.equal(s.total, guide.STEPS.length);
70
+ assert.equal(s.unread, s.total);
71
+ assert.equal(s.next_unread, first);
72
+ assert.ok(s.steps.every((x) => x.state === 'unread' && x.read_at === null));
73
+ });
74
+
75
+ await test(`${label}: mark one read → it is read, next moves on, other people untouched`, async () => {
76
+ const r = await hs.markRead(A, { steps: [first] }, deps);
77
+ assert.equal(r.ok, true);
78
+ assert.equal(r.state.read, 1);
79
+ assert.equal(r.state.next_unread, second);
80
+ assert.equal(r.state.steps[0].state, 'read');
81
+ assert.ok(r.state.steps[0].read_at);
82
+ const other = await hs.readState(B, deps);
83
+ assert.equal(other.read, 0, 'per person');
84
+ });
85
+
86
+ await test(`${label}: re-marking keeps the first read time (idempotent)`, async () => {
87
+ const before = (await hs.readState(A, deps)).steps[0].read_at;
88
+ await hs.markRead(A, { steps: [first] }, deps);
89
+ assert.equal((await hs.readState(A, deps)).steps[0].read_at, before);
90
+ });
91
+
92
+ await test(`${label}: an unknown step id is refused and nothing is stored`, async () => {
93
+ const r = await hs.markRead(A, { steps: [second, 'not_a_step'] }, deps);
94
+ assert.equal(r.ok, false);
95
+ assert.equal(r.reason, 'unknown_step');
96
+ assert.deepEqual(r.unknown, ['not_a_step']);
97
+ assert.equal((await hs.readState(A, deps)).read, 1);
98
+ assert.equal((await hs.markRead(A, { steps: [] }, deps)).reason, 'no_steps');
99
+ assert.equal((await hs.markRead(A, { steps: [second], how: 'seen' }, deps)).reason, 'bad_how');
100
+ });
101
+
102
+ await test(`${label}: skip all marks every other step skipped; nothing left unread`, async () => {
103
+ const r = await hs.markRead(A, { all: true, how: 'skipped' }, deps);
104
+ assert.equal(r.state.unread, 0);
105
+ assert.equal(r.state.next_unread, null);
106
+ assert.equal(r.state.read, 1, 'the step already read stays read');
107
+ assert.equal(r.state.skipped, guide.STEPS.length - 1);
108
+ });
109
+
110
+ await test(`${label}: reading a skipped step upgrades it; skipping a read one does not downgrade`, async () => {
111
+ await hs.markRead(A, { steps: [second] }, deps);
112
+ let s = await hs.readState(A, deps);
113
+ assert.equal(s.steps[1].state, 'read');
114
+ await hs.markRead(A, { steps: [second], how: 'skipped' }, deps);
115
+ s = await hs.readState(A, deps);
116
+ assert.equal(s.steps[1].state, 'read');
117
+ });
118
+
119
+ await test(`${label}: reset some steps, then reset everything`, async () => {
120
+ let r = await hs.reset(A, { steps: [first] }, deps);
121
+ assert.equal(r.state.steps[0].state, 'unread');
122
+ assert.equal(r.state.next_unread, first);
123
+ assert.equal((await hs.reset(A, { steps: ['nope_nope'] }, deps)).reason, 'unknown_step');
124
+ r = await hs.reset(A, {}, deps);
125
+ assert.equal(r.state.unread, guide.STEPS.length);
126
+ });
127
+ }
128
+
129
+ await test('shapeState ignores a stored mark for a step the guide no longer has', async () => {
130
+ const s = hs.shapeState([{ step_id: 'retired_step', how: 'read', read_at: new Date() }]);
131
+ assert.equal(s.read, 0);
132
+ assert.equal(s.total, guide.STEPS.length);
133
+ });
134
+
135
+ await test('the module declares its migration and the routes are mounted', async () => {
136
+ const mod = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/onboarding/module.json'), 'utf8'));
137
+ assert.equal(mod.contributes.migrations, true);
138
+ assert.ok(fs.existsSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql')));
139
+ const src = fs.readFileSync(path.join(ROOT, 'modules/onboarding/routes/onboarding.js'), 'utf8');
140
+ for (const r of ["router.get('/me/howto'", "router.post('/me/howto/read'", "router.post('/me/howto/reset'"]) assert.ok(src.includes(r), r);
141
+ });
142
+
143
+ await contract('in-memory', { pool: fakePool() }, 1, 2);
144
+
145
+ // ── the real database ────────────────────────────────────────────────────────
146
+ const { Pool } = require('pg');
147
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
148
+ let dbUp = true;
149
+ try { await pool.query('SELECT 1'); } catch (e) {
150
+ dbUp = false;
151
+ console.log(` (db half SKIPPED — no reachable Postgres: ${e.code || e.message}. Set DATABASE_URL to run.)`);
152
+ }
153
+ if (dbUp) {
154
+ await pool.query(`
155
+ CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
156
+ CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text);
157
+ `);
158
+ await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8'));
159
+ await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8')); // idempotent
160
+ const A = 910000001;
161
+ const B = 910000002;
162
+ await pool.query('INSERT INTO builders (id, github_login) VALUES ($1, $2), ($3, $4) ON CONFLICT (id) DO NOTHING', [A, 'howto-a', B, 'howto-b']);
163
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]);
164
+ try {
165
+ await contract('postgres', { pool }, A, B);
166
+ await test('postgres: the CHECK refuses a malformed step id and a bad how', async () => {
167
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id) VALUES ($1, 'Bad Id')", [A]));
168
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id, how) VALUES ($1, 'welcome', 'seen')", [A]));
169
+ });
170
+ } finally {
171
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]).catch(() => {});
172
+ await pool.query('DELETE FROM builders WHERE id IN ($1, $2)', [A, B]).catch(() => {});
173
+ }
174
+ }
175
+ await pool.end().catch(() => {});
176
+
177
+ console.log(`\n${passed} passed, ${failed} failed`);
178
+ if (failed) process.exit(1);
@@ -164,6 +164,12 @@ const OWN_SCOPED_SURFACES = [
164
164
  },
165
165
  ],
166
166
  },
167
+ {
168
+ file: 'modules/onboarding/routes/onboarding.js',
169
+ why: "the how-to read state (task 1004634). GET /me/howto and POST /me/howto/read|reset read and write the CALLER's own marks, keyed on req.builder.id; the body names step ids, never a builder. The rest of the router is /founding/*, whose one path param names a founding STAGE, not a builder.",
170
+ minOwnScoped: 1,
171
+ crossOwner: [],
172
+ },
167
173
  ];
168
174
 
169
175
  // Floors that keep this guard from going quietly vacuous (a regex that stops