@bongos/core 1.19.358 → 1.19.359

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.
@@ -205,6 +205,10 @@
205
205
  "name": "sso",
206
206
  "description": "Endpoints under `/api/bongos/sso`."
207
207
  },
208
+ {
209
+ "name": "task-recommendations",
210
+ "description": "Endpoints under `/api/bongos/task-recommendations`."
211
+ },
208
212
  {
209
213
  "name": "task-visuals",
210
214
  "description": "Endpoints under `/api/bongos/task-visuals`."
@@ -12092,6 +12096,194 @@
12092
12096
  "security": []
12093
12097
  }
12094
12098
  },
12099
+ "/task-recommendations": {
12100
+ "post": {
12101
+ "operationId": "post_task_recommendations",
12102
+ "tags": [
12103
+ "task-recommendations"
12104
+ ],
12105
+ "summary": "POST /task-recommendations",
12106
+ "description": "recommending is not a privileged act. It grants the recipient nothing they did not already have (the task was claimable by them or it was not), so gating it by rank would only stop newcomers from pointing at something useful.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12107
+ "x-rank": "any-builder",
12108
+ "x-source": "modules/lifecycle/routes/recommendations.js",
12109
+ "requestBody": {
12110
+ "required": true,
12111
+ "content": {
12112
+ "application/json": {
12113
+ "schema": {
12114
+ "$ref": "#/components/schemas/PostTaskRecommendationsRequest"
12115
+ }
12116
+ }
12117
+ },
12118
+ "x-validated": true
12119
+ },
12120
+ "responses": {
12121
+ "200": {
12122
+ "description": "Success.",
12123
+ "content": {
12124
+ "application/json": {
12125
+ "schema": {
12126
+ "$ref": "#/components/schemas/PostTaskRecommendationsResponse"
12127
+ }
12128
+ }
12129
+ }
12130
+ },
12131
+ "400": {
12132
+ "$ref": "#/components/responses/ValidationFailed"
12133
+ },
12134
+ "401": {
12135
+ "$ref": "#/components/responses/Unauthorized"
12136
+ },
12137
+ "403": {
12138
+ "$ref": "#/components/responses/Forbidden"
12139
+ }
12140
+ },
12141
+ "security": [
12142
+ {
12143
+ "builderSession": []
12144
+ }
12145
+ ]
12146
+ }
12147
+ },
12148
+ "/task-recommendations/for-me": {
12149
+ "get": {
12150
+ "operationId": "get_task_recommendations_for_me",
12151
+ "tags": [
12152
+ "task-recommendations"
12153
+ ],
12154
+ "summary": "GET /task-recommendations/for-me",
12155
+ "description": "you only ever see what is addressed to you. NOTE WHAT THIS RESPONSE DOES NOT CONTAIN: any signal that the recipient now holds the task. It carries the task's own status so the recipient can judge, and that is all. Claiming is a separate, deliberate act on their part.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12156
+ "x-rank": "any-builder",
12157
+ "x-source": "modules/lifecycle/routes/recommendations.js",
12158
+ "responses": {
12159
+ "200": {
12160
+ "description": "Success.",
12161
+ "content": {
12162
+ "application/json": {
12163
+ "schema": {
12164
+ "$ref": "#/components/schemas/GetTaskRecommendationsForMeResponse"
12165
+ }
12166
+ }
12167
+ }
12168
+ },
12169
+ "400": {
12170
+ "$ref": "#/components/responses/BadRequest"
12171
+ },
12172
+ "401": {
12173
+ "$ref": "#/components/responses/Unauthorized"
12174
+ },
12175
+ "403": {
12176
+ "$ref": "#/components/responses/Forbidden"
12177
+ }
12178
+ },
12179
+ "security": [
12180
+ {
12181
+ "builderSession": []
12182
+ }
12183
+ ]
12184
+ }
12185
+ },
12186
+ "/task-recommendations/mine": {
12187
+ "get": {
12188
+ "operationId": "get_task_recommendations_mine",
12189
+ "tags": [
12190
+ "task-recommendations"
12191
+ ],
12192
+ "summary": "GET /task-recommendations/mine",
12193
+ "description": "what I recommended, and what became of it. Unfiltered by default: a declined recommendation stays readable, which is the criterion's \"the recommendation and its author are readable afterwards\".\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12194
+ "x-rank": "any-builder",
12195
+ "x-source": "modules/lifecycle/routes/recommendations.js",
12196
+ "responses": {
12197
+ "200": {
12198
+ "description": "Success.",
12199
+ "content": {
12200
+ "application/json": {
12201
+ "schema": {
12202
+ "$ref": "#/components/schemas/GetTaskRecommendationsMineResponse"
12203
+ }
12204
+ }
12205
+ }
12206
+ },
12207
+ "400": {
12208
+ "$ref": "#/components/responses/BadRequest"
12209
+ },
12210
+ "401": {
12211
+ "$ref": "#/components/responses/Unauthorized"
12212
+ },
12213
+ "403": {
12214
+ "$ref": "#/components/responses/Forbidden"
12215
+ }
12216
+ },
12217
+ "security": [
12218
+ {
12219
+ "builderSession": []
12220
+ }
12221
+ ]
12222
+ }
12223
+ },
12224
+ "/task-recommendations/{id}": {
12225
+ "patch": {
12226
+ "operationId": "patch_task_recommendations_id",
12227
+ "tags": [
12228
+ "task-recommendations"
12229
+ ],
12230
+ "summary": "PATCH /task-recommendations/:id",
12231
+ "description": "Decline. Costs nothing, and this route is where that is visible: it flips a status and returns the row. No karma call, no counter, no notification to the sender beyond the row itself turning up in their own list. rank: any-builder — ownership is enforced in the UPDATE's WHERE, not by rank.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
12232
+ "x-rank": "any-builder",
12233
+ "x-source": "modules/lifecycle/routes/recommendations.js",
12234
+ "parameters": [
12235
+ {
12236
+ "name": "id",
12237
+ "in": "path",
12238
+ "required": true,
12239
+ "schema": {
12240
+ "type": "string"
12241
+ },
12242
+ "description": "Path parameter `id`."
12243
+ }
12244
+ ],
12245
+ "requestBody": {
12246
+ "required": true,
12247
+ "content": {
12248
+ "application/json": {
12249
+ "schema": {
12250
+ "$ref": "#/components/schemas/PatchTaskRecommendationsIdRequest"
12251
+ }
12252
+ }
12253
+ },
12254
+ "x-validated": true
12255
+ },
12256
+ "responses": {
12257
+ "200": {
12258
+ "description": "Success.",
12259
+ "content": {
12260
+ "application/json": {
12261
+ "schema": {
12262
+ "$ref": "#/components/schemas/PatchTaskRecommendationsIdResponse"
12263
+ }
12264
+ }
12265
+ }
12266
+ },
12267
+ "400": {
12268
+ "$ref": "#/components/responses/ValidationFailed"
12269
+ },
12270
+ "401": {
12271
+ "$ref": "#/components/responses/Unauthorized"
12272
+ },
12273
+ "403": {
12274
+ "$ref": "#/components/responses/Forbidden"
12275
+ },
12276
+ "404": {
12277
+ "$ref": "#/components/responses/NotFound"
12278
+ }
12279
+ },
12280
+ "security": [
12281
+ {
12282
+ "builderSession": []
12283
+ }
12284
+ ]
12285
+ }
12286
+ },
12095
12287
  "/task-visuals/{name}": {
12096
12288
  "get": {
12097
12289
  "operationId": "get_task_visuals_name",
@@ -13431,6 +13623,58 @@
13431
13623
  ]
13432
13624
  }
