@bongos/core 1.19.286 → 1.19.288
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 +48 -28
- package/.claude/skills/goal-review/SKILL.md +15 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +11 -3
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0183-criteria-close-themselves.md +58 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +224 -31
- package/docs/api-reference.md +9 -6
- package/modules/ideas/inbox.js +43 -0
- package/modules/ideas/routes/inbox.js +75 -0
- package/modules/lifecycle/db.js +1 -1
- package/modules/lifecycle/done-when.js +145 -0
- package/modules/lifecycle/goal-edits.js +296 -0
- package/modules/lifecycle/publish-reconciler.js +51 -6
- package/modules/lifecycle/routes/done-when.js +51 -0
- package/modules/lifecycle/routes/goals.js +5 -24
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/route-rank-check.js +11 -0
- package/src/module-api.js +5 -1
- package/tests/auto_satisfy_criteria.mjs +212 -0
- package/tests/prose_edits.mjs +339 -0
package/docs/api/openapi.json
CHANGED
|
@@ -3609,6 +3609,72 @@
|
|
|
3609
3609
|
]
|
|
3610
3610
|
}
|
|
3611
3611
|
},
|
|
3612
|
+
"/done-when/{criterionId}": {
|
|
3613
|
+
"patch": {
|
|
3614
|
+
"operationId": "patch_done_when_criterionId",
|
|
3615
|
+
"tags": [
|
|
3616
|
+
"done-when"
|
|
3617
|
+
],
|
|
3618
|
+
"summary": "PATCH /done-when/:criterionId",
|
|
3619
|
+
"description": "PATCH /done-when/:criterionId — correct a criterion's TEXT (task 1003102). Was write-once: this file exposed create, satisfy and unsatisfy and nothing else, so wrong text had no API repair path. The incident that forced it: a shell-quoting error stripped backtick-wrapped literals out of three criteria on goal 1000073 at CREATION time — the intent was recorded, the text was wrong, and the only channels were prod SQL or archiving the goal (worse: criteria are version-level rows, so an archived goal's criteria linger in the version rollup). A SATISFIED criterion is REFUSED, by name — see prose-edits.criterionProseRefusal for why that is a decision and not a limitation: a satisfied criterion is a claim someone verified, and re-wording it re-points that verification at text nobody checked. Unsatisfy → edit → re-satisfy makes the undo a recorded act. Gated like criterion CREATION — BOTH HALVES. The permission `criterion.create` is only the RANK floor; the sibling POST /versions/:id/done-when also runs authorizeCriterionCreate, the per-goal owner|manager wall (ADR 0154). Copying just the rank half would let any Metic rewrite criteria on a goal they neither own, manage, nor belong to — reusing the SAME pure gate is the only way the claim \"gated like creation\" is true. (Caught by the grader on the first cut of task 1003102: the comment said it, the code did half of it.) rank: metic+archon — matches POST /versions/:id/done-when.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `criterion.create` (all required).",
|
|
3620
|
+
"x-rank": "metic+archon",
|
|
3621
|
+
"x-source": "modules/lifecycle/routes/done-when.js",
|
|
3622
|
+
"x-permissions": [
|
|
3623
|
+
"criterion.create"
|
|
3624
|
+
],
|
|
3625
|
+
"parameters": [
|
|
3626
|
+
{
|
|
3627
|
+
"name": "criterionId",
|
|
3628
|
+
"in": "path",
|
|
3629
|
+
"required": true,
|
|
3630
|
+
"schema": {
|
|
3631
|
+
"type": "string"
|
|
3632
|
+
},
|
|
3633
|
+
"description": "Path parameter `criterionId`."
|
|
3634
|
+
}
|
|
3635
|
+
],
|
|
3636
|
+
"requestBody": {
|
|
3637
|
+
"required": true,
|
|
3638
|
+
"content": {
|
|
3639
|
+
"application/json": {
|
|
3640
|
+
"schema": {
|
|
3641
|
+
"$ref": "#/components/schemas/PatchDoneWhenCriterionIdRequest"
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
},
|
|
3645
|
+
"x-validated": true
|
|
3646
|
+
},
|
|
3647
|
+
"responses": {
|
|
3648
|
+
"200": {
|
|
3649
|
+
"description": "Success.",
|
|
3650
|
+
"content": {
|
|
3651
|
+
"application/json": {
|
|
3652
|
+
"schema": {
|
|
3653
|
+
"$ref": "#/components/schemas/PatchDoneWhenCriterionIdResponse"
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
},
|
|
3658
|
+
"400": {
|
|
3659
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
3660
|
+
},
|
|
3661
|
+
"401": {
|
|
3662
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
3663
|
+
},
|
|
3664
|
+
"403": {
|
|
3665
|
+
"$ref": "#/components/responses/Forbidden"
|
|
3666
|
+
},
|
|
3667
|
+
"404": {
|
|
3668
|
+
"$ref": "#/components/responses/NotFound"
|
|
3669
|
+
}
|
|
3670
|
+
},
|
|
3671
|
+
"security": [
|
|
3672
|
+
{
|
|
3673
|
+
"builderSession": []
|
|
3674
|
+
}
|
|
3675
|
+
]
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3612
3678
|
"/done-when/{criterionId}/satisfy": {
|
|
3613
3679
|
"post": {
|
|
3614
3680
|
"operationId": "post_done_when_criterionId_satisfy",
|
|
@@ -4212,6 +4278,49 @@
|
|
|
4212
4278
|
"builderSession": []
|
|
4213
4279
|
}
|
|
4214
4280
|
]
|
|
4281
|
+
},
|
|
4282
|
+
"patch": {
|
|
4283
|
+
"operationId": "patch_goals_id",
|
|
4284
|
+
"tags": [
|
|
4285
|
+
"goals"
|
|
4286
|
+
],
|
|
4287
|
+
"summary": "PATCH /goals/:id",
|
|
4288
|
+
"description": "PATCH /goals/:id — correct a goal's own title/description (task 1003102). Handler + rationale + rank note: ../goal-edits.js. Pinned in route-rank-check.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
4289
|
+
"x-rank": "any-builder",
|
|
4290
|
+
"x-source": "modules/lifecycle/routes/goals.js",
|
|
4291
|
+
"parameters": [
|
|
4292
|
+
{
|
|
4293
|
+
"name": "id",
|
|
4294
|
+
"in": "path",
|
|
4295
|
+
"required": true,
|
|
4296
|
+
"schema": {
|
|
4297
|
+
"type": "string"
|
|
4298
|
+
},
|
|
4299
|
+
"description": "Path parameter `id`."
|
|
4300
|
+
}
|
|
4301
|
+
],
|
|
4302
|
+
"responses": {
|
|
4303
|
+
"200": {
|
|
4304
|
+
"description": "Success."
|
|
4305
|
+
},
|
|
4306
|
+
"400": {
|
|
4307
|
+
"$ref": "#/components/responses/BadRequest"
|
|
4308
|
+
},
|
|
4309
|
+
"401": {
|
|
4310
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
4311
|
+
},
|
|
4312
|
+
"403": {
|
|
4313
|
+
"$ref": "#/components/responses/Forbidden"
|
|
4314
|
+
},
|
|
4315
|
+
"404": {
|
|
4316
|
+
"$ref": "#/components/responses/NotFound"
|
|
4317
|
+
}
|
|
4318
|
+
},
|
|
4319
|
+
"security": [
|
|
4320
|
+
{
|
|
4321
|
+
"builderSession": []
|
|
4322
|
+
}
|
|
4323
|
+
]
|
|
4215
4324
|
}
|
|
4216
4325
|
},
|
|
4217
4326
|
"/goals/{id}/archive": {
|
|
@@ -5324,30 +5433,12 @@
|
|
|
5324
5433
|
"description": "Path parameter `id`."
|
|
5325
5434
|
}
|
|
5326
5435
|
],
|
|
5327
|
-
"requestBody": {
|
|
5328
|
-
"required": true,
|
|
5329
|
-
"content": {
|
|
5330
|
-
"application/json": {
|
|
5331
|
-
"schema": {
|
|
5332
|
-
"$ref": "#/components/schemas/PatchGoalsIdVisibilityRequest"
|
|
5333
|
-
}
|
|
5334
|
-
}
|
|
5335
|
-
},
|
|
5336
|
-
"x-validated": true
|
|
5337
|
-
},
|
|
5338
5436
|
"responses": {
|
|
5339
5437
|
"200": {
|
|
5340
|
-
"description": "Success."
|
|
5341
|
-
"content": {
|
|
5342
|
-
"application/json": {
|
|
5343
|
-
"schema": {
|
|
5344
|
-
"$ref": "#/components/schemas/PatchGoalsIdVisibilityResponse"
|
|
5345
|
-
}
|
|
5346
|
-
}
|
|
5347
|
-
}
|
|
5438
|
+
"description": "Success."
|
|
5348
5439
|
},
|
|
5349
5440
|
"400": {
|
|
5350
|
-
"$ref": "#/components/responses/
|
|
5441
|
+
"$ref": "#/components/responses/BadRequest"
|
|
5351
5442
|
},
|
|
5352
5443
|
"401": {
|
|
5353
5444
|
"$ref": "#/components/responses/Unauthorized"
|
|
@@ -6249,7 +6340,7 @@
|
|
|
6249
6340
|
"inbox"
|
|
6250
6341
|
],
|
|
6251
6342
|
"summary": "PATCH /inbox/:id",
|
|
6252
|
-
"description": "
|
|
6343
|
+
"description": "**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `idea.triage` (all required).",
|
|
6253
6344
|
"x-rank": "metic+archon",
|
|
6254
6345
|
"x-source": "modules/ideas/routes/inbox.js",
|
|
6255
6346
|
"x-permissions": [
|
|
@@ -6308,6 +6399,69 @@
|
|
|
6308
6399
|
]
|
|
6309
6400
|
}
|
|
6310
6401
|
},
|
|
6402
|
+
"/inbox/{id}/body": {
|
|
6403
|
+
"patch": {
|
|
6404
|
+
"operationId": "patch_inbox_id_body",
|
|
6405
|
+
"tags": [
|
|
6406
|
+
"inbox"
|
|
6407
|
+
],
|
|
6408
|
+
"summary": "PATCH /inbox/:id/body",
|
|
6409
|
+
"description": "Metic+ (task 360 / ADR 0018): idea triage (promote/discard/merge) shapes the backlog. Criterion C2 assigns triage to Metic, so this widened from archon-only to metic+archon when the three-rank model went live. Any builder can still FILE an idea (POST /inbox above); a Xenos cannot triage. rank: metic+archon — idea triage shapes the backlog; criterion C2 / task 360 / ADR 0018. PATCH /inbox/:id/body — correct an idea's own TEXT (task 1003102 / idea 1000740). A SEPARATE route rather than a new field on PATCH /inbox/:id, deliberately. That route is gated `requirePermission('idea.triage')`, and the ask here is that an idea's AUTHOR can fix their own words. Adding body_md there would have meant dropping the whole triage route to requireBuilder and re-authorizing every field in-handler — widening a pinned route's classified rank to reach one field. A new route with its own pin widens nothing: triage keeps its gate, and this one carries its own, narrower rule. AUTHOR (idea_inbox.captured_by) or a triager. The author case is the point: fixing your own typo must not require escalating to a Metic. rank: any-builder (requireBuilder) + the in-handler author|triage gate. Pinned in route-rank-check.MODULE_EXPECTED_RANKS['ideas']. asyncHandler (the doorway wrapper) owns the catch + the 500 log, so this route carries no console.error of its own — one central log site instead of a 16th copy of the same two lines in this file.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
6410
|
+
"x-rank": "any-builder",
|
|
6411
|
+
"x-source": "modules/ideas/routes/inbox.js",
|
|
6412
|
+
"parameters": [
|
|
6413
|
+
{
|
|
6414
|
+
"name": "id",
|
|
6415
|
+
"in": "path",
|
|
6416
|
+
"required": true,
|
|
6417
|
+
"schema": {
|
|
6418
|
+
"type": "string"
|
|
6419
|
+
},
|
|
6420
|
+
"description": "Path parameter `id`."
|
|
6421
|
+
}
|
|
6422
|
+
],
|
|
6423
|
+
"requestBody": {
|
|
6424
|
+
"required": true,
|
|
6425
|
+
"content": {
|
|
6426
|
+
"application/json": {
|
|
6427
|
+
"schema": {
|
|
6428
|
+
"$ref": "#/components/schemas/PatchInboxIdBodyRequest"
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
},
|
|
6432
|
+
"x-validated": true
|
|
6433
|
+
},
|
|
6434
|
+
"responses": {
|
|
6435
|
+
"200": {
|
|
6436
|
+
"description": "Success.",
|
|
6437
|
+
"content": {
|
|
6438
|
+
"application/json": {
|
|
6439
|
+
"schema": {
|
|
6440
|
+
"$ref": "#/components/schemas/PatchInboxIdBodyResponse"
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
},
|
|
6445
|
+
"400": {
|
|
6446
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
6447
|
+
},
|
|
6448
|
+
"401": {
|
|
6449
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
6450
|
+
},
|
|
6451
|
+
"403": {
|
|
6452
|
+
"$ref": "#/components/responses/Forbidden"
|
|
6453
|
+
},
|
|
6454
|
+
"404": {
|
|
6455
|
+
"$ref": "#/components/responses/NotFound"
|
|
6456
|
+
}
|
|
6457
|
+
},
|
|
6458
|
+
"security": [
|
|
6459
|
+
{
|
|
6460
|
+
"builderSession": []
|
|
6461
|
+
}
|
|
6462
|
+
]
|
|
6463
|
+
}
|
|
6464
|
+
},
|
|
6311
6465
|
"/inbox/{id}/ratify": {
|
|
6312
6466
|
"post": {
|
|
6313
6467
|
"operationId": "post_inbox_id_ratify",
|
|
@@ -14674,6 +14828,39 @@
|
|
|
14674
14828
|
],
|
|
14675
14829
|
"additionalProperties": false
|
|
14676
14830
|
},
|
|
14831
|
+
"PatchDoneWhenCriterionIdRequest": {
|
|
14832
|
+
"type": "object",
|
|
14833
|
+
"properties": {
|
|
14834
|
+
"criterion_md": {
|
|
14835
|
+
"type": "string",
|
|
14836
|
+
"minLength": 1,
|
|
14837
|
+
"maxLength": 10000
|
|
14838
|
+
}
|
|
14839
|
+
},
|
|
14840
|
+
"required": [
|
|
14841
|
+
"criterion_md"
|
|
14842
|
+
],
|
|
14843
|
+
"additionalProperties": false
|
|
14844
|
+
},
|
|
14845
|
+
"PatchDoneWhenCriterionIdResponse": {
|
|
14846
|
+
"type": "object",
|
|
14847
|
+
"properties": {
|
|
14848
|
+
"ok": {
|
|
14849
|
+
"type": "boolean"
|
|
14850
|
+
},
|
|
14851
|
+
"criterion": {},
|
|
14852
|
+
"deltas": {},
|
|
14853
|
+
"unchanged": {
|
|
14854
|
+
"type": "boolean"
|
|
14855
|
+
}
|
|
14856
|
+
},
|
|
14857
|
+
"required": [
|
|
14858
|
+
"ok",
|
|
14859
|
+
"criterion",
|
|
14860
|
+
"deltas",
|
|
14861
|
+
"unchanged"
|
|
14862
|
+
]
|
|
14863
|
+
},
|
|
14677
14864
|
"PatchGoalsIdMembersBuilderIdRequest": {
|
|
14678
14865
|
"type": "object",
|
|
14679
14866
|
"properties": {
|
|
@@ -14703,29 +14890,35 @@
|
|
|
14703
14890
|
"changed"
|
|
14704
14891
|
]
|
|
14705
14892
|
},
|
|
14706
|
-
"
|
|
14893
|
+
"PatchInboxIdBodyRequest": {
|
|
14707
14894
|
"type": "object",
|
|
14708
14895
|
"properties": {
|
|
14709
|
-
"
|
|
14710
|
-
"type": "string"
|
|
14896
|
+
"body_md": {
|
|
14897
|
+
"type": "string",
|
|
14898
|
+
"minLength": 1,
|
|
14899
|
+
"maxLength": 50000
|
|
14711
14900
|
}
|
|
14712
14901
|
},
|
|
14713
14902
|
"required": [
|
|
14714
|
-
"
|
|
14903
|
+
"body_md"
|
|
14715
14904
|
],
|
|
14716
14905
|
"additionalProperties": false
|
|
14717
14906
|
},
|
|
14718
|
-
"
|
|
14907
|
+
"PatchInboxIdBodyResponse": {
|
|
14719
14908
|
"type": "object",
|
|
14720
14909
|
"properties": {
|
|
14721
14910
|
"ok": {
|
|
14722
14911
|
"type": "boolean"
|
|
14723
14912
|
},
|
|
14724
|
-
"
|
|
14913
|
+
"idea": {},
|
|
14914
|
+
"delta": {},
|
|
14915
|
+
"unchanged": {}
|
|
14725
14916
|
},
|
|
14726
14917
|
"required": [
|
|
14727
14918
|
"ok",
|
|
14728
|
-
"
|
|
14919
|
+
"idea",
|
|
14920
|
+
"delta",
|
|
14921
|
+
"unchanged"
|
|
14729
14922
|
]
|
|
14730
14923
|
},
|
|
14731
14924
|
"PatchInboxIdRequest": {
|
|
@@ -18353,9 +18546,9 @@
|
|
|
18353
18546
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
18354
18547
|
}
|
|
18355
18548
|
},
|
|
18356
|
-
"x-endpoint-count":
|
|
18357
|
-
"x-schema-count":
|
|
18549
|
+
"x-endpoint-count": 293,
|
|
18550
|
+
"x-schema-count": 328,
|
|
18358
18551
|
"x-undocumented-bodies": 8,
|
|
18359
|
-
"x-response-schemas":
|
|
18552
|
+
"x-response-schemas": 219,
|
|
18360
18553
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
18361
18554
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 293 endpoints across 47 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`.
|
|
@@ -172,10 +172,11 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
172
172
|
| GET | `/api/bongos/discord/channels/plan` | `metic+archon` | — | GET /api/bongos/discord/channels/plan — dry run. |
|
|
173
173
|
| POST | `/api/bongos/discord/channels/reconcile` | `metic+archon` | — | POST /api/bongos/discord/channels/reconcile — apply the config to Discord now. |
|
|
174
174
|
|
|
175
|
-
## `done-when` (
|
|
175
|
+
## `done-when` (4)
|
|
176
176
|
|
|
177
177
|
| Method | Path | Rank | Body | Description |
|
|
178
178
|
|---|---|---|---|---|
|
|
179
|
+
| PATCH | `/api/bongos/done-when/:criterionId` | `metic+archon` | `criterion_md` | PATCH /done-when/:criterionId — correct a criterion's TEXT (task 1003102). |
|
|
179
180
|
| POST | `/api/bongos/done-when/:criterionId/satisfy` | `metic+archon` | `satisfied_by_task_id` | Auth (Metic+): confirm a criterion — flip it to satisfied. |
|
|
180
181
|
| POST | `/api/bongos/done-when/:criterionId/unsatisfy` | `metic+archon` | — | Auth: flip a criterion back to unsatisfied (rare; for corrections). |
|
|
181
182
|
| GET | `/api/bongos/done-when/pending-review` | `metic+archon` | — | Auth (Metic+): the criterion review queue — every criterion auto-flagged "met — pending review" (≥1 linked task, all shipped, not yet con… |
|
|
@@ -196,13 +197,14 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
196
197
|
| GET | `/api/bongos/github/repos` | `any-builder` | — | acts only on the caller's OWN GitHub identity (the token they granted). |
|
|
197
198
|
| POST | `/api/bongos/github/repos` | `any-builder` | `name` | creates a repo in the caller's OWN account. |
|
|
198
199
|
|
|
199
|
-
## `goals` (
|
|
200
|
+
## `goals` (25)
|
|
200
201
|
|
|
201
202
|
| Method | Path | Rank | Body | Description |
|
|
202
203
|
|---|---|---|---|---|
|
|
203
204
|
| GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
|
|
204
205
|
| POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `description`, `scope_modules`, `succeeds_goal_id` | POST /goals — create a goal. |
|
|
205
206
|
| GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
|
|
207
|
+
| PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/description (task 1003102). |
|
|
206
208
|
| POST | `/api/bongos/goals/:id/archive` | `metic+archon` | — | POST /goals/:id/archive — retire a goal (open\|achieved → archived). |
|
|
207
209
|
| GET | `/api/bongos/goals/:id/conflicts` | `any-builder` | — | GET /goals/:id/conflicts — the server-side task-conflict view (task 1736). |
|
|
208
210
|
| POST | `/api/bongos/goals/:id/conflicts/resolve` | `any-builder` | `action`, `note`, `touches`, `first_task_id`, `then_task_id`, `task_id` | POST /goals/:id/conflicts/resolve — the owner/manager resolution actions for a detected conflict (task 1736; authority model ADR 0106): s… |
|
|
@@ -221,7 +223,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
221
223
|
| GET | `/api/bongos/goals/:id/suggest-criteria` | `any-builder` | — | GET /goals/:id/suggest-criteria?title=&description=&limit= — task 2165. |
|
|
222
224
|
| POST | `/api/bongos/goals/:id/tasks` | `any-builder` | `title`, `description`, `touches`, `est_minutes`, `priority`, `kind`, `discipline`, `value_summary`, `parent_task_id` | POST /goals/:id/tasks — bounded task authoring within a goal's scope (BV1.R60 / ADR 0086 §3). |
|
|
223
225
|
| POST | `/api/bongos/goals/:id/transfer` | `any-builder` | `builder_id` | POST /goals/:id/transfer — hand the goal to a new owner (task 1735). |
|
|
224
|
-
| PATCH | `/api/bongos/goals/:id/visibility` | `any-builder` |
|
|
226
|
+
| PATCH | `/api/bongos/goals/:id/visibility` | `any-builder` | — | PATCH /goals/:id/visibility — make a goal public\|private (task 1952 / ADR 0112). |
|
|
225
227
|
| GET | `/api/bongos/goals/inbox` | `any-builder` | — | GET /goals/inbox — the caller's private-goals inbox (task 1952 / ADR 0112): { invitations: invites addressed to me (accept/decline); join… |
|
|
226
228
|
| POST | `/api/bongos/goals/reorder` | `metic+archon` | `version_id`, `ordered_ids` | POST /goals/reorder — Archon drag-to-reorder: set the priority order of every goal WITHIN one version (task 1760). |
|
|
227
229
|
|
|
@@ -255,14 +257,15 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
255
257
|
|---|---|---|---|---|
|
|
256
258
|
| GET | `/api/bongos/healthz` | `public` | — | liveness probe; status.example.com hits this anonymously. |
|
|
257
259
|
|
|
258
|
-
## `inbox` (
|
|
260
|
+
## `inbox` (8)
|
|
259
261
|
|
|
260
262
|
| Method | Path | Rank | Body | Description |
|
|
261
263
|
|---|---|---|---|---|
|
|
262
264
|
| GET | `/api/bongos/inbox` | `any-builder` | — | read open ideas; skill-friction rows filtered by viewer rank. |
|
|
263
265
|
| POST | `/api/bongos/inbox` | `any-builder` | `title`, `body_md`, `kind`, `suggested_version`, `suggested_priority`, `goal_id`, `grade`, `template_key`, `field_values` | for ordinary ideas; metic+ for kind='criterion-proposal'. |
|
|
264
266
|
| GET | `/api/bongos/inbox/:id` | `any-builder` | — | single idea read for the "#NNN" deep-link resolver (task 1047). |
|
|
265
|
-
| PATCH | `/api/bongos/inbox/:id` | `metic+archon` | `status`, `kind`, `promoted_to_task_id`, `reviewed_at`, `merged_into_idea_id`, `triage_note` |
|
|
267
|
+
| PATCH | `/api/bongos/inbox/:id` | `metic+archon` | `status`, `kind`, `promoted_to_task_id`, `reviewed_at`, `merged_into_idea_id`, `triage_note` | |
|
|
268
|
+
| PATCH | `/api/bongos/inbox/:id/body` | `any-builder` | `body_md` | Metic+ (task 360 / ADR 0018): idea triage (promote/discard/merge) shapes the backlog. |
|
|
266
269
|
| POST | `/api/bongos/inbox/:id/ratify` | `metic+archon` | `notes_md` | V3.R31 (task 252) — Archon ratifies a criterion-proposal idea, atomically: - INSERT done_when_criteria row on the target version - UPDATE… |
|
|
267
270
|
| POST | `/api/bongos/inbox/:id/resubmit` | `any-builder` | `field_values`, `template_key` | RESUBMIT a Full Idea after a rejection (R13 of goal 1000069, criterion C2: an idea never dies and never stalls). |
|
|
268
271
|
| GET | `/api/bongos/inbox/awaiting-nod` | `metic+archon` | — | GET /inbox/awaiting-nod — the NOD QUEUE (task 1003066, BV1.R05). |
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -546,6 +546,48 @@ async function countOpenIdeas({ viewerRank = null } = {}) {
|
|
|
546
546
|
// open), then merge C into A, whose status still reads 'open'. So the chain
|
|
547
547
|
// guard tests the TARGET COLUMN, not the target's status, and a target write
|
|
548
548
|
// must carry status='merged'.
|
|
549
|
+
// updateIdeaBody — correct an idea's PROSE (task 1003102 / idea 1000740).
|
|
550
|
+
//
|
|
551
|
+
// PATCH /inbox/:id accepts status, kind, promoted_to_task_id, reviewed_at,
|
|
552
|
+
// merged_into_idea_id and triage_note — every field ABOUT the idea, and not the
|
|
553
|
+
// idea itself. So a correction to the text had to live in a triage_note beside
|
|
554
|
+
// the words it corrected, rather than in them.
|
|
555
|
+
//
|
|
556
|
+
// Returns { idea, delta } — delta is { field, from, to, from_len, to_len } with
|
|
557
|
+
// clipped excerpts, or null when the text is unchanged. The global write-audit
|
|
558
|
+
// middleware records only the REQUEST body (the NEW text), so an edit to durable
|
|
559
|
+
// prose would otherwise leave no trace of what it replaced.
|
|
560
|
+
//
|
|
561
|
+
// BOUNDARY-FORCED DUPLICATION (ADR 0091 §2): the same excerpt/delta shaping lives
|
|
562
|
+
// in modules/lifecycle/prose-edits.js for goals + criteria. A module may not
|
|
563
|
+
// require a sibling module, and this is 6 lines of formatting — copied
|
|
564
|
+
// deliberately and noted at both sites rather than pushed into the doorway.
|
|
565
|
+
const BODY_AUDIT_EXCERPT = 120;
|
|
566
|
+
async function updateIdeaBody(id, bodyMd) {
|
|
567
|
+
const ideaId = Number(id);
|
|
568
|
+
if (!Number.isFinite(ideaId)) throw new Error('updateIdeaBody: numeric id required');
|
|
569
|
+
const { rows: before } = await pool.query(`SELECT ${IDEA_COLS} FROM idea_inbox WHERE id = $1`, [ideaId]);
|
|
570
|
+
if (before.length === 0) return null;
|
|
571
|
+
const clip = (v) => {
|
|
572
|
+
const t = v == null ? '' : String(v).replace(/\s+/g, ' ').trim();
|
|
573
|
+
return t.length > BODY_AUDIT_EXCERPT ? `${t.slice(0, BODY_AUDIT_EXCERPT - 1)}…` : t;
|
|
574
|
+
};
|
|
575
|
+
const prev = before[0].body_md;
|
|
576
|
+
if (prev === bodyMd) return { idea: before[0], delta: null };
|
|
577
|
+
const { rows } = await pool.query(
|
|
578
|
+
`UPDATE idea_inbox SET body_md = $2 WHERE id = $1 RETURNING ${IDEA_COLS}`,
|
|
579
|
+
[ideaId, bodyMd]
|
|
580
|
+
);
|
|
581
|
+
return {
|
|
582
|
+
idea: rows[0],
|
|
583
|
+
delta: {
|
|
584
|
+
field: 'body_md', from: clip(prev), to: clip(bodyMd),
|
|
585
|
+
from_len: prev == null ? 0 : String(prev).length,
|
|
586
|
+
to_len: bodyMd == null ? 0 : String(bodyMd).length,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
549
591
|
async function updateIdea(id, { status, kind, promoted_to_task_id, reviewed_at, merged_into_idea_id, triage_note } = {}) {
|
|
550
592
|
if (merged_into_idea_id !== undefined && merged_into_idea_id !== null) {
|
|
551
593
|
if (status !== 'merged') {
|
|
@@ -1056,6 +1098,7 @@ module.exports = {
|
|
|
1056
1098
|
getIdeaGrade,
|
|
1057
1099
|
// mutate
|
|
1058
1100
|
updateIdea,
|
|
1101
|
+
updateIdeaBody,
|
|
1059
1102
|
setIdeaGrade,
|
|
1060
1103
|
setIdeaCompleteness,
|
|
1061
1104
|
// idea → task lineage (task 1002997 / BV1.R173) — the one-idea-many-tasks
|
|
@@ -448,6 +448,81 @@ module.exports = function buildInboxRouter() {
|
|
|
448
448
|
// archon-only to metic+archon when the three-rank model went live. Any
|
|
449
449
|
// builder can still FILE an idea (POST /inbox above); a Xenos cannot triage.
|
|
450
450
|
// rank: metic+archon — idea triage shapes the backlog; criterion C2 / #360 / ADR 0018.
|
|
451
|
+
// PATCH /inbox/:id/body — correct an idea's own TEXT (task 1003102 / idea 1000740).
|
|
452
|
+
//
|
|
453
|
+
// A SEPARATE route rather than a new field on PATCH /inbox/:id, deliberately.
|
|
454
|
+
// That route is gated `requirePermission('idea.triage')`, and the ask here is
|
|
455
|
+
// that an idea's AUTHOR can fix their own words. Adding body_md there would have
|
|
456
|
+
// meant dropping the whole triage route to requireBuilder and re-authorizing
|
|
457
|
+
// every field in-handler — widening a pinned route's classified rank to reach
|
|
458
|
+
// one field. A new route with its own pin widens nothing: triage keeps its gate,
|
|
459
|
+
// and this one carries its own, narrower rule.
|
|
460
|
+
//
|
|
461
|
+
// AUTHOR (idea_inbox.captured_by) or a triager. The author case is the point:
|
|
462
|
+
// fixing your own typo must not require escalating to a Metic.
|
|
463
|
+
// rank: any-builder (requireBuilder) + the in-handler author|triage gate.
|
|
464
|
+
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS['ideas'].
|
|
465
|
+
// asyncHandler (the doorway wrapper) owns the catch + the 500 log, so this
|
|
466
|
+
// route carries no console.error of its own — one central log site instead of
|
|
467
|
+
// a 16th copy of the same two lines in this file.
|
|
468
|
+
router.patch('/inbox/:id/body', api.requireBuilder, api.asyncHandler('PATCH /inbox/:id/body', async (req, res) => {
|
|
469
|
+
if (validateOrRespond(req, res, {
|
|
470
|
+
body_md: { required: true, type: 'string', minLength: 1, maxLength: LIMITS.DESCRIPTION },
|
|
471
|
+
})) return;
|
|
472
|
+
const id = Number(req.params.id);
|
|
473
|
+
if (!Number.isFinite(id)) return res.fail('bad_idea_id', 400);
|
|
474
|
+
const existing = await inbox.getIdea(id);
|
|
475
|
+
if (!existing) return res.fail('idea_not_found', 404);
|
|
476
|
+
// Ordered + fail-closed: authority first, and it reveals nothing about the
|
|
477
|
+
// idea a non-author could not already read from the inbox.
|
|
478
|
+
const isAuthor = existing.captured_by != null
|
|
479
|
+
&& String(existing.captured_by) === String(req.builder.id);
|
|
480
|
+
// The doorway exposes requirePermission as MIDDLEWARE, not a callable
|
|
481
|
+
// predicate, and this decision is in-handler (it depends on the row), so the
|
|
482
|
+
// triager arm is expressed as the rank floor that `idea.triage` resolves to
|
|
483
|
+
// — the same floor PATCH /inbox/:id classifies at (metic+archon). Stated
|
|
484
|
+
// rather than hidden: if the permission's floor ever moves, this must move
|
|
485
|
+
// with it, and tests/prose_edits.mjs pins the pair.
|
|
486
|
+
// rankMeetsThreshold takes the ALLOWED-rank ARRAY (the floor is its
|
|
487
|
+
// lowest-authority member), not a single rank — passing a string makes it
|
|
488
|
+
// throw on .reduce, which is exactly the trap a dead fallback would have hidden.
|
|
489
|
+
const canTriage = api.rankMeetsThreshold(req.builder.rank, ['metic', 'archon']);
|
|
490
|
+
if (!isAuthor && !canTriage) {
|
|
491
|
+
return res.fail('not_idea_author', { status: 403, message: 'Only the idea\'s author (or a triager) may correct its text.' });
|
|
492
|
+
}
|
|
493
|
+
const result = await inbox.updateIdeaBody(id, req.body.body_md);
|
|
494
|
+
if (!result) return res.fail('idea_not_found', 404);
|
|
495
|
+
if (result.delta) {
|
|
496
|
+
const d = result.delta;
|
|
497
|
+
// Through api.logger — the seam this file already uses (see
|
|
498
|
+
// /inbox/awaiting-nod) — not a raw console call.
|
|
499
|
+
api.logger('ideas').info(`idea ${id} body edited by builder ${req.builder.id}${isAuthor ? ' (author)' : ' (triage)'}: "${d.from}" -> "${d.to}" (${d.from_len}->${d.to_len} chars)`);
|
|
500
|
+
// ...AND persist it. Task item 4 asks for an audit ROW on all three prose
|
|
501
|
+
// surfaces, and a log line is not one: not queryable, and it rotates away.
|
|
502
|
+
// The goal + criterion paths get this from lifecycle's recordProseEdit; a
|
|
503
|
+
// module may not require a sibling module, so this writes the same shape
|
|
504
|
+
// through the doorway's own insertAuditLog — boundary-forced duplication
|
|
505
|
+
// (ADR 0091 §2), noted at both sites. Best-effort: the edit already
|
|
506
|
+
// committed, so a failed audit write must not fail the request — but it is
|
|
507
|
+
// logged as an AUDIT GAP rather than swallowed, because a silent gap is the
|
|
508
|
+
// failure mode this task exists to close.
|
|
509
|
+
try {
|
|
510
|
+
await api.insertAuditLog({
|
|
511
|
+
builderId: req.builder.id,
|
|
512
|
+
route: `idea:${id}:prose-edit`,
|
|
513
|
+
method: 'PATCH',
|
|
514
|
+
requestBodyRedacted: JSON.stringify({ deltas: [d] }),
|
|
515
|
+
responseStatus: 200,
|
|
516
|
+
ip: req.ip || null,
|
|
517
|
+
userAgent: req.get ? req.get('user-agent') : null,
|
|
518
|
+
});
|
|
519
|
+
} catch (auditErr) {
|
|
520
|
+
api.logger('ideas').error(`AUDIT GAP: idea ${id} body edit landed but its audit row failed: ${auditErr && auditErr.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return res.json({ ok: true, idea: result.idea, delta: result.delta, unchanged: !result.delta });
|
|
524
|
+
}, { errorCode: 'idea_body_update_failed' }));
|
|
525
|
+
|
|
451
526
|
router.patch('/inbox/:id', api.requireBuilder, api.requirePermission('idea.triage'), async (req, res) => {
|
|
452
527
|
if (validateOrRespond(req, res, {
|
|
453
528
|
// Enum kept light — inbox.updateIdea returns a structured BAD_STATUS
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -3863,7 +3863,7 @@ async function shipTask({ taskId, builderId }, deps = {}) {
|
|
|
3863
3863
|
justUnlocked = [];
|
|
3864
3864
|
}
|
|
3865
3865
|
}
|
|
3866
|
-
|
|
3866
|
+
await doneWhen.closeCompletedWorkForShip(client, taskId); // task 1002069/ADR 0183 — in-txn criterion+goal close
|
|
3867
3867
|
return {
|
|
3868
3868
|
shipRow: rows[0], graduationBuilder, rankGraduation, actualShipperId,
|
|
3869
3869
|
closedClaimCount: closedClaims.length,
|