@bongos/core 1.19.513 → 1.19.515

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.
@@ -161,6 +161,10 @@
161
161
  "name": "memory",
162
162
  "description": "Endpoints under `/api/bongos/memory`."
163
163
  },
164
+ {
165
+ "name": "mingle",
166
+ "description": "Endpoints under `/api/bongos/mingle`."
167
+ },
164
168
  {
165
169
  "name": "modules",
166
170
  "description": "Endpoints under `/api/bongos/modules`."
@@ -10054,6 +10058,253 @@
10054
10058
  ]
10055
10059
  }
10056
10060
  },
10061
+ "/mingle": {
10062
+ "get": {
10063
+ "operationId": "get_mingle",
10064
+ "tags": [
10065
+ "mingle"
10066
+ ],
10067
+ "summary": "GET /mingle",
10068
+ "description": ", and OWN-SCOPED — your own state, never someone else's. Who else has opted in to being introduced is a fact about them.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
10069
+ "x-rank": "any-builder",
10070
+ "x-source": "modules/lifecycle/routes/mingle.js",
10071
+ "responses": {
10072
+ "200": {
10073
+ "description": "Success.",
10074
+ "content": {
10075
+ "application/json": {
10076
+ "schema": {
10077
+ "$ref": "#/components/schemas/GetMingleResponse"
10078
+ }
10079
+ }
10080
+ }
10081
+ },
10082
+ "400": {
10083
+ "$ref": "#/components/responses/BadRequest"
10084
+ },
10085
+ "401": {
10086
+ "$ref": "#/components/responses/Unauthorized"
10087
+ },
10088
+ "403": {
10089
+ "$ref": "#/components/responses/Forbidden"
10090
+ }
10091
+ },
10092
+ "security": [
10093
+ {
10094
+ "builderSession": []
10095
+ }
10096
+ ]
10097
+ }
10098
+ },
10099
+ "/mingle/opt-in": {
10100
+ "put": {
10101
+ "operationId": "put_mingle_opt_in",
10102
+ "tags": [
10103
+ "mingle"
10104
+ ],
10105
+ "summary": "PUT /mingle/opt-in",
10106
+ "description": ", OWN-SCOPED. Keyed by the SESSION, never a body field — consent you can give on someone else's behalf is not consent.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
10107
+ "x-rank": "any-builder",
10108
+ "x-source": "modules/lifecycle/routes/mingle.js",
10109
+ "requestBody": {
10110
+ "required": true,
10111
+ "content": {
10112
+ "application/json": {
10113
+ "schema": {
10114
+ "$ref": "#/components/schemas/PutMingleOptInRequest"
10115
+ }
10116
+ }
10117
+ },
10118
+ "x-validated": true
10119
+ },
10120
+ "responses": {
10121
+ "200": {
10122
+ "description": "Success.",
10123
+ "content": {
10124
+ "application/json": {
10125
+ "schema": {
10126
+ "$ref": "#/components/schemas/PutMingleOptInResponse"
10127
+ }
10128
+ }
10129
+ }
10130
+ },
10131
+ "400": {
10132
+ "$ref": "#/components/responses/ValidationFailed"
10133
+ },
10134
+ "401": {
10135
+ "$ref": "#/components/responses/Unauthorized"
10136
+ },
10137
+ "403": {
10138
+ "$ref": "#/components/responses/Forbidden"
10139
+ }
10140
+ },
10141
+ "security": [
10142
+ {
10143
+ "builderSession": []
10144
+ }
10145
+ ]
10146
+ }
10147
+ },
10148
+ "/mingle/pairs/{id}/decide": {
10149
+ "post": {
10150
+ "operationId": "post_mingle_pairs_id_decide",
10151
+ "tags": [
10152
+ "mingle"
10153
+ ],
10154
+ "summary": "POST /mingle/pairs/:id/decide",
10155
+ "description": ". The UPDATE itself requires the caller to be one of the two people in the pair, so a builder cannot answer an introduction they are not part of — enforced in SQL, not by a check here that a later caller could skip.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
10156
+ "x-rank": "any-builder",
10157
+ "x-source": "modules/lifecycle/routes/mingle.js",
10158
+ "parameters": [
10159
+ {
10160
+ "name": "id",
10161
+ "in": "path",
10162
+ "required": true,
10163
+ "schema": {
10164
+ "type": "string"
10165
+ },
10166
+ "description": "Path parameter `id`."
10167
+ }
10168
+ ],
10169
+ "requestBody": {
10170
+ "required": true,
10171
+ "content": {
10172
+ "application/json": {
10173
+ "schema": {
10174
+ "$ref": "#/components/schemas/PostMinglePairsIdDecideRequest"
10175
+ }
10176
+ }
10177
+ },
10178
+ "x-validated": true
10179
+ },
10180
+ "responses": {
10181
+ "200": {
10182
+ "description": "Success.",
10183
+ "content": {
10184
+ "application/json": {
10185
+ "schema": {
10186
+ "$ref": "#/components/schemas/PostMinglePairsIdDecideResponse"
10187
+ }
10188
+ }
10189
+ }
10190
+ },
10191
+ "400": {
10192
+ "$ref": "#/components/responses/ValidationFailed"
10193
+ },
10194
+ "401": {
10195
+ "$ref": "#/components/responses/Unauthorized"
10196
+ },
10197
+ "403": {
10198
+ "$ref": "#/components/responses/Forbidden"
10199
+ },
10200
+ "404": {
10201
+ "$ref": "#/components/responses/NotFound"
10202
+ }
10203
+ },
10204
+ "security": [
10205
+ {
10206
+ "builderSession": []
10207
+ }
10208
+ ]
10209
+ }
10210
+ },
10211
+ "/mingle/project": {
10212
+ "put": {
10213
+ "operationId": "put_mingle_project",
10214
+ "tags": [
10215
+ "mingle"
10216
+ ],
10217
+ "summary": "PUT /mingle/project",
10218
+ "description": "perm: mingle.manage (floor metic) — see the header. Offering the feature to the project is an operator act; it still cannot pair anyone who has not opted themselves in.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `mingle.manage` (all required).",
10219
+ "x-rank": "metic+archon",
10220
+ "x-source": "modules/lifecycle/routes/mingle.js",
10221
+ "x-permissions": [
10222
+ "mingle.manage"
10223
+ ],
10224
+ "requestBody": {
10225
+ "required": true,
10226
+ "content": {
10227
+ "application/json": {
10228
+ "schema": {
10229
+ "$ref": "#/components/schemas/PutMingleProjectRequest"
10230
+ }
10231
+ }
10232
+ },
10233
+ "x-validated": true
10234
+ },
10235
+ "responses": {
10236
+ "200": {
10237
+ "description": "Success."
10238
+ },
10239
+ "400": {
10240
+ "$ref": "#/components/responses/ValidationFailed"
10241
+ },
10242
+ "401": {
10243
+ "$ref": "#/components/responses/Unauthorized"
10244
+ },
10245
+ "403": {
10246
+ "$ref": "#/components/responses/Forbidden"
10247
+ }
10248
+ },
10249
+ "security": [
10250
+ {
10251
+ "builderSession": []
10252
+ }
10253
+ ]
10254
+ }
10255
+ },
10256
+ "/mingle/round": {
10257
+ "post": {
10258
+ "operationId": "post_mingle_round",
10259
+ "tags": [
10260
+ "mingle"
10261
+ ],
10262
+ "summary": "POST /mingle/round",
10263
+ "description": "perm: mingle.manage — running a round writes introductions for other people, so it is an operator action rather than something a builder does to themselves. Refuses on its own when the project switch is off (runMingleRound checks).\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `mingle.manage` (all required).",
10264
+ "x-rank": "metic+archon",
10265
+ "x-source": "modules/lifecycle/routes/mingle.js",
10266
+ "x-permissions": [
10267
+ "mingle.manage"
10268
+ ],
10269
+ "requestBody": {
10270
+ "required": false,
10271
+ "content": {
10272
+ "application/json": {
10273
+ "schema": {
10274
+ "$ref": "#/components/schemas/PostMingleRoundRequest"
10275
+ }
10276
+ }
10277
+ },
10278
+ "x-validated": true
10279
+ },
10280
+ "responses": {
10281
+ "200": {
10282
+ "description": "Success.",
10283
+ "content": {
10284
+ "application/json": {
10285
+ "schema": {
10286
+ "$ref": "#/components/schemas/PostMingleRoundResponse"
10287
+ }
10288
+ }
10289
+ }
10290
+ },
10291
+ "400": {
10292
+ "$ref": "#/components/responses/ValidationFailed"
10293
+ },
10294
+ "401": {
10295
+ "$ref": "#/components/responses/Unauthorized"
10296
+ },
10297
+ "403": {
10298
+ "$ref": "#/components/responses/Forbidden"
10299
+ }
10300
+ },
10301
+ "security": [
10302
+ {
10303
+ "builderSession": []
10304
+ }
10305
+ ]
10306
+ }
10307
+ },
10057
10308
  "/modules": {
10058
10309
  "get": {
10059
10310
  "operationId": "get_modules",
@@ -10786,7 +11037,7 @@
10786
11037
  "projects"
10787
11038
  ],
10788
11039
  "summary": "GET /projects/featured",
10789
- "description": "a curated public showcase; no auth by design.\n\n**Rank:** `public` — No authentication — any caller.",
11040
+ "description": "the public map of projects; no auth by design.\n\n**Rank:** `public` — No authentication — any caller.",
10790
11041
  "x-rank": "public",
10791
11042
  "x-source": "modules/platform-identity/routes/projects.js",
10792
11043
  "responses": {
@@ -16840,6 +17091,19 @@
16840
17091
  "files"
16841
17092
  ]
16842
17093
  },