13433
13625
  },
13626
+ "/tasks/{id}/recommendations": {
13627
+ "get": {
13628
+ "operationId": "get_tasks_id_recommendations",
13629
+ "tags": [
13630
+ "tasks"
13631
+ ],
13632
+ "summary": "GET /tasks/:id/recommendations",
13633
+ "description": "The per-task audit read: who recommended this task, to whom, and why. rank: any-builder, and deliberately NOT scoped to the two parties. The criterion's argument for a ledger is that an untraced recommendation is indistinguishable from a task quietly changing hands — a trace only the two people involved can read does not answer that. Tasks are already visible to every builder, and a recommendation says nothing more private than \"this person thought that person should look at this\".\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
13634
+ "x-rank": "any-builder",
13635
+ "x-source": "modules/lifecycle/routes/recommendations.js",
13636
+ "parameters": [
13637
+ {
13638
+ "name": "id",
13639
+ "in": "path",
13640
+ "required": true,
13641
+ "schema": {
13642
+ "type": "string"
13643
+ },
13644
+ "description": "Path parameter `id`."
13645
+ }
13646
+ ],
13647
+ "responses": {
13648
+ "200": {
13649
+ "description": "Success.",
13650
+ "content": {
13651
+ "application/json": {
13652
+ "schema": {
13653
+ "$ref": "#/components/schemas/GetTasksIdRecommendationsResponse"
13654
+ }
13655
+ }
13656
+ }
13657
+ },
13658
+ "400": {
13659
+ "$ref": "#/components/responses/BadRequest"
13660
+ },
13661
+ "401": {
13662
+ "$ref": "#/components/responses/Unauthorized"
13663
+ },
13664
+ "403": {
13665
+ "$ref": "#/components/responses/Forbidden"
13666
+ },
13667
+ "404": {
13668
+ "$ref": "#/components/responses/NotFound"
13669
+ }
13670
+ },
13671
+ "security": [
13672
+ {
13673
+ "builderSession": []
13674
+ }
13675
+ ]
13676
+ }
13677
+ },
13434
13678
  "/tasks/{id}/ship": {
13435
13679
  "post": {
13436
13680
  "operationId": "post_tasks_id_ship",
@@ -15307,6 +15551,28 @@
15307
15551
  "public_key_b64"
15308
15552
  ]
15309
15553
  },
