@bongos/core 1.19.1059 → 1.19.1060
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 +39 -24
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +32 -1
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/copy-desk/flags.js +14 -1
- package/modules/copy-desk/page-status.js +177 -3
- package/modules/copy-desk/routes/copy-desk.js +41 -0
- package/modules/copy-desk/tests/copy_page_next.mjs +184 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/copy_desk_flags_db.mjs +12 -0
- package/tests/copy_desk_page_claim.mjs +9 -124
- package/tests/copy_desk_page_next.mjs +172 -0
- package/tests/fixtures/page-ledger.mjs +175 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.1060",
|
|
6
|
+
"core_contract": "1.19.1060",
|
|
7
|
+
"source_commit": "053ab05047f2cede246dd8c6cca763a032ee8495",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-28T11:
|
|
9
|
+
"built_at": "2026-09-28T11:58:43.905Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 543,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2528,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3098,
|
|
20
|
+
"tree_sha256": "80aa074ee45fe0f2b5b60a190786cd4833e3aad1153828d157d5fe2b865b7db0",
|
|
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": "
|
|
320
|
+
"sha256": "3e5980fb95bfaf6029cdddc9c9921053d9897cbcbf15be30e58e9b494177d4af"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
324
324
|
"mode": "0000644",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "c587bec39fefe04695a0c1673882e3af552fe72486909e9e90759a3b46e20916"
|
|
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": "
|
|
335
|
+
"sha256": "ee6df391f3c319e77b9601fb47bf5deddc56adb95af62e8086a3d6dbeac4993b"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "ed5f287b10278823cf322091feff1e784125d21a7856ad021a98e6e3e2269f71"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
344
344
|
"mode": "0000644",
|
|
345
|
-
"sha256": "
|
|
345
|
+
"sha256": "a43b2952238b2f19e76fe8767ec6b8a313fdfb0173cfa10946b6dee63e97af69"
|
|
346
346
|
},
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/package.json",
|
|
@@ -2177,12 +2177,12 @@
|
|
|
2177
2177
|
{
|
|
2178
2178
|
"path": "docs/api-reference.md",
|
|
2179
2179
|
"mode": "0000644",
|
|
2180
|
-
"sha256": "
|
|
2180
|
+
"sha256": "d642de14db69137bb04bf79bf4a67b8ec01818ac2ff6b85f04fca4944f2bdd15"
|
|
2181
2181
|
},
|
|
2182
2182
|
{
|
|
2183
2183
|
"path": "docs/api/openapi.json",
|
|
2184
2184
|
"mode": "0000644",
|
|
2185
|
-
"sha256": "
|
|
2185
|
+
"sha256": "f47a697a636c8b50dafd4d8e58986676af0b8fcacaf89f6a418b230ebcb28cdc"
|
|
2186
2186
|
},
|
|
2187
2187
|
{
|
|
2188
2188
|
"path": "docs/architecture.md",
|
|
@@ -2742,7 +2742,7 @@
|
|
|
2742
2742
|
{
|
|
2743
2743
|
"path": "docs/module-api-changelog.md",
|
|
2744
2744
|
"mode": "0000644",
|
|
2745
|
-
"sha256": "
|
|
2745
|
+
"sha256": "72f72960133805df016e92eb786b37d8fb7cb67bf3f09e9fcade295cce4773cb"
|
|
2746
2746
|
},
|
|
2747
2747
|
{
|
|
2748
2748
|
"path": "docs/modules-contract.md",
|
|
@@ -4112,7 +4112,7 @@
|
|
|
4112
4112
|
{
|
|
4113
4113
|
"path": "modules/copy-desk/flags.js",
|
|
4114
4114
|
"mode": "0000644",
|
|
4115
|
-
"sha256": "
|
|
4115
|
+
"sha256": "ca617aef2c209f89c57acf2db7772b27f0bb7e790afb48dc940822422ec8ad81"
|
|
4116
4116
|
},
|
|
4117
4117
|
{
|
|
4118
4118
|
"path": "modules/copy-desk/migrations/copy_desk_001_flags.sql",
|
|
@@ -4137,7 +4137,7 @@
|
|
|
4137
4137
|
{
|
|
4138
4138
|
"path": "modules/copy-desk/page-status.js",
|
|
4139
4139
|
"mode": "0000644",
|
|
4140
|
-
"sha256": "
|
|
4140
|
+
"sha256": "fe85e899abb8524b36ef7187a8a2b5b127ea87342c378a32b95a644d18a83021"
|
|
4141
4141
|
},
|
|
4142
4142
|
{
|
|
4143
4143
|
"path": "modules/copy-desk/pages.js",
|
|
@@ -4157,7 +4157,7 @@
|
|
|
4157
4157
|
{
|
|
4158
4158
|
"path": "modules/copy-desk/routes/copy-desk.js",
|
|
4159
4159
|
"mode": "0000644",
|
|
4160
|
-
"sha256": "
|
|
4160
|
+
"sha256": "6d313d730906c5f240504695b792401fc0dc9ffde993efd2f330a9e71df276c0"
|
|
4161
4161
|
},
|
|
4162
4162
|
{
|
|
4163
4163
|
"path": "modules/copy-desk/tests/copy_flags.mjs",
|
|
@@ -4169,6 +4169,11 @@
|
|
|
4169
4169
|
"mode": "0000644",
|
|
4170
4170
|
"sha256": "17ad6640e97dde18273ccd5c533b6910d490bfad970da16fd0712c20a8b5f7f3"
|
|
4171
4171
|
},
|
|
4172
|
+
{
|
|
4173
|
+
"path": "modules/copy-desk/tests/copy_page_next.mjs",
|
|
4174
|
+
"mode": "0000644",
|
|
4175
|
+
"sha256": "d82db6db97b22a88a83b4cf9bac7724a411dc7831b08cacd5548c0ac367ad3b9"
|
|
4176
|
+
},
|
|
4172
4177
|
{
|
|
4173
4178
|
"path": "modules/copy-desk/tests/copy_page_status.mjs",
|
|
4174
4179
|
"mode": "0000644",
|
|
@@ -8517,12 +8522,12 @@
|
|
|
8517
8522
|
{
|
|
8518
8523
|
"path": "package-lock.json",
|
|
8519
8524
|
"mode": "0000644",
|
|
8520
|
-
"sha256": "
|
|
8525
|
+
"sha256": "1dd792c5514fb93f90ca899378a3fe7fb0ba2759d11ae763a593eaa4de211a37"
|
|
8521
8526
|
},
|
|
8522
8527
|
{
|
|
8523
8528
|
"path": "package.json",
|
|
8524
8529
|
"mode": "0000644",
|
|
8525
|
-
"sha256": "
|
|
8530
|
+
"sha256": "8e2cc56778737033b8c49aabdb41869132a0343fae6419e90aa23adedf474daa"
|
|
8526
8531
|
},
|
|
8527
8532
|
{
|
|
8528
8533
|
"path": "public-docs/index.html",
|
|
@@ -8542,7 +8547,7 @@
|
|
|
8542
8547
|
{
|
|
8543
8548
|
"path": "release-notes.json",
|
|
8544
8549
|
"mode": "0000644",
|
|
8545
|
-
"sha256": "
|
|
8550
|
+
"sha256": "971eebaefe45a5e3bd6eac7a217d7a1abc571d02240dac97799f9167643a227b"
|
|
8546
8551
|
},
|
|
8547
8552
|
{
|
|
8548
8553
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -10627,7 +10632,7 @@
|
|
|
10627
10632
|
{
|
|
10628
10633
|
"path": "src/module-api.js",
|
|
10629
10634
|
"mode": "0000644",
|
|
10630
|
-
"sha256": "
|
|
10635
|
+
"sha256": "5be8c8ecfa92788bfeef57fd9e4235d24ac3e4257d10e9936f38ace6b627d2cb"
|
|
10631
10636
|
},
|
|
10632
10637
|
{
|
|
10633
10638
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11577,12 +11582,17 @@
|
|
|
11577
11582
|
{
|
|
11578
11583
|
"path": "tests/copy_desk_flags_db.mjs",
|
|
11579
11584
|
"mode": "0000644",
|
|
11580
|
-
"sha256": "
|
|
11585
|
+
"sha256": "739bbd623955742864ac712a5f119968ebac671c005054a07a8521c7770a929b"
|
|
11581
11586
|
},
|
|
11582
11587
|
{
|
|
11583
11588
|
"path": "tests/copy_desk_page_claim.mjs",
|
|
11584
11589
|
"mode": "0000644",
|
|
11585
|
-
"sha256": "
|
|
11590
|
+
"sha256": "af6cdac77b90aad511a603757e12d7ffefaa7fdbcdd14aefd70ad49e3cd193fe"
|
|
11591
|
+
},
|
|
11592
|
+
{
|
|
11593
|
+
"path": "tests/copy_desk_page_next.mjs",
|
|
11594
|
+
"mode": "0000644",
|
|
11595
|
+
"sha256": "d9827f75b542ce1a3fe9bfe9e3d5cd8ecaaed4691d611dc73617d280ec6cf292"
|
|
11586
11596
|
},
|
|
11587
11597
|
{
|
|
11588
11598
|
"path": "tests/copy_desk_page_reads.mjs",
|
|
@@ -12069,6 +12079,11 @@
|
|
|
12069
12079
|
"mode": "0000644",
|
|
12070
12080
|
"sha256": "2f532cacb06f0f074c15101c16578f3183104042644d6c8aa0d2e80fdb425b2f"
|
|
12071
12081
|
},
|
|
12082
|
+
{
|
|
12083
|
+
"path": "tests/fixtures/page-ledger.mjs",
|
|
12084
|
+
"mode": "0000644",
|
|
12085
|
+
"sha256": "7e6cee001284e873e06a3037f6916abcfbfa53e243daafd652c6f629d4f73012"
|
|
12086
|
+
},
|
|
12072
12087
|
{
|
|
12073
12088
|
"path": "tests/fixtures/session-506-digest.json",
|
|
12074
12089
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 449 operations across 68 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -343,6 +343,8 @@ function createClient(opts = {}) {
|
|
|
343
343
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
344
344
|
// POST /copy-desk/flags/{id}/close — rank: any-builder — POST /copy-desk/flags/:id/close
|
|
345
345
|
postCopyDeskFlagsIdClose: (args) => request("POST", "/copy-desk/flags/{id}/close", { hasBody: true }, args),
|
|
346
|
+
// GET /copy-desk/next — rank: any-builder — GET /copy-desk/next
|
|
347
|
+
getCopyDeskNext: (args) => request("GET", "/copy-desk/next", { hasBody: false }, args),
|
|
346
348
|
// GET /copy-desk/pages — rank: any-builder — GET /copy-desk/pages
|
|
347
349
|
getCopyDeskPages: (args) => request("GET", "/copy-desk/pages", { hasBody: false }, args),
|
|
348
350
|
// GET /copy-desk/pages/{pageId} — rank: any-builder — GET /copy-desk/pages/:pageId
|
|
@@ -342,6 +342,8 @@ function createClient(opts = {}) {
|
|
|
342
342
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
343
343
|
// POST /copy-desk/flags/{id}/close — rank: any-builder — POST /copy-desk/flags/:id/close
|
|
344
344
|
postCopyDeskFlagsIdClose: (args) => request("POST", "/copy-desk/flags/{id}/close", { hasBody: true }, args),
|
|
345
|
+
// GET /copy-desk/next — rank: any-builder — GET /copy-desk/next
|
|
346
|
+
getCopyDeskNext: (args) => request("GET", "/copy-desk/next", { hasBody: false }, args),
|
|
345
347
|
// GET /copy-desk/pages — rank: any-builder — GET /copy-desk/pages
|
|
346
348
|
getCopyDeskPages: (args) => request("GET", "/copy-desk/pages", { hasBody: false }, args),
|
|
347
349
|
// GET /copy-desk/pages/{pageId} — rank: any-builder — GET /copy-desk/pages/:pageId
|
|
@@ -760,6 +760,8 @@ export interface BongosClient {
|
|
|
760
760
|
postCopyDeskFlags(args: RequestArgs & { body: PostCopyDeskFlagsRequest }): Promise<PostCopyDeskFlagsResponse>;
|
|
761
761
|
/** POST /copy-desk/flags/{id}/close — rank: any-builder */
|
|
762
762
|
postCopyDeskFlagsIdClose(args: RequestArgs & { body: PostCopyDeskFlagsIdCloseRequest }): Promise<PostCopyDeskFlagsIdCloseResponse>;
|
|
763
|
+
/** GET /copy-desk/next — rank: any-builder */
|
|
764
|
+
getCopyDeskNext(args?: RequestArgs): Promise<ApiResponse>;
|
|
763
765
|
/** GET /copy-desk/pages — rank: any-builder */
|
|
764
766
|
getCopyDeskPages(args?: RequestArgs): Promise<ApiResponse>;
|
|
765
767
|
/** GET /copy-desk/pages/{pageId} — rank: any-builder */
|
|
@@ -339,6 +339,8 @@ export function createClient(opts = {}) {
|
|
|
339
339
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
340
340
|
// POST /copy-desk/flags/{id}/close — rank: any-builder — POST /copy-desk/flags/:id/close
|
|
341
341
|
postCopyDeskFlagsIdClose: (args) => request("POST", "/copy-desk/flags/{id}/close", { hasBody: true }, args),
|
|
342
|
+
// GET /copy-desk/next — rank: any-builder — GET /copy-desk/next
|
|
343
|
+
getCopyDeskNext: (args) => request("GET", "/copy-desk/next", { hasBody: false }, args),
|
|
342
344
|
// GET /copy-desk/pages — rank: any-builder — GET /copy-desk/pages
|
|
343
345
|
getCopyDeskPages: (args) => request("GET", "/copy-desk/pages", { hasBody: false }, args),
|
|
344
346
|
// GET /copy-desk/pages/{pageId} — rank: any-builder — GET /copy-desk/pages/:pageId
|
package/docs/api/openapi.json
CHANGED
|
@@ -5091,6 +5091,37 @@
|
|
|
5091
5091
|
]
|
|
5092
5092
|
}
|
|
5093
5093
|
},
|
|
5094
|
+
"/copy-desk/next": {
|
|
5095
|
+
"get": {
|
|
5096
|
+
"operationId": "get_copy_desk_next",
|
|
5097
|
+
"tags": [
|
|
5098
|
+
"copy-desk"
|
|
5099
|
+
],
|
|
5100
|
+
"summary": "GET /copy-desk/next",
|
|
5101
|
+
"description": "------------------------------------------------------------------------- GET /copy-desk/next — the Tweak/CopyWrite Flow's recommendation (task 1004318 / BV2.TW07, ADR 0341 D8): the page the caller should tweak next and why (\"2 people asked for it\"), plus the next few for \"pick another\". Order: asked for (most distinct askers, then the oldest ask), then changed since the last tweak (most changed lines), then untweaked (landing, builders, status, then inventory order); tweaked pages with no drift last. A page another builder holds, or whose round is in flight, is never recommended (it is listed in `skipped`). The caller's own held page is `resume`, not a recommendation. \"Take next\" is POST /copy-desk/pages/:pageId/claim on `recommended.page_id` (each candidate carries its `take`), one call. The availability rule here is the claim's own, so that call is not refused for a reason this read knew. rank: any signed-in builder. The read is the caller's own and needs no craft; the claim it points at is where the artist gate lives. -------------------------------------------------------------------------\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
5102
|
+
"x-rank": "any-builder",
|
|
5103
|
+
"x-source": "modules/copy-desk/routes/copy-desk.js",
|
|
5104
|
+
"responses": {
|
|
5105
|
+
"200": {
|
|
5106
|
+
"description": "Success."
|
|
5107
|
+
},
|
|
5108
|
+
"400": {
|
|
5109
|
+
"$ref": "#/components/responses/BadRequest"
|
|
5110
|
+
},
|
|
5111
|
+
"401": {
|
|
5112
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
5113
|
+
},
|
|
5114
|
+
"403": {
|
|
5115
|
+
"$ref": "#/components/responses/Forbidden"
|
|
5116
|
+
}
|
|
5117
|
+
},
|
|
5118
|
+
"security": [
|
|
5119
|
+
{
|
|
5120
|
+
"builderSession": []
|
|
5121
|
+
}
|
|
5122
|
+
]
|
|
5123
|
+
}
|
|
5124
|
+
},
|
|
5094
5125
|
"/copy-desk/pages": {
|
|
5095
5126
|
"get": {
|
|
5096
5127
|
"operationId": "get_copy_desk_pages",
|
|
@@ -28354,7 +28385,7 @@
|
|
|
28354
28385
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
28355
28386
|
}
|
|
28356
28387
|
},
|
|
28357
|
-
"x-endpoint-count":
|
|
28388
|
+
"x-endpoint-count": 449,
|
|
28358
28389
|
"x-schema-count": 484,
|
|
28359
28390
|
"x-undocumented-bodies": 9,
|
|
28360
28391
|
"x-response-schemas": 331,
|
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. 449 endpoints across 76 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`.
|
|
@@ -210,12 +210,13 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
210
210
|
| GET | `/api/bongos/connections/requests` | `any-builder` | — | MY pending requests, both directions (own data). |
|
|
211
211
|
| GET | `/api/bongos/connections/suggestions` | `any-builder` | — | "builders you may know" (R21, task 1002297; ADR 0202 D5). |
|
|
212
212
|
|
|
213
|
-
## `copy-desk` (
|
|
213
|
+
## `copy-desk` (11)
|
|
214
214
|
|
|
215
215
|
| Method | Path | Rank | Body | Description |
|
|
216
216
|
|---|---|---|---|---|
|
|
217
217
|
| POST | `/api/bongos/copy-desk/flags` | `any-builder` | `scope`, `surface`, `string_id`, `reason`, `kind` | ------------------------------------------------------------------------- POST /copy-desk/flags — file a flag. |
|
|
218
218
|
| POST | `/api/bongos/copy-desk/flags/:id/close` | `any-builder` | `status`, `resolution_note`, `resolved_task_id` | ------------------------------------------------------------------------- POST /copy-desk/flags/:id/close — resolve or dismiss. |
|
|
219
|
+
| GET | `/api/bongos/copy-desk/next` | `any-builder` | — | ------------------------------------------------------------------------- GET /copy-desk/next — the Tweak/CopyWrite Flow's recommendation… |
|
|
219
220
|
| GET | `/api/bongos/copy-desk/pages` | `any-builder` | — | ------------------------------------------------------------------------- GET /copy-desk/pages — every page's status, count and drift, pl… |
|
|
220
221
|
| GET | `/api/bongos/copy-desk/pages/:pageId` | `any-builder` | — | ------------------------------------------------------------------------- GET /copy-desk/pages/:pageId — one page: status, changelog, the… |
|
|
221
222
|
| POST | `/api/bongos/copy-desk/pages/:pageId/asks` | `any-builder` | `reason` | ------------------------------------------------------------------------- POST /copy-desk/pages/:pageId/asks — ask for a page (task 10043… |
|
|
@@ -2605,5 +2605,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2605
2605
|
landed since 1.19.1057 with no explicit bump. run 36390971623. (task 1002620)
|
|
2606
2606
|
1.19.1059 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2607
2607
|
landed since 1.19.1058 with no explicit bump. run 36415558760. (task 1002620)
|
|
2608
|
+
1.19.1060 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2609
|
+
landed since 1.19.1059 with no explicit bump. run 36418832327. (task 1002620)
|
|
2608
2610
|
---------------------------------------------------------------------------
|
|
2609
2611
|
```
|
|
@@ -297,6 +297,19 @@ async function countPageAskers(pageId) {
|
|
|
297
297
|
return rows[0] ? Number(rows[0].askers) : 0;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
// The recommendation's signal (TW07, ADR 0341 D8): every page with an open
|
|
301
|
+
// ask, its distinct askers and its OLDEST open ask (the tie-break). One grouped
|
|
302
|
+
// read over copy_desk_flags_page_open_idx, never a count per page.
|
|
303
|
+
async function pageAskSummary() {
|
|
304
|
+
const { rows } = await pool.query(
|
|
305
|
+
`SELECT page_id, COUNT(DISTINCT flagged_by)::int AS askers, MIN(flagged_at) AS oldest_ask_at
|
|
306
|
+
FROM copy_desk_flags
|
|
307
|
+
WHERE scope = 'page' AND status = 'open'
|
|
308
|
+
GROUP BY page_id`
|
|
309
|
+
);
|
|
310
|
+
return rows.map((r) => ({ page_id: r.page_id, askers: Number(r.askers) || 0, oldest_ask_at: r.oldest_ask_at }));
|
|
311
|
+
}
|
|
312
|
+
|
|
300
313
|
// An open ask is RESOLVED when the page's next tweak ships (ADR 0341 D6): every
|
|
301
314
|
// ask on the page filed before that ship, stamped with the tweak task. Guarded on
|
|
302
315
|
// status = 'open', so a re-run changes nothing and a dismissed ask stays dismissed.
|
|
@@ -316,5 +329,5 @@ module.exports = {
|
|
|
316
329
|
SCOPES, FLAG_SCOPES, PAGE_SCOPE, STATUSES, KINDS, CLOSING_STATUSES, MIN_REASON_CHARS, MAX_REASON_CHARS, MAX_NOTE_CHARS,
|
|
317
330
|
tidy, normalizeFlagPayload, normalizeResolution, normalizePageAsk,
|
|
318
331
|
insertFlag, listFlags, getFlag, closeFlag,
|
|
319
|
-
findOpenPageAsk, countPageAskers, resolvePageAsks,
|
|
332
|
+
findOpenPageAsk, countPageAskers, pageAskSummary, resolvePageAsks,
|
|
320
333
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// modules/copy-desk/page-status.js — the
|
|
2
|
-
// (task 1004316 / BV2.TW05,
|
|
1
|
+
// modules/copy-desk/page-status.js — the Tweak Mode READS, derived
|
|
2
|
+
// (task 1004316 / BV2.TW05, and the recommendation, task 1004318 / BV2.TW07;
|
|
3
|
+
// ADR 0341 D7 and D8).
|
|
3
4
|
//
|
|
4
5
|
// There is NO status table, by the planning session's ruling: a table would be a
|
|
5
6
|
// second record of what the task ledger already says. Everything here is a pure
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
// * docs/page-readings.json (what each page says now), for drift only;
|
|
14
15
|
// * the caller's `tweak.page_approved:` credit rows, for the tally.
|
|
15
16
|
//
|
|
16
|
-
// The
|
|
17
|
+
// The four reads:
|
|
17
18
|
//
|
|
18
19
|
// PER PAGE status (Untweaked / Text tweaked / UI tweaked / Both) with its
|
|
19
20
|
// count ("Text tweaked 2x"), the changelog, and the lines changed
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
// shipped round, and drift does not lower it.
|
|
23
24
|
// PER ARTIST the studio corner's tally: credits from approved pages, pages
|
|
24
25
|
// tweaked, and this week's gain.
|
|
26
|
+
// NEXT the recommendation (TW07): the page to tweak next, its why,
|
|
27
|
+
// and the next few for "pick another" (composeNext, below).
|
|
25
28
|
//
|
|
26
29
|
// Pure: no fs, no DB, no clock (the caller passes `now`). The route owns the I/O.
|
|
27
30
|
|
|
@@ -340,8 +343,179 @@ function composeTally({ builderId, tasks, creditRows, now }) {
|
|
|
340
343
|
};
|
|
341
344
|
}
|
|
342
345
|
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// The recommendation, "what should I tweak next" (task 1004318 / BV2.TW07,
|
|
348
|
+
// ADR 0341 D8). The fourth read, over the same ledger rows as the other three
|
|
349
|
+
// plus one more signal: the open page asks (flags.pageAskSummary()).
|
|
350
|
+
//
|
|
351
|
+
// WHO MAY BE RECOMMENDED A PAGE. Exactly the pages the claim route would hand
|
|
352
|
+
// the caller, decided by the claim's own rule (modules/lifecycle/
|
|
353
|
+
// page-tweak-claim.js) over the same fields, so "take next" on the answer can
|
|
354
|
+
// never be refused for a reason this read already knew:
|
|
355
|
+
//
|
|
356
|
+
// no open round free: the claim creates round n+1
|
|
357
|
+
// open, held by someone else SKIPPED (page_held)
|
|
358
|
+
// open, the caller's own web claim RESUME, reported apart, never a
|
|
359
|
+
// recommendation (the caller is already
|
|
360
|
+
// writing it)
|
|
361
|
+
// open, the caller's own CLI claim SKIPPED: they are applying it (in flight)
|
|
362
|
+
// open, unheld, roundOpenForWriting free: an unheld draft, taken over
|
|
363
|
+
// open, unheld, anything else SKIPPED (page_in_flight)
|
|
364
|
+
//
|
|
365
|
+
// THE ORDER (D8). Four tiers, and within each its own ties, then the base
|
|
366
|
+
// order (surface landing, builders, status, then inventory order):
|
|
367
|
+
//
|
|
368
|
+
// asked open asks: most distinct askers first, then the OLDEST ask
|
|
369
|
+
// changed tweaked and drifted: most changed lines first
|
|
370
|
+
// untweaked count 0
|
|
371
|
+
// tweaked tweaked with no drift, or drift unknown: last
|
|
372
|
+
//
|
|
373
|
+
// The mock's sample list puts an untweaked page before a changed one; the
|
|
374
|
+
// spec's order wins and the mock's list is sample data (ADR 0341 pick 8).
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
const NEXT_ALTERNATIVES = 4; // the mock's "pick another" row holds four
|
|
378
|
+
const NEXT_TIERS = Object.freeze(['asked', 'changed', 'untweaked', 'tweaked']);
|
|
379
|
+
|
|
380
|
+
function plural(n, one, many) { return `${n} ${n === 1 ? one : many}`; }
|
|
381
|
+
|
|
382
|
+
// Where the caller stands with one page, by the claim route's own rule.
|
|
383
|
+
function availability(rounds, builderId) {
|
|
384
|
+
const open = [...(rounds || [])].reverse().find((r) => !TERMINAL.has(r.status)) || null;
|
|
385
|
+
if (!open) return { kind: 'free', open: null };
|
|
386
|
+
if (open.holder_id != null) {
|
|
387
|
+
if (String(open.holder_id) !== String(builderId)) {
|
|
388
|
+
return { kind: 'held', open, holder: person(open.holder_id, open.holder_login) };
|
|
389
|
+
}
|
|
390
|
+
return open.holder_worktree ? { kind: 'in_flight', open } : { kind: 'resume', open };
|
|
391
|
+
}
|
|
392
|
+
return pages.roundOpenForWriting(open) ? { kind: 'free', open } : { kind: 'in_flight', open };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function surfaceWord(id) {
|
|
396
|
+
return (SURFACE_LABELS[id] || id).toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function candidateFor({ page, rounds, readingPage, readingsOk, ask, builderId, avail }) {
|
|
400
|
+
const counts = countsFor(rounds);
|
|
401
|
+
const drift = driftFor(readingPage, rounds, readingsOk);
|
|
402
|
+
const askers = ask ? ask.askers : 0;
|
|
403
|
+
// "Lines changed": a reworded line is one line that differs on each side of
|
|
404
|
+
// the multiset, so it counts once; a line only added, or only removed, still
|
|
405
|
+
// counts. The larger side is that number.
|
|
406
|
+
const changedLines = drift.known && drift.changed ? Math.max(drift.changed_count, drift.removed_count) : 0;
|
|
407
|
+
const last = latestShipped(rounds);
|
|
408
|
+
const yours = !!last && last.artist_id != null && String(last.artist_id) === String(builderId);
|
|
409
|
+
|
|
410
|
+
const parts = [];
|
|
411
|
+
if (askers > 0) parts.push(`${plural(askers, 'person', 'people')} asked for it`);
|
|
412
|
+
if (counts.count === 0) parts.push('untweaked');
|
|
413
|
+
else if (changedLines > 0) parts.push(`${plural(changedLines, 'line', 'lines')} changed since ${yours ? 'your' : 'the'} last pass`);
|
|
414
|
+
else parts.push(drift.known ? `${counts.status_label.toLowerCase()}, nothing changed since` : counts.status_label.toLowerCase());
|
|
415
|
+
|
|
416
|
+
let tier = 'tweaked';
|
|
417
|
+
if (askers > 0) tier = 'asked';
|
|
418
|
+
else if (changedLines > 0) tier = 'changed';
|
|
419
|
+
else if (counts.count === 0) tier = 'untweaked';
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
page_id: page.id,
|
|
423
|
+
surface: page.surface,
|
|
424
|
+
title: page.title || page.id,
|
|
425
|
+
path: page.path || null,
|
|
426
|
+
tier,
|
|
427
|
+
why: parts[0],
|
|
428
|
+
why_line: [surfaceWord(page.surface), ...parts].join(' · '),
|
|
429
|
+
askers,
|
|
430
|
+
oldest_ask_at: ask ? iso(ask.oldest_ask_at) : null,
|
|
431
|
+
changed_lines: changedLines,
|
|
432
|
+
drift_known: drift.known,
|
|
433
|
+
count: counts.count,
|
|
434
|
+
status_label: counts.status_label,
|
|
435
|
+
// An unheld draft the claim takes over, so the take is honest about it.
|
|
436
|
+
draft_round: avail.open ? { task_id: String(avail.open.id), round: avail.open.round } : null,
|
|
437
|
+
take: { method: 'POST', path: `/copy-desk/pages/${page.id}/claim` },
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function compareCandidates(a, b) {
|
|
442
|
+
const t = NEXT_TIERS.indexOf(a.tier) - NEXT_TIERS.indexOf(b.tier);
|
|
443
|
+
if (t) return t;
|
|
444
|
+
if (a.tier === 'asked') {
|
|
445
|
+
if (a.askers !== b.askers) return b.askers - a.askers;
|
|
446
|
+
const at = a.oldest_ask_at ? Date.parse(a.oldest_ask_at) : Infinity;
|
|
447
|
+
const bt = b.oldest_ask_at ? Date.parse(b.oldest_ask_at) : Infinity;
|
|
448
|
+
if (at !== bt) return at - bt;
|
|
449
|
+
}
|
|
450
|
+
if (a.tier === 'changed' && a.changed_lines !== b.changed_lines) return b.changed_lines - a.changed_lines;
|
|
451
|
+
return a._base - b._base;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// GET /copy-desk/next — the recommended page for `builderId`, its why, and
|
|
455
|
+
// the next few for "pick another". `asks` is flags.pageAskSummary()'s rows.
|
|
456
|
+
function composeNext({ inventory, readings, tasks, asks, builderId, alternatives = NEXT_ALTERNATIVES }) {
|
|
457
|
+
const byPage = indexRounds(tasks);
|
|
458
|
+
const readingsOk = !!readings;
|
|
459
|
+
const rIdx = readingIndex(readings);
|
|
460
|
+
const askIdx = new Map();
|
|
461
|
+
for (const a of asks || []) if (a && a.page_id && Number(a.askers) > 0) askIdx.set(a.page_id, { askers: Number(a.askers), oldest_ask_at: a.oldest_ask_at });
|
|
462
|
+
|
|
463
|
+
const surfaceRank = new Map(orderedSurfaces(inventory).map((id, i) => [id, i]));
|
|
464
|
+
const invPages = ((inventory && inventory.pages) || []).filter((p) => p && p.id);
|
|
465
|
+
const base = new Map(invPages
|
|
466
|
+
.map((p, i) => ({ id: p.id, s: surfaceRank.has(p.surface) ? surfaceRank.get(p.surface) : surfaceRank.size, i }))
|
|
467
|
+
.sort((a, b) => a.s - b.s || a.i - b.i)
|
|
468
|
+
.map((x, k) => [x.id, k]));
|
|
469
|
+
|
|
470
|
+
let resume = null;
|
|
471
|
+
const skipped = [];
|
|
472
|
+
const candidates = [];
|
|
473
|
+
for (const page of invPages) {
|
|
474
|
+
const rounds = byPage.get(page.id) || [];
|
|
475
|
+
const avail = availability(rounds, builderId);
|
|
476
|
+
if (avail.kind === 'resume') {
|
|
477
|
+
resume = {
|
|
478
|
+
page_id: page.id, surface: page.surface, title: page.title || page.id, path: page.path || null,
|
|
479
|
+
task_id: String(avail.open.id), round: avail.open.round,
|
|
480
|
+
take: { method: 'POST', path: `/copy-desk/pages/${page.id}/claim` },
|
|
481
|
+
};
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (avail.kind === 'held' || avail.kind === 'in_flight') {
|
|
485
|
+
skipped.push({
|
|
486
|
+
page_id: page.id,
|
|
487
|
+
reason: avail.kind === 'held' ? 'page_held' : 'page_in_flight',
|
|
488
|
+
state: roundState(avail.open),
|
|
489
|
+
...(avail.holder ? { holder: avail.holder } : {}),
|
|
490
|
+
});
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const c = candidateFor({ page, rounds, readingPage: rIdx.get(page.id), readingsOk, ask: askIdx.get(page.id), builderId, avail });
|
|
494
|
+
c._base = base.get(page.id);
|
|
495
|
+
candidates.push(c);
|
|
496
|
+
}
|
|
497
|
+
candidates.sort(compareCandidates);
|
|
498
|
+
for (const c of candidates) delete c._base;
|
|
499
|
+
|
|
500
|
+
const n = Math.max(0, Math.floor(Number(alternatives) || 0));
|
|
501
|
+
return {
|
|
502
|
+
builder_id: String(builderId),
|
|
503
|
+
resume,
|
|
504
|
+
recommended: candidates[0] || null,
|
|
505
|
+
alternatives: candidates.slice(1, 1 + n),
|
|
506
|
+
candidates: candidates.length,
|
|
507
|
+
// "4 pages have been asked for" (the mock's Artist Review Status line):
|
|
508
|
+
// every inventory page with an open ask, held or not.
|
|
509
|
+
asked_pages: invPages.filter((p) => askIdx.has(p.id)).length,
|
|
510
|
+
skipped,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
343
514
|
module.exports = {
|
|
344
515
|
TALLY_REASON_PREFIX,
|
|
516
|
+
NEXT_ALTERNATIVES,
|
|
517
|
+
NEXT_TIERS,
|
|
518
|
+
composeNext,
|
|
345
519
|
SURFACE_ORDER,
|
|
346
520
|
SURFACE_LABELS,
|
|
347
521
|
STATUS,
|
|
@@ -663,6 +663,47 @@ module.exports = function buildCopyDeskRouter() {
|
|
|
663
663
|
}
|
|
664
664
|
});
|
|
665
665
|
|
|
666
|
+
// -------------------------------------------------------------------------
|
|
667
|
+
// GET /copy-desk/next — the Tweak/CopyWrite Flow's recommendation (task
|
|
668
|
+
// 1004318 / BV2.TW07, ADR 0341 D8): the page the caller should tweak next
|
|
669
|
+
// and why ("2 people asked for it"), plus the next few for "pick another".
|
|
670
|
+
//
|
|
671
|
+
// Order: asked for (most distinct askers, then the oldest ask), then changed
|
|
672
|
+
// since the last tweak (most changed lines), then untweaked (landing,
|
|
673
|
+
// builders, status, then inventory order); tweaked pages with no drift last.
|
|
674
|
+
// A page another builder holds, or whose round is in flight, is never
|
|
675
|
+
// recommended (it is listed in `skipped`). The caller's own held page is
|
|
676
|
+
// `resume`, not a recommendation.
|
|
677
|
+
//
|
|
678
|
+
// "Take next" is POST /copy-desk/pages/:pageId/claim on `recommended.page_id`
|
|
679
|
+
// (each candidate carries its `take`), one call. The availability rule here
|
|
680
|
+
// is the claim's own, so that call is not refused for a reason this read knew.
|
|
681
|
+
//
|
|
682
|
+
// rank: any signed-in builder. The read is the caller's own and needs no
|
|
683
|
+
// craft; the claim it points at is where the artist gate lives.
|
|
684
|
+
// -------------------------------------------------------------------------
|
|
685
|
+
router.get('/copy-desk/next', api.requireBuilder, async (req, res) => {
|
|
686
|
+
try {
|
|
687
|
+
const inv = inventoryOrFail(res);
|
|
688
|
+
if (!inv) return;
|
|
689
|
+
const lifecycle = pageReadsOrFail(res);
|
|
690
|
+
if (!lifecycle) return;
|
|
691
|
+
const readings = readingsState();
|
|
692
|
+
const [tasks, asks] = await Promise.all([
|
|
693
|
+
lifecycle.listPageTweakTasks(),
|
|
694
|
+
flags.pageAskSummary(),
|
|
695
|
+
]);
|
|
696
|
+
const out = pageStatus.composeNext({ inventory: inv.data, readings: readings.data, tasks, asks, builderId: req.builder.id });
|
|
697
|
+
out.inventory = { at: inv.at };
|
|
698
|
+
out.readings = readings.info;
|
|
699
|
+
res.set('Cache-Control', 'no-store');
|
|
700
|
+
res.json(out);
|
|
701
|
+
} catch (err) {
|
|
702
|
+
log.error({ err }, 'GET /copy-desk/next failed');
|
|
703
|
+
if (!res.headersSent) res.fail('copy_next_failed', 500);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
666
707
|
// -------------------------------------------------------------------------
|
|
667
708
|
// POST /copy-desk/pages/:pageId/claim — claim a page's tweak (task 1004317 /
|
|
668
709
|
// BV2.TW06, ADR 0341 D2 and D5). Also Resume and "Take next".
|