17094
+ "GetMingleResponse": {
17095
+ "type": "object",
17096
+ "properties": {
17097
+ "mingle": {},
17098
+ "open_pairs": {},
17099
+ "matching": {}
17100
+ },
17101
+ "required": [
17102
+ "mingle",
17103
+ "open_pairs",
17104
+ "matching"
17105
+ ]
17106
+ },
16843
17107
  "GetModulesResponse": {
16844
17108
  "type": "object",
16845
17109
  "properties": {
@@ -20219,6 +20483,54 @@
20219
20483
  },
20220
20484
  "additionalProperties": true
20221
20485
  },
20486
+ "PostMinglePairsIdDecideRequest": {
20487
+ "type": "object",
20488
+ "properties": {
20489
+ "decision": {
20490
+ "type": "string",
20491
+ "enum": [
20492
+ "accepted",
20493
+ "declined"
20494
+ ]
20495
+ }
20496
+ },
20497
+ "required": [
20498
+ "decision"
20499
+ ],
20500
+ "additionalProperties": false
20501
+ },
20502
+ "PostMinglePairsIdDecideResponse": {
20503
+ "type": "object",
20504
+ "properties": {
20505
+ "pair": {}
20506
+ },
20507
+ "required": [
20508
+ "pair"
20509
+ ]
20510
+ },
20511
+ "PostMingleRoundRequest": {
20512
+ "type": "object",
20513
+ "properties": {
20514
+ "limit": {
20515
+ "type": "integer",
20516
+ "minimum": 1
20517
+ }
20518
+ },
20519
+ "additionalProperties": false
20520
+ },
20521
+ "PostMingleRoundResponse": {
20522
+ "type": "object",
20523
+ "properties": {
20524
+ "paired": {},
20525
+ "count": {},
20526
+ "skipped": {}
20527
+ },
20528
+ "required": [
20529
+ "paired",
20530
+ "count",
20531
+ "skipped"
20532
+ ]
20533
+ },
20222
20534
  "PostModulesKeyEnableRequest": {
20223
20535
  "type": "object",
20224
20536
  "properties": {
@@ -21683,6 +21995,41 @@
21683
21995
  "granted"
21684
21996
  ]
21685
21997
  },