15554
+ "GetTaskRecommendationsForMeResponse": {
15555
+ "type": "object",
15556
+ "properties": {
15557
+ "recommendations": {},
15558
+ "count": {}
15559
+ },
15560
+ "required": [
15561
+ "recommendations",
15562
+ "count"
15563
+ ]
15564
+ },
15565
+ "GetTaskRecommendationsMineResponse": {
15566
+ "type": "object",
15567
+ "properties": {
15568
+ "recommendations": {},
15569
+ "count": {}
15570
+ },
15571
+ "required": [
15572
+ "recommendations",
15573
+ "count"
15574
+ ]
15575
+ },
15310
15576
  "GetTasksClaimableResponse": {
15311
15577
  "type": "object",
15312
15578
  "properties": {
@@ -15338,6 +15604,17 @@
15338
15604
  "dependents"
15339
15605
  ]
15340
15606
  },
15607
+ "GetTasksIdRecommendationsResponse": {
15608
+ "type": "object",
15609
+ "properties": {
15610
+ "recommendations": {},
15611
+ "count": {}
15612
+ },
15613
+ "required": [
15614
+ "recommendations",
15615
+ "count"
15616
+ ]
15617
+ },
15341
15618
  "GetTasksNewcomerFloorResponse": {
15342
15619
  "type": "object",
15343
15620
  "properties": {
@@ -15925,6 +16202,35 @@
15925
16202
  "push"
15926
16203
  ]
15927
16204
  },
16205
+ "PatchTaskRecommendationsIdRequest": {
16206
+ "type": "object",
16207
+ "properties": {
16208
+ "status": {
16209
+ "type": "string",
16210
+ "enum": [
16211
+ "declined",
16212
+ "withdrawn"
16213
+ ]
16214
+ }
16215
+ },
16216
+ "required": [
16217
+ "status"
16218
+ ],
16219
+ "additionalProperties": false
16220
+ },
16221
+ "PatchTaskRecommendationsIdResponse": {
16222
+ "type": "object",
16223
+ "properties": {
16224
+ "ok": {
16225
+ "type": "boolean"
16226
+ },
16227
+ "recommendation": {}
16228
+ },
16229
+ "required": [
16230
+ "ok",
16231
+ "recommendation"
16232
+ ]
16233
+ },
15928
16234
  "PatchTasksIdRequest": {
15929
16235
  "type": "object",
15930
16236
  "properties": {
@@ -18777,6 +19083,42 @@
18777
19083
  "avatar_url"
18778
19084
  ]
18779
19085
  },