21998
+ "PutMingleOptInRequest": {
21999
+ "type": "object",
22000
+ "properties": {
22001
+ "opted_in": {
22002
+ "type": "boolean"
22003
+ }
22004
+ },
22005
+ "required": [
22006
+ "opted_in"
22007
+ ],
22008
+ "additionalProperties": false
22009
+ },
22010
+ "PutMingleOptInResponse": {
22011
+ "type": "object",
22012
+ "properties": {
22013
+ "opted_in": {},
22014
+ "updated_at": {}
22015
+ },
22016
+ "required": [
22017
+ "opted_in",
22018
+ "updated_at"
22019
+ ]
22020
+ },
22021
+ "PutMingleProjectRequest": {
22022
+ "type": "object",
22023
+ "properties": {
22024
+ "enabled": {
22025
+ "type": "boolean"
22026
+ }
22027
+ },
22028
+ "required": [
22029
+ "enabled"
22030
+ ],
22031
+ "additionalProperties": false
22032
+ },
21686
22033
  "PutTasksIdDependenciesRequest": {
21687
22034
  "type": "object",
21688
22035
  "properties": {
@@ -21834,9 +22181,9 @@
21834
22181
  "description": "A required dependency/feature is not configured or is temporarily down."
21835
22182
  }
21836
22183
  },
21837
- "x-endpoint-count": 347,
21838
- "x-schema-count": 379,
22184
+ "x-endpoint-count": 352,
22185
+ "x-schema-count": 387,
21839
22186
  "x-undocumented-bodies": 9,
21840
- "x-response-schemas": 259,
22187
+ "x-response-schemas": 263,
21841
22188
  "x-generated-by": "scripts/gds/gen-api-docs.js"
21842
22189
  }
@@ -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. 347 endpoints across 56 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 352 endpoints across 57 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`.
@@ -399,6 +399,16 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
399
399
  | GET | `/api/bongos/memory/files` | `any-builder` | — | GET /memory/files — list the AUTHENTICATED builder's memory files (metadata only, no content). |
400
400
  | POST | `/api/bongos/memory/sync` | `any-builder` | `files` | POST /memory/sync — push a builder's local memory files to the server. |
401
401
 
402
+ ## `mingle` (5)
403
+
404
+ | Method | Path | Rank | Body | Description |
405
+ |---|---|---|---|---|
406
+ | GET | `/api/bongos/mingle` | `any-builder` | — | , and OWN-SCOPED — your own state, never someone else's. |
407
+ | PUT | `/api/bongos/mingle/opt-in` | `any-builder` | `opted_in` | , OWN-SCOPED. |
408
+ | POST | `/api/bongos/mingle/pairs/:id/decide` | `any-builder` | `decision` | . |
409
+ | PUT | `/api/bongos/mingle/project` | `metic+archon` | `enabled` | perm: mingle.manage (floor metic) — see the header. |
410
+ | POST | `/api/bongos/mingle/round` | `metic+archon` | `limit` | perm: mingle.manage — running a round writes introductions for other people, so it is an operator action rather than something a builder … |
411
+
402
412
  ## `modules` (4)
403
413
 
404
414
  | Method | Path | Rank | Body | Description |
@@ -439,7 +449,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
439
449
  | GET | `/api/bongos/projects` | `metic+archon` | — | the full curation list (includes unfeatured + owner attribution). |
440
450
  | POST | `/api/bongos/projects` | `metic+archon` | `origin`, `name`, `tagline`, `description`, `art_url`, `type`, `template` | create / update a catalog entry (manual seed for hand-wired instances; federated projects auto-catalogue at registration). |
441
451
  | PATCH | `/api/bongos/projects/:id/featured` | `metic+archon` | `featured` | the feature / unfeature control (the ADR 0141 §5 curation gate), BOUNDED BY THE PUBLISH GATE (ADR 0182 D3, task 1002335). |
442
- | GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
452
+ | GET | `/api/bongos/projects/featured` | `public` | — | the public map of projects; no auth by design. |
443
453
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
444
454
 
445
455
  ## `provisioning` (21)
@@ -1463,5 +1463,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1463
1463
  landed since 1.19.511 with no explicit bump. run 33793208383. (task 1002620)
1464
1464
  1.19.513 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1465
1465
  landed since 1.19.512 with no explicit bump. run 33795977371. (task 1002620)
1466
+ 1.19.514 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1467
+ landed since 1.19.513 with no explicit bump. run 33796959752. (task 1002620)
1468
+ 1.19.515 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1469
+ landed since 1.19.514 with no explicit bump. run 33797825811. (task 1002620)
1466
1470
  ---------------------------------------------------------------------------
1467
1471
  ```