19086
+ "PostTaskRecommendationsRequest": {
19087
+ "type": "object",
19088
+ "properties": {
19089
+ "task_id": {
19090
+ "type": "integer",
19091
+ "minimum": 1
19092
+ },
19093
+ "recommended_to": {
19094
+ "type": "integer",
19095
+ "minimum": 1
19096
+ },
19097
+ "reason": {
19098
+ "type": "string",
19099
+ "minLength": 1
19100
+ }
19101
+ },
19102
+ "required": [
19103
+ "task_id",
19104
+ "recommended_to",
19105
+ "reason"
19106
+ ],
19107
+ "additionalProperties": false
19108
+ },
19109
+ "PostTaskRecommendationsResponse": {
19110
+ "type": "object",
19111
+ "properties": {
19112
+ "ok": {
19113
+ "type": "boolean"
19114
+ },
19115
+ "recommendation": {}
19116
+ },
19117
+ "required": [
19118
+ "ok",
19119
+ "recommendation"
19120
+ ]
19121
+ },
18780
19122
  "PostTasksIdAbandonRequest": {
18781
19123
  "type": "object",
18782
19124
  "properties": {
@@ -19452,9 +19794,9 @@
19452
19794
  "description": "A required dependency/feature is not configured or is temporarily down."
19453
19795
  }
19454
19796
  },
19455
- "x-endpoint-count": 307,
19456
- "x-schema-count": 343,
19797
+ "x-endpoint-count": 312,
19798
+ "x-schema-count": 350,
19457
19799
  "x-undocumented-bodies": 8,
19458
- "x-response-schemas": 229,
19800
+ "x-response-schemas": 234,
19459
19801
  "x-generated-by": "scripts/gds/gen-api-docs.js"
19460
19802
  }