@@ -587,6 +587,18 @@ function createClient(opts = {}) {
587
587
  // POST /memory/sync — rank: any-builder — POST /memory/sync
588
588
  postMemorySync: (args) => request("POST", "/memory/sync", { hasBody: true }, args),
589
589
  },
590
+ "mingle": {
591
+ // GET /mingle — rank: any-builder — GET /mingle
592
+ getMingle: (args) => request("GET", "/mingle", { hasBody: false }, args),
593
+ // PUT /mingle/opt-in — rank: any-builder — PUT /mingle/opt-in
594
+ putMingleOptIn: (args) => request("PUT", "/mingle/opt-in", { hasBody: true }, args),
595
+ // POST /mingle/pairs/{id}/decide — rank: any-builder — POST /mingle/pairs/:id/decide
596
+ postMinglePairsIdDecide: (args) => request("POST", "/mingle/pairs/{id}/decide", { hasBody: true }, args),
597
+ // PUT /mingle/project — rank: metic+archon — PUT /mingle/project
598
+ putMingleProject: (args) => request("PUT", "/mingle/project", { hasBody: true }, args),
599
+ // POST /mingle/round — rank: metic+archon — POST /mingle/round
600
+ postMingleRound: (args) => request("POST", "/mingle/round", { hasBody: true }, args),
601
+ },
590
602
  "modules": {
591
603
  // GET /modules — rank: any-builder — GET /modules
592
604
  getModules: (args) => request("GET", "/modules", { hasBody: false }, args),
@@ -194,6 +194,12 @@ const PERMISSIONS = [
194
194
  { key: 'box.fleet.manage', system: false, floor: 'metic', scope: 'any', resource: 'box', guards: 'GET /boxes[/cost-ledger], POST /boxes/:b/close, PATCH .../block (ADR 0157, was archon)' },
195
195
  { key: 'autonomy.precheck', system: false, floor: 'metic', guards: 'GET /autonomy/precheck — unsupervised-runner gate (ADR 0157, was archon)' },
196
196
  { key: 'discord.channels.manage', system: false, floor: 'metic', guards: 'GET /discord/channels/plan, POST .../reconcile (ADR 0157, was archon)' },
197
+ // Metic by the discord.channels.manage analogue — configuring a project-wide
198
+ // social surface — rather than archon. The blast radius is bounded by the OTHER
199
+ // switch: enabling mingle offers it, and can never pair a builder who has not
200
+ // personally opted in (task 1003177, ADR 0187 §5), so this atom cannot put
201
+ // anyone in a room with a stranger on its own.
202
+ { key: 'mingle.manage', system: false, floor: 'metic', guards: 'PUT /mingle/project (the project half of mingle consent), POST /mingle/round (run a pairing round) — task 1003177, goal 1000075' },
197
203
  { key: 'project.curate', system: false, floor: 'metic', guards: 'platform-identity: GET/POST /projects, /projects/invite, PATCH /projects/:id/featured, GET /scouting (ADR 0157, was archon)' },
198
204
  { key: 'override_request.decide', system: false, floor: 'metic', guards: 'GET /override-requests, POST .../:id/decide (ADR 0157, was archon)' },
199
205
  { key: 'claim.release.any', system: false, floor: 'metic', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf (ADR 0157, was archon)' },
@@ -0,0 +1,55 @@
1
+ -- government_009_mingle_manage.sql — grant the new `mingle.manage` atom to the
2
+ -- Metic and Archon rank-roles (task 1003177 / C07, goal 1000075; ADR 0187 §5).
3
+ --
4
+ -- Mingle's two operator routes — `PUT /mingle/project` (the PROJECT half of
5
+ -- mingle consent) and `POST /mingle/round` (run a pairing round) — need a gate,
6
+ -- and BV1.R105 says a modules/*/routes gate is a permission atom and never a
7
+ -- rank: tests/government_require_permission.mjs fails the build on a new
8
+ -- `requireRank`, and the two files still spelling it that way are documented
9
+ -- holdouts rather than a pattern to copy.
10
+ --
11
+ -- FLOOR `metic`, by the `discord.channels.manage` analogue — configuring a
12
+ -- project-wide social surface — rather than archon. What makes that safe is
13
+ -- mingle's OTHER switch: enabling mingle only OFFERS it, and the matcher can never
14
+ -- pair a builder who has not personally opted in (lifecycle_mingle_optin), so this
15
+ -- atom cannot put anyone in a room with a stranger on its own.
16
+ --
17
+ -- A BRAND-NEW key: no rank holds it yet, so BOTH rungs the floor implies are
18
+ -- granted here. RANK_SEED is a cumulative superset — floor `metic` means
19
+ -- {metic, archon} — and the drift guard (tests/government_seed.mjs) compares the
20
+ -- UNION of the run-once seed plus every later grant migration against that
21
+ -- floor-derived seed, per rank. Minting the catalog row WITHOUT this migration is
22
+ -- exactly what that guard caught on this task.
23
+ --
24
+ -- COPY government_004_board_permissions.sql, NOT governance_004_criterion_create.
25
+ -- The `governance_*` grant migrations predate government_001_rename_from_governance,
26
+ -- which renamed `governance_role_permissions` → `government_rank_permissions` and
27
+ -- its `role_key` column → `rank_key`, with NO compatibility shim. Copying one of
28
+ -- those files verbatim — which the first draft of THIS migration did — writes to a
29
+ -- table that no longer exists, so it errors at apply time on any database that
30
+ -- runs migrations in filename order, a fresh install included. The drift guard
31
+ -- above does not catch it: it is a regex over `SELECT '<rank>', unnest(ARRAY[…])`
32
+ -- and never looks at the target table (tests/mingle.mjs now checks that part).
33
+ --
34
+ -- Additive only: one new permission key granted to two ranks; nothing is revoked.
35
+ -- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
36
+ --
37
+ -- One block PER RANK, in the government_004 shape: the drift guard scans for
38
+ -- `SELECT '<rank>', unnest(ARRAY[…])` literally, so a combined
39
+ -- `unnest(ARRAY['metic','archon']), 'key'` form parses as zero grants and reds it.
40
+
41
+ BEGIN;
42
+
43
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
44
+ SELECT 'metic', unnest(ARRAY[
45
+ 'mingle.manage'
46
+ ])
47
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
48
+
49
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
50
+ SELECT 'archon', unnest(ARRAY[
51
+ 'mingle.manage'
52
+ ])
53
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
54
+
55
+ COMMIT;
@@ -0,0 +1,134 @@
1
+ -- lifecycle_007_mingle.sql — mingle: pairing two builders who do NOT share a craft,
2
+ -- and only when both the project and the person said yes (task 1003177 / C07,
3
+ -- goal 1000075; criterion collab-mingle-two-switches, ADR 0187 §5).
4
+ --
5
+ -- TWO SWITCHES, AND THIS TABLE IS ONLY THE SECOND ONE. Consent is the project
6
+ -- enabling mingle AND the individual opting in. The PROJECT switch is not here —
7
+ -- it is `project_settings['mingle.enabled']` (migration core_231), because a
8
+ -- project-wide knob belongs in the project's own store and forking a second one is
9
+ -- what src/bongos/project-settings.js exists to prevent. This table is the
10
+ -- per-builder half. Both are read on every pairing; either one off means no pair.
11
+ --
12
+ -- WHY AN EXPLICIT ROW RATHER THAN A behavior_prefs KEY. Opting in to be
13
+ -- auto-introduced to a stranger a few times a week is a real-world social
14
+ -- commitment, so the record of it should say WHEN it was given and be legible on
15
+ -- its own — not be one key inside a jsonb blob of unrelated UI knobs. It also
16
+ -- keeps the write inside this module: lifecycle may READ core `builders`, but the
17
+ -- behaviour prefs belong to builder-settings and writing them from here would
18
+ -- reach across a module boundary (ADR 0083).
19
+ --
20
+ -- FAIL CLOSED, both halves. No row means NOT participating — never "hasn't said
21
+ -- no". `opted_in = false` is a deliberate withdrawal and is kept rather than
22
+ -- deleted, so the surface can tell "asked and declined the whole feature" from
23
+ -- "never offered it", the same distinction ADR 0171 draws between a hidden account
24
+ -- and one that never federated.
25
+ --
26
+ -- MODULE-OWNED and `lifecycle_`-prefixed, so fitness check 11 permits it (ADR 0083
27
+ -- §Decision #5). Mirrors lifecycle_004_help_requests and lifecycle_005_task_-
28
+ -- recommendations, its siblings under this goal.
29
+
30
+ CREATE TABLE IF NOT EXISTS lifecycle_mingle_optin (
31
+ builder_id bigint PRIMARY KEY REFERENCES builders(id),
32
+
33
+ -- The switch itself. See the header: absent ≠ false ≠ true.
34
+ opted_in boolean NOT NULL,
35
+
36
+ -- When consent was last changed. The criterion calls mingle a real-world social
37
+ -- commitment; a consent record with no timestamp cannot answer "since when".
38
+ updated_at timestamptz NOT NULL DEFAULT now()
39
+ );
40
+
41
+ -- The matcher's read: everyone currently participating.
42
+ CREATE INDEX IF NOT EXISTS lifecycle_mingle_optin_in_idx
43
+ ON lifecycle_mingle_optin (builder_id) WHERE opted_in = true;
44
+
45
+ COMMENT ON TABLE lifecycle_mingle_optin IS
46
+ 'The per-builder half of mingle consent (task 1003177). The project half is project_settings[''mingle.enabled'']; both must be on. No row means not participating.';
47
+
48
+
49
+ -- ---------------------------------------------------------------------------
50
+ -- The pairs
51
+ -- ---------------------------------------------------------------------------
52
+ --
53
+ -- A PROPOSAL, never an obligation. A row here says "you two were introduced"; it
54
+ -- is declinable, and declining is terminal and free.
55
+ --
56
+ -- DECLINING COSTS NOTHING, AND THE SCHEMA IS WHERE THAT IS TRUE. There is no
57
+ -- decline counter, no cooldown column, no cost or streak field on either party,
58
+ -- and no karma or credit reference anywhere in this file — a table that recorded
59
+ -- a decline ANYWHERE BUT HERE would be the penalty the criterion forbids. This is
60
+ -- the same posture lifecycle_005 takes for a declined recommendation, deliberately
61
+ -- restated rather than cross-referenced, because the next person to add a column
62
+ -- reads this file and not that one.
63
+ --
64
+ -- Nothing reads `status` back into matching, either: the matcher's eligibility read
65
+ -- never joins this table, so a decline cannot make the decliner less likely to be
66
+ -- paired again. That is the "no future matching weight" half of the criterion, and
67
+ -- it is a property of the READ, not a promise made here.
68
+
69
+ CREATE TABLE IF NOT EXISTS lifecycle_mingle_pairs (
70
+ id bigserial PRIMARY KEY,
71
+
72
+ -- Ordered low/high at write time so a pair has ONE row whichever way round it
73
+ -- was generated, and the open-pair unique index below actually bites.
74
+ builder_lo bigint NOT NULL REFERENCES builders(id),
75
+ builder_hi bigint NOT NULL REFERENCES builders(id),
76
+
77
+ -- The crafts that made this pair legal, recorded AS MATCHED. The criterion asks
78
+ -- for cross-craft "chosen deliberately and recorded"; storing the two crafts
79
+ -- means a pair can be audited later even if a builder changes their disciplines
80
+ -- afterwards, which a re-derivation from builders.preferred_disciplines could
81
+ -- not do.
82
+ craft_lo text NOT NULL,
83
+ craft_hi text NOT NULL,
84
+
85
+ -- 'proposed' — introduced, nobody has answered
86
+ -- 'accepted' — someone said yes
87
+ -- 'declined' — someone said no. Terminal, and carries NO consequence.
88
+ status text NOT NULL DEFAULT 'proposed',
89
+
90
+ -- Who answered. A record of what happened, never an input to matching.
91
+ decided_by bigint REFERENCES builders(id),
92
+ decided_at timestamptz,
93
+
94
+ created_at timestamptz NOT NULL DEFAULT now(),
95
+
96
+ CONSTRAINT lifecycle_mingle_pairs_status_chk
97
+ CHECK (status IN ('proposed', 'accepted', 'declined')),
98
+
99
+ -- Pairing someone with themselves is not a pairing.
100
+ CONSTRAINT lifecycle_mingle_pairs_not_self_chk
101
+ CHECK (builder_lo <> builder_hi),
102
+
103
+ -- The ordering invariant the open-pair index depends on.
104
+ CONSTRAINT lifecycle_mingle_pairs_ordered_chk
105
+ CHECK (builder_lo < builder_hi),
106
+
107
+ -- CROSS-CRAFT IS A DATABASE CONSTRAINT, not only a matcher rule. The criterion's
108
+ -- first testable claim is "a pair is only ever cross-craft"; enforcing it solely
109
+ -- in the matcher would leave any future writer — a backfill, a second caller, a
110
+ -- fixture — free to violate the one property the feature is named for.
111
+ CONSTRAINT lifecycle_mingle_pairs_cross_craft_chk
112
+ CHECK (craft_lo <> craft_hi),
113
+
114
+ CONSTRAINT lifecycle_mingle_pairs_decided_chk
115
+ CHECK ((status = 'proposed') = (decided_at IS NULL))
116
+ );
117
+
118
+ -- One OPEN proposal per pair. Partial, so a pair CAN be introduced again in a later
119
+ -- round after it was settled — a permanent unique key would make a single decline
120
+ -- final between two people forever, which is a consequence, and declining is free.
121
+ CREATE UNIQUE INDEX IF NOT EXISTS lifecycle_mingle_pairs_open_uniq
122
+ ON lifecycle_mingle_pairs (builder_lo, builder_hi) WHERE status = 'proposed';
123
+
124
+ -- "What am I being introduced to right now" — both sides of an open pair.
125
+ CREATE INDEX IF NOT EXISTS lifecycle_mingle_pairs_open_lo_idx
126
+ ON lifecycle_mingle_pairs (builder_lo) WHERE status = 'proposed';
127
+ CREATE INDEX IF NOT EXISTS lifecycle_mingle_pairs_open_hi_idx
128
+ ON lifecycle_mingle_pairs (builder_hi) WHERE status = 'proposed';
129
+
130
+ COMMENT ON TABLE lifecycle_mingle_pairs IS
131
+ 'A cross-craft mingle introduction (task 1003177). Declinable, and a decline is terminal, free, and never read back into matching.';
132
+
133
+ INSERT INTO schema_migrations (version) VALUES ('lifecycle_007_mingle')
134
+ ON CONFLICT (version) DO NOTHING;