@@ -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. 307 endpoints across 50 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 312 endpoints across 51 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`.
@@ -507,13 +507,22 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
507
507
  | GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
508
508
  | POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
509
509
 
510
+ ## `task-recommendations` (4)
511
+
512
+ | Method | Path | Rank | Body | Description |
513
+ |---|---|---|---|---|
514
+ | POST | `/api/bongos/task-recommendations` | `any-builder` | `task_id`, `recommended_to`, `reason` | recommending is not a privileged act. |
515
+ | PATCH | `/api/bongos/task-recommendations/:id` | `any-builder` | `status` | Decline. |
516
+ | GET | `/api/bongos/task-recommendations/for-me` | `any-builder` | — | you only ever see what is addressed to you. |
517
+ | GET | `/api/bongos/task-recommendations/mine` | `any-builder` | — | what I recommended, and what became of it. |
518
+
510
519
  ## `task-visuals` (1)
511
520
 
512
521
  | Method | Path | Rank | Body | Description |
513
522
  |---|---|---|---|---|
514
523
  | GET | `/api/bongos/task-visuals/:name` | `public` | — | the WIDEST path this route can take, declared honestly. |
515
524
 
516
- ## `tasks` (29)
525
+ ## `tasks` (30)
517
526
 
518
527
  | Method | Path | Rank | Body | Description |
519
528
  |---|---|---|---|---|
@@ -537,6 +546,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
537
546
  | POST | `/api/bongos/tasks/:id/promote` | `metic+archon` | — | backlog→ready promotion; scope mutation; ADR 0090 (was archon). |
538
547
  | POST | `/api/bongos/tasks/:id/publish-branch` | `any-builder` | `branch`, `head_sha`, `bundle_b64`, `value_summary` | |
539
548
  | GET | `/api/bongos/tasks/:id/publish-status` | `any-builder` | — | GET /tasks/:id/publish-status?branch=<branch> — poll the PR + prod-deploy state for a server-mediated publish so ship.js can confirm the … |
549
+ | GET | `/api/bongos/tasks/:id/recommendations` | `any-builder` | — | The per-task audit read: who recommended this task, to whom, and why. |
540
550
  | POST | `/api/bongos/tasks/:id/ship` | `any-builder` | validated | confirmed → shipped. |
541
551
  | POST | `/api/bongos/tasks/:id/visual` | `any-builder` | `image_b64`, `content_type`, `alt` | POST /tasks/:id/visual — attach (or replace) this task's ship-time visual. |
542
552
  | DELETE | `/api/bongos/tasks/:id/visual` | `any-builder` | — | DELETE /tasks/:id/visual — remove it again. |
@@ -121,7 +121,34 @@ function helpRequestsNeed(ctx) {
121
121
 
122
122
  // The registry. To add a future input the system needs from a builder, append a
123
123
  // pure (ctx) => need|null function here — every renderer picks it up for free.
124
- const NEEDS = [artKeyNeed, helpRequestsNeed];
124
+ // Open task RECOMMENDATIONS waiting on this builder (task 1003173 / C03, goal
125
+ // 1000075; criterion collab-pass-a-task). Sibling of helpRequestsNeed above, and
126
+ // here for the same reason C01's grader round put that one here: a recommendation
127
+ // nobody surfaces is merely stored, and the criterion's whole argument is that a
128
+ // recommendation must be a visible, traceable judgement rather than a task
129
+ // quietly changing hands.
130
+ //
131
+ // 'info', NOT a demand, and the message says so. A recommendation grants the
132
+ // recipient nothing and obliges them to nothing — they still claim it themselves
133
+ // if they want it, and declining costs nothing (ADR 0187 §3). Wording that
134
+ // pressured the reader would BE the penalty the decision forbids.
135
+ function taskRecommendationsNeed(ctx) {
136
+ const n = Number(ctx.openTaskRecommendations) || 0;
137
+ if (n <= 0) return null;
138
+ const subject = n === 1 ? 'A task' : `${n} tasks`;
139
+ const verb = n === 1 ? 'was' : 'were';
140
+ return {
141
+ id: 'task_recommendations',
142
+ title: n === 1 ? 'Someone recommended a task to you' : 'Tasks were recommended to you',
143
+ what: 'Another builder thought you should look at a task, and said why. It is a suggestion, not an assignment.',
144
+ state: 'action_needed',
145
+ severity: 'info',
146
+ message: `${subject} ${verb} recommended to you. Nothing has been assigned — you would still claim it yourself, and declining costs nothing.`,
147
+ action: { label: n === 1 ? 'See the recommendation' : 'See the recommendations', href: '/#/collab' },
148
+ };
149
+ }
150
+
151
+ const NEEDS = [artKeyNeed, helpRequestsNeed, taskRecommendationsNeed];
125
152
 
126
153
  // ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
127
154
  //
@@ -199,7 +226,7 @@ module.exports = {
199
226
  computeNeeds,
200
227
  // exported for tests / direct reuse
201
228
  artKeyNeed,
202
- helpRequestsNeed,
229
+ helpRequestsNeed, taskRecommendationsNeed,
203
230
  foundingGraceActive,
204
231
  FOUNDING_GRACE_DAYS,
205
232
  SETTINGS_PATH,
@@ -715,6 +715,16 @@ function createClient(opts = {}) {
715
715
  // POST /sso/token — rank: public — POST /sso/token
716
716
  postSsoToken: (args) => request("POST", "/sso/token", { hasBody: true }, args),
717
717
  },
718
+ "taskRecommendations": {
719
+ // POST /task-recommendations — rank: any-builder — POST /task-recommendations
720
+ postTaskRecommendations: (args) => request("POST", "/task-recommendations", { hasBody: true }, args),
721
+ // PATCH /task-recommendations/{id} — rank: any-builder — PATCH /task-recommendations/:id
722
+ patchTaskRecommendationsId: (args) => request("PATCH", "/task-recommendations/{id}", { hasBody: true }, args),
723
+ // GET /task-recommendations/for-me — rank: any-builder — GET /task-recommendations/for-me
724
+ getTaskRecommendationsForMe: (args) => request("GET", "/task-recommendations/for-me", { hasBody: false }, args),
725
+ // GET /task-recommendations/mine — rank: any-builder — GET /task-recommendations/mine
726
+ getTaskRecommendationsMine: (args) => request("GET", "/task-recommendations/mine", { hasBody: false }, args),
727
+ },
718
728
  "taskVisuals": {
719
729
  // GET /task-visuals/{name} — rank: public — GET /task-visuals/:name
720
730
  getTaskVisualsName: (args) => request("GET", "/task-visuals/{name}", { hasBody: false }, args),
@@ -760,6 +770,8 @@ function createClient(opts = {}) {
760
770
  postTasksIdPublishBranch: (args) => request("POST", "/tasks/{id}/publish-branch", { hasBody: true }, args),
761
771
  // GET /tasks/{id}/publish-status — rank: any-builder — GET /tasks/:id/publish-status
762
772
  getTasksIdPublishStatus: (args) => request("GET", "/tasks/{id}/publish-status", { hasBody: false }, args),
773
+ // GET /tasks/{id}/recommendations — rank: any-builder — GET /tasks/:id/recommendations
774
+ getTasksIdRecommendations: (args) => request("GET", "/tasks/{id}/recommendations", { hasBody: false }, args),
763
775
  // POST /tasks/{id}/ship — rank: any-builder — POST /tasks/:id/ship
764
776
  postTasksIdShip: (args) => request("POST", "/tasks/{id}/ship", { hasBody: true }, args),
765
777
  // DELETE /tasks/{id}/visual — rank: any-builder — DELETE /tasks/:id/visual
@@ -19,6 +19,7 @@
19
19
  const db = require('./db');
20
20
  const classifier = require('./task-classifier');
21
21
  const helpRequests = require('./help-requests');
22
+ const taskRecommendations = require('./task-recommendations');
22
23
 
23
24
  module.exports = {
24
25
  // --- retired doorway leaks (ADR 0093 §3) ---------------------------------
@@ -39,6 +40,10 @@ module.exports = {
39
40
  // task 1003171 (C01): the count of open help requests addressed to a builder,
40
41
  // so the kernel's /me can put it in the needs ctx WITHOUT importing this module.
41
42
  openHelpRequestsFor: (...a) => helpRequests.countOpenForBuilder(...a),
43
+ // task 1003173 (C03): the count of open task recommendations waiting on a
44
+ // builder, exposed the same way and for the same reason — a recommendation
45
+ // nobody surfaces is merely stored.
46
+ openTaskRecommendationsFor: (...a) => taskRecommendations.countOpenForRecipient(...a),
42
47
  getTask: (...a) => db.getTask(...a),
43
48
  listVersions: (...a) => db.listVersions(...a),
44
49
  versionProgress: (...a) => db.versionProgress(...a),
@@ -0,0 +1,90 @@
1
+ -- lifecycle_005_task_recommendations.sql — "you should look at this": recommending
2
+ -- a task to someone, which is NOT handing it to them (task 1003173 / C03,
3
+ -- goal 1000075; criterion collab-pass-a-task, ADR 0187 §3).
4
+ --
5
+ -- THE DECISION THIS TABLE ENCODES. Passing a task RECOMMENDS it; it never
6
+ -- transfers the claim. The recipient still claims it themselves, so claim/release
7
+ -- stays the single path to holding a task and nobody acquires work without opting
8
+ -- in. There is deliberately NO transfer column, NO 'accepted' status that grants
9
+ -- anything, and no seam for a future transfer: the decision was recommend-only,
10
+ -- and an unused seam is a cost that later reads as an intention.
11
+ --
12
+ -- WHY THE REASON IS NOT NULLABLE. The criterion's own argument is that a
13
+ -- recommendation which leaves no trace is indistinguishable from a task quietly
14
+ -- changing hands. A reason the sender must write is what makes the ledger a
15
+ -- record of a judgement rather than a nudge — so it is NOT NULL with a non-blank
16
+ -- CHECK, the same shape lifecycle_help_requests gives what_is_stuck.
17
+ --
18
+ -- DECLINING COSTS NOTHING, and that is why 'declined' is just a status here and
19
+ -- nothing else in the schema references it. There is no counter, no cooldown, no
20
+ -- penalty column on either party, and no karma or credit link — a table that
21
+ -- recorded a decline anywhere but here would be the penalty the criterion forbids.
22
+ --
23
+ -- MODULE-OWNED and `lifecycle_`-prefixed, so fitness check 11 permits it: a NEW
24
+ -- table the lifecycle module owns outright, unlike `tasks`/`builders` which are
25
+ -- core tables it only references (ADR 0083 §Decision #5). Mirrors
26
+ -- lifecycle_004_help_requests, its sibling under this goal.
27
+
28
+ CREATE TABLE IF NOT EXISTS lifecycle_task_recommendations (
29
+ id bigserial PRIMARY KEY,
30
+
31
+ task_id bigint NOT NULL REFERENCES tasks(id),
32
+ recommended_by bigint NOT NULL REFERENCES builders(id),
33
+ recommended_to bigint NOT NULL REFERENCES builders(id),
34
+
35
+ -- Required, per the criterion. See the header: this column IS the trace.
36
+ reason text NOT NULL,
37
+
38
+ -- 'open' — sent, not yet acted on
39
+ -- 'declined' — the recipient said no. Terminal, and carries no consequence.
40
+ -- 'withdrawn' — the sender took it back. Also terminal.
41
+ --
42
+ -- There is NO 'accepted'. Accepting a recommendation is CLAIMING THE TASK, which
43
+ -- is the claims table's job and the recipient's own action; a status here saying
44
+ -- "accepted" would be a second, weaker record of holding work and would make this
45
+ -- table look like the place claims come from. Whether the recipient went on to
46
+ -- claim it is answered by joining tasks/claims, not by a column here.
47
+ status text NOT NULL DEFAULT 'open',
48
+
49
+ created_at timestamptz NOT NULL DEFAULT now(),
50
+ updated_at timestamptz NOT NULL DEFAULT now(),
51
+
52
+ CONSTRAINT lifecycle_task_recommendations_reason_chk
53
+ CHECK (length(btrim(reason)) > 0),
54
+
55
+ CONSTRAINT lifecycle_task_recommendations_status_chk
56
+ CHECK (status IN ('open', 'declined', 'withdrawn')),
57
+
58
+ -- Recommending a task to yourself is not a recommendation.
59
+ CONSTRAINT lifecycle_task_recommendations_not_self_chk
60
+ CHECK (recommended_by <> recommended_to)
61
+ );
62
+
63
+ -- One OPEN recommendation of a given task to a given person. A second send is the
64
+ -- same judgement repeated, and two identical open rows would make the recipient's
65
+ -- list lie about how many distinct things are waiting on them. Partial, so the
66
+ -- same task CAN be recommended again after a decline or a withdrawal — the
67
+ -- decision was that declining costs nothing, and a permanent unique key would
68
+ -- quietly make a decline final.
69
+ CREATE UNIQUE INDEX IF NOT EXISTS lifecycle_task_recommendations_open_uniq
70
+ ON lifecycle_task_recommendations (task_id, recommended_to) WHERE status = 'open';
71
+
72
+ -- The recipient's read: "what has been recommended to me". Partial — a settled
73
+ -- recommendation is history and never needs routing again (the 004 pattern).
74
+ CREATE INDEX IF NOT EXISTS lifecycle_task_recommendations_open_to_idx
75
+ ON lifecycle_task_recommendations (recommended_to) WHERE status = 'open';
76
+
77
+ -- The sender's read, and the audit read the criterion asks for: the recommendation
78
+ -- and its author are readable AFTERWARDS, so this one is not filtered by status.
79
+ CREATE INDEX IF NOT EXISTS lifecycle_task_recommendations_by_idx
80
+ ON lifecycle_task_recommendations (recommended_by, created_at DESC);
81
+
82
+ -- "Who recommended this task, and why" — the per-task history.
83
+ CREATE INDEX IF NOT EXISTS lifecycle_task_recommendations_task_idx
84
+ ON lifecycle_task_recommendations (task_id, created_at DESC);
85
+
86
+ COMMENT ON TABLE lifecycle_task_recommendations IS
87
+ 'A recommendation that someone look at a task (task 1003173). It NEVER transfers a claim: there is no accepted status and no transfer column, because claiming remains the recipient''s own action.';
88
+
89
+ INSERT INTO schema_migrations (version) VALUES ('lifecycle_005_task_recommendations')
90
+ ON CONFLICT (version) DO NOTHING;
@@ -19,6 +19,7 @@
19
19
  "analytics",
20
20
  "lifecycle",
21
21
  "help-requests",
22
+ "recommendations",
22
23
  "visuals"
23
24
  ],
24
25
  "pollers": [