@bongos/core 1.21.168 → 1.21.170
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 +34 -29
- package/docs/adr/0344-a-tester-is-a-project-that-opts-in-once.md +12 -0
- package/docs/api/openapi.json +2 -2
- package/docs/architecture.md +2 -0
- package/docs/copy-inventory.md +22 -20
- package/docs/copy-registry.json +41 -23
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -1
- package/docs/page-readings.json +1 -1
- package/modules/copy-desk/page-status.js +38 -1
- package/modules/hall-ui/public/approval-queue-lib.js +15 -1
- package/modules/hall-ui/public/approval-queue.css +9 -0
- package/modules/hall-ui/public/approval-queue.js +18 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/module-assess-installs.js +94 -9
- package/src/bongos/module-store.js +47 -9
- package/src/bongos/routes/modules.js +26 -10
- package/src/module-api.js +1 -1
- package/tests/hall_approval_queue.mjs +35 -0
- package/tests/module_assess_installs.mjs +72 -6
- package/tests/module_channel_install.mjs +112 -0
- package/tests/module_upgrade_updates.mjs +2 -2
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 2793,
|
|
7
|
+
"unique_ids": 2374,
|
|
8
|
+
"certain": 2793,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 243
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 150,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1973
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -2260,8 +2260,8 @@
|
|
|
2260
2260
|
"text": "highlighted: the words you changed",
|
|
2261
2261
|
"occurrences": 2,
|
|
2262
2262
|
"at": [
|
|
2263
|
-
"modules/hall-ui/public/approval-queue.js:
|
|
2264
|
-
"modules/hall-ui/public/approval-queue.js:
|
|
2263
|
+
"modules/hall-ui/public/approval-queue.js:171",
|
|
2264
|
+
"modules/hall-ui/public/approval-queue.js:184"
|
|
2265
2265
|
]
|
|
2266
2266
|
},
|
|
2267
2267
|
{
|
|
@@ -9437,7 +9437,7 @@
|
|
|
9437
9437
|
"surface": "builders-hall",
|
|
9438
9438
|
"text": "No render is attached to this page yet. /tweak attaches them when it applies a page; until then there is nothing to approve from, so send it back or wait.",
|
|
9439
9439
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
9440
|
-
"line":
|
|
9440
|
+
"line": 136,
|
|
9441
9441
|
"origin": "js-markup",
|
|
9442
9442
|
"confidence": "certain"
|
|
9443
9443
|
},
|
|
@@ -9590,7 +9590,7 @@
|
|
|
9590
9590
|
"surface": "builders-hall",
|
|
9591
9591
|
"text": "No {…} render is attached to this page.",
|
|
9592
9592
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
9593
|
-
"line":
|
|
9593
|
+
"line": 135,
|
|
9594
9594
|
"origin": "js-markup",
|
|
9595
9595
|
"confidence": "certain"
|
|
9596
9596
|
},
|
|
@@ -9608,7 +9608,7 @@
|
|
|
9608
9608
|
"surface": "builders-hall",
|
|
9609
9609
|
"text": "No {…} {…} render; showing {…} {…}.",
|
|
9610
9610
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
9611
|
-
"line":
|
|
9611
|
+
"line": 140,
|
|
9612
9612
|
"origin": "js-markup",
|
|
9613
9613
|
"confidence": "certain"
|
|
9614
9614
|
},
|
|
@@ -10229,7 +10229,7 @@
|
|
|
10229
10229
|
"surface": "builders-hall",
|
|
10230
10230
|
"text": "Only the artist who wrote this page, or another artist, can approve it or send it back.",
|
|
10231
10231
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
10232
|
-
"line":
|
|
10232
|
+
"line": 171,
|
|
10233
10233
|
"origin": "js-markup",
|
|
10234
10234
|
"confidence": "certain"
|
|
10235
10235
|
},
|
|
@@ -10499,7 +10499,7 @@
|
|
|
10499
10499
|
"surface": "builders-hall",
|
|
10500
10500
|
"text": "Opening the rendered page…",
|
|
10501
10501
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
10502
|
-
"line":
|
|
10502
|
+
"line": 130,
|
|
10503
10503
|
"origin": "js-markup",
|
|
10504
10504
|
"confidence": "certain"
|
|
10505
10505
|
},
|
|
@@ -13131,6 +13131,24 @@
|
|
|
13131
13131
|
"origin": "html-title",
|
|
13132
13132
|
"confidence": "certain"
|
|
13133
13133
|
},
|
|
13134
|
+
{
|
|
13135
|
+
"id": "30b78bc382a1",
|
|
13136
|
+
"surface": "builders-hall",
|
|
13137
|
+
"text": "Submitted, not applied yet",
|
|
13138
|
+
"file": "modules/hall-ui/public/approval-queue.js",
|
|
13139
|
+
"line": 193,
|
|
13140
|
+
"origin": "js-markup",
|
|
13141
|
+
"confidence": "certain"
|
|
13142
|
+
},
|
|
13143
|
+
{
|
|
13144
|
+
"id": "7d56be90cd16",
|
|
13145
|
+
"surface": "builders-hall",
|
|
13146
|
+
"text": "Submitted, not applied yet ({…})",
|
|
13147
|
+
"file": "modules/hall-ui/public/approval-queue.js",
|
|
13148
|
+
"line": 193,
|
|
13149
|
+
"origin": "js-markup",
|
|
13150
|
+
"confidence": "certain"
|
|
13151
|
+
},
|
|
13134
13152
|
{
|
|
13135
13153
|
"id": "f79e1fb8ebd4",
|
|
13136
13154
|
"surface": "builders-hall",
|
|
@@ -13802,7 +13820,7 @@
|
|
|
13802
13820
|
"surface": "builders-hall",
|
|
13803
13821
|
"text": "The hero line is too long on a phone.",
|
|
13804
13822
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
13805
|
-
"line":
|
|
13823
|
+
"line": 177,
|
|
13806
13824
|
"origin": "js-markup",
|
|
13807
13825
|
"confidence": "certain"
|
|
13808
13826
|
},
|
|
@@ -13973,7 +13991,7 @@
|
|
|
13973
13991
|
"surface": "builders-hall",
|
|
13974
13992
|
"text": "The page, rendered",
|
|
13975
13993
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
13976
|
-
"line":
|
|
13994
|
+
"line": 215,
|
|
13977
13995
|
"origin": "js-markup",
|
|
13978
13996
|
"confidence": "certain"
|
|
13979
13997
|
},
|
|
@@ -14054,7 +14072,7 @@
|
|
|
14054
14072
|
"surface": "builders-hall",
|
|
14055
14073
|
"text": "The renders could not be read just now. Close the queue and open it again.",
|
|
14056
14074
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
14057
|
-
"line":
|
|
14075
|
+
"line": 131,
|
|
14058
14076
|
"origin": "js-markup",
|
|
14059
14077
|
"confidence": "certain"
|
|
14060
14078
|
},
|
|
@@ -14405,7 +14423,7 @@
|
|
|
14405
14423
|
"surface": "builders-hall",
|
|
14406
14424
|
"text": "The words you changed",
|
|
14407
14425
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
14408
|
-
"line":
|
|
14426
|
+
"line": 215,
|
|
14409
14427
|
"origin": "js-markup",
|
|
14410
14428
|
"confidence": "certain"
|
|
14411
14429
|
},
|
|
@@ -14801,7 +14819,7 @@
|
|
|
14801
14819
|
"surface": "builders-hall",
|
|
14802
14820
|
"text": "This round changed no lines.",
|
|
14803
14821
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
14804
|
-
"line":
|
|
14822
|
+
"line": 150,
|
|
14805
14823
|
"origin": "js-markup",
|
|
14806
14824
|
"confidence": "certain"
|
|
14807
14825
|
},
|
|
@@ -15575,7 +15593,7 @@
|
|
|
15575
15593
|
"surface": "builders-hall",
|
|
15576
15594
|
"text": "What needs changing",
|
|
15577
15595
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
15578
|
-
"line":
|
|
15596
|
+
"line": 177,
|
|
15579
15597
|
"origin": "js-markup",
|
|
15580
15598
|
"confidence": "certain"
|
|
15581
15599
|
},
|
|
@@ -16763,7 +16781,7 @@
|
|
|
16763
16781
|
"surface": "builders-hall",
|
|
16764
16782
|
"text": "approval queue",
|
|
16765
16783
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
16766
|
-
"line":
|
|
16784
|
+
"line": 208,
|
|
16767
16785
|
"origin": "js-markup",
|
|
16768
16786
|
"confidence": "certain"
|
|
16769
16787
|
},
|
|
@@ -16772,7 +16790,7 @@
|
|
|
16772
16790
|
"surface": "builders-hall",
|
|
16773
16791
|
"text": "approve and ship",
|
|
16774
16792
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
16775
|
-
"line":
|
|
16793
|
+
"line": 175,
|
|
16776
16794
|
"origin": "js-markup",
|
|
16777
16795
|
"confidence": "certain"
|
|
16778
16796
|
},
|
|
@@ -17348,7 +17366,7 @@
|
|
|
17348
17366
|
"surface": "builders-hall",
|
|
17349
17367
|
"text": "highlighted: the words you changed",
|
|
17350
17368
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
17351
|
-
"line":
|
|
17369
|
+
"line": 171,
|
|
17352
17370
|
"origin": "js-markup",
|
|
17353
17371
|
"confidence": "certain"
|
|
17354
17372
|
},
|
|
@@ -17357,7 +17375,7 @@
|
|
|
17357
17375
|
"surface": "builders-hall",
|
|
17358
17376
|
"text": "highlighted: the words you changed",
|
|
17359
17377
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
17360
|
-
"line":
|
|
17378
|
+
"line": 184,
|
|
17361
17379
|
"origin": "js-markup",
|
|
17362
17380
|
"confidence": "certain"
|
|
17363
17381
|
},
|
|
@@ -18005,7 +18023,7 @@
|
|
|
18005
18023
|
"surface": "builders-hall",
|
|
18006
18024
|
"text": "open {…}",
|
|
18007
18025
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
18008
|
-
"line":
|
|
18026
|
+
"line": 166,
|
|
18009
18027
|
"origin": "js-markup",
|
|
18010
18028
|
"confidence": "certain"
|
|
18011
18029
|
},
|
|
@@ -18275,7 +18293,7 @@
|
|
|
18275
18293
|
"surface": "builders-hall",
|
|
18276
18294
|
"text": "send it back",
|
|
18277
18295
|
"file": "modules/hall-ui/public/approval-queue.js",
|
|
18278
|
-
"line":
|
|
18296
|
+
"line": 182,
|
|
18279
18297
|
"origin": "js-markup",
|
|
18280
18298
|
"confidence": "certain"
|
|
18281
18299
|
},
|
|
@@ -3173,5 +3173,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3173
3173
|
landed since 1.21.166 with no explicit bump. run 39986992124. (task 1002620)
|
|
3174
3174
|
1.21.168 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3175
3175
|
landed since 1.21.167 with no explicit bump. run 39990468332. (task 1002620)
|
|
3176
|
+
1.21.169 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3177
|
+
landed since 1.21.168 with no explicit bump. run 39992064615. (task 1002620)
|
|
3178
|
+
1.21.170 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3179
|
+
landed since 1.21.169 with no explicit bump. run 39994339257. (task 1002620)
|
|
3176
3180
|
---------------------------------------------------------------------------
|
|
3177
3181
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -394,6 +394,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
|
|
|
394
394
|
- Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
|
|
395
395
|
- **A how-to is required** ([ADR 0347](adr/0347-every-store-module-ships-a-how-to.md)). `modules/<key>/HOWTO.md` is what a buyer reads — the store shows it as the module's page — so it is written for the person who installs the module, not for an AI working inside it (that is `CLAUDE.md`). It is plain Markdown: any AI or person can write it. Publish refuses the version unless it has these five level-2 headings, in any order and any case, each with some text under it: `## What it does`, `## Install and enable`, `## How to use it` (with one worked example), `## Configuration` ("None." is fine) and `## Limits and known issues` ("None known." is fine). HTML comments don't count as text, so the scaffold's prompts must be replaced. The refusal names each failing section. The check is one function, `checkHowto` in `scripts/gds/module-artifact.js`; the CLI runs it before uploading and the store runs it again on upload, so a hand-built upload can't skip it. `bongos module check` reports it as advice only — an upstream submit is not gated by it. Install and update don't re-check it, so a version published before the gate still installs. The file travels in the tarball, so its hash pins it to the version. An optional `howto.artifactUrl` in `module.json` (an `https://` link on `claude.ai`) is shown as an extra, never graded.
|
|
396
396
|
- **Getting listed** (task 1003807, [ADR 0343](adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md)). Publishing a version does not by itself put it in the store's catalog, `GET /store/modules`. The catalog shows each module's newest version, judged by its current score. It must pass the Security gate first; a version that failed it, or has not been checked yet, is not listed. A new version with no install or reliability data yet is listed with a **New** label and is not ranked; the floor does not apply to it until it has a real score. Every other version is listed only if its overall score is **at least 50 out of 100**. Below that it is not listed at all. The floor is fixed in Bongos itself (`QUALITY_FLOOR` in `src/bongos/module-listing.js`) and returned in the catalog response; no author or instance setting moves it, and changing it is an owner-approved, reviewed task.
|
|
397
|
+
- **Tester feedback** (task 1003806, ADR 0344 D5). While a version is on the tester channel, what tester projects report about it becomes its Tester feedback part: the share of testers whose install worked and who hit no runtime errors, blended with ten neutral testers so a small sample cannot score high — `round(100 × (went well + 5) / (testers + 10))`. Reports from after the version left the tester channel, and the author's own, never count; under 3 testers it is not scored yet. The rule is `assessTesterFeedback` in `scripts/gds/module-assess-installs.js`.
|
|
397
398
|
- **What a module is strong and weak at** (task 1003799, ADR 0343 D4). Each catalog entry also carries `parts` (the current result of Tests, Clean installs, Reliability, Tester feedback and How-to) and a reading of them: `strengths` (a part scored **80 or more**), `weaknesses` (a part scored **below the floor**) and `unmeasured` (a part with no score yet, which is never counted as weak). Each strength and weakness has a one-sentence `detail` saying what the number rests on. Security is not among them: every listed version has passed it. The reading is `readParts` in `src/bongos/module-listing.js`; the hall's Modules tab shows it in its "In the store" group, so two modules with the same overall score can be told apart.
|
|
398
399
|
- **Sorting by a part** (task 1003800). `GET /store/modules?sort=<part>` orders the listed modules by one part a buyer cares about: `tests`, `install`, `reliability`, `tester_feedback` or `docs` (the response lists them as `sort_parts`, and says which one it applied as `sort`). Modules with a score for that part come first, highest first; a tie goes to the higher overall score, then the module key. A module with no score for that part goes last, so missing data never wins or loses a place on its own. New modules are not sorted: they are never ranked. Any other `sort` value is refused with a 400. Without `sort`, both lists are in key order. The rule is `sortByPart` in `src/bongos/module-listing.js`.
|
|
399
400
|
- **Testing new versions first** (task 1003802, [ADR 0344](adr/0344-a-tester-is-a-project-that-opts-in-once.md) D1). A project can choose to get each new version of its modules before everyone else. It is the project that opts in, never one builder for someone else's project, and never by default. `GET /modules/testing` returns `{ tester, general_only, updated_at, updated_by }` to anyone signed in. `POST /modules/testing { tester: true|false }` switches it, and `POST /modules/:key/general-only { general_only: true|false }` keeps one module on the versions everyone gets while the rest are tested. Both writes take the `module.enable` permission, the same one that turns modules on and off. Both are reversible: switching testing off puts the project back on general versions from its next update, and a module's "general only" mark is kept if testing is switched off and on again. The choice is stored in the project's own settings (`modules.tester`, `modules.general_only`); anything that cannot be read counts as not testing. `testsVersionsOf` in `src/bongos/module-testing.js` is the one answer to "does this project get the tester version of this module", which installs and updates use (task 1003805). The hall's Modules tab has the switch in its "Testing new versions" group, and the general-only control in each module's details.
|
|
@@ -407,7 +408,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
|
|
|
407
408
|
- Install never enables. Switch the module on from the hall's Modules tab.
|
|
408
409
|
- **Every version's how-to is a page** (task 1004366). `GET /store/modules/:key/versions/:version/howto` reads the `HOWTO.md` from that version's own stored tarball, after the same hash check install runs, and returns it as text; the hall shows it at `/builders/modules?howto=<key>&version=<X.Y.Z>`, rendered through the hall's escaping markdown reader (`md-reader.js`) because the file is author-written. Any signed-in builder can read it without holding the module; a delisted module's how-to reads only for people who hold it. "Save as PDF" is the browser's own print, with a print sheet that keeps only the document. The optional `howto.artifactUrl` shows as "Also available as a Claude page". Install prints the link, and a Modules-tab row links it when the module on disk is a published store version. A version published before the gate has none and answers 404 `howto_missing`.
|
|
409
410
|
- A module already on disk, in the instance's `modules/` or bundled with the core, is refused; moving one to a newer version is `update`. A module this core cannot mount is refused before download.
|
|
410
|
-
- A delisted module cannot be acquired or downloaded.
|
|
411
|
+
- A delisted module cannot be acquired or downloaded. **Install and update honour the channel** (task 1003805, [ADR 0344](adr/0344-a-tester-is-a-project-that-opts-in-once.md) D1/D2): a project that tests a module (`testsVersionsOf`) is handed the newest version on `tester` or `general`, every other project the newest on `general`, and nobody a `withdrawn` one (`servedChannels` in `src/bongos/module-store.js`). The acquire answer names the version's `channel`. Asking for an exact version off the project's channels, or downloading it, is refused with `version_not_on_channel` (409); a module with nothing on them yet answers `no_versions_on_channel` (404). `GET /store/modules/latest`, which `bongos upgrade` reads, counts only versions on those channels, so it reports what `bongos module update` would hand over. A project that stops testing keeps the tester version it has; its next update is the newest general one, and update only moves forward, so nothing is rolled back. Recording a version already placed (`POST …/acquired`) does not re-check the channel.
|
|
411
412
|
|
|
412
413
|
`bongos module update <key> [--version X.Y.Z]` moves an installed store module forward through the same three calls, so a paid module and a free one take one identical path, and a module you already hold is never charged again (`tests/module_update.mjs` fails if a price-conditional branch appears).
|
|
413
414
|
|
package/docs/page-readings.json
CHANGED
|
@@ -3978,7 +3978,7 @@
|
|
|
3978
3978
|
"surface": "builders",
|
|
3979
3979
|
"title": "Studio",
|
|
3980
3980
|
"states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back"],
|
|
3981
|
-
"files_hash": "
|
|
3981
|
+
"files_hash": "a072c3fdf156d639",
|
|
3982
3982
|
"reading_hash": "55e9090fdd96a979",
|
|
3983
3983
|
"counts": {"lines":30,"placed":16,"shared":6,"unplaced":8},
|
|
3984
3984
|
"omitted": {"data":4},
|
|
@@ -800,8 +800,40 @@ function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
|
|
|
800
800
|
};
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
+
// The submitted-but-unapplied rounds, oldest first, in the in-flight list's
|
|
804
|
+
// own words (task 1004662's IN_FLIGHT.submitted), with how long each waited.
|
|
805
|
+
function notAppliedFor({ byPage, invById, now }) {
|
|
806
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now);
|
|
807
|
+
const rows = [];
|
|
808
|
+
for (const [pageId, rounds] of byPage) {
|
|
809
|
+
const open = rounds.filter((r) => !TERMINAL.has(r.status));
|
|
810
|
+
const round = open.length ? open[open.length - 1] : null;
|
|
811
|
+
if (!round || roundState(round) !== 'submitted') continue;
|
|
812
|
+
const page = invById.get(pageId) || null;
|
|
813
|
+
const b = pages.parseBatchBlock(round.description);
|
|
814
|
+
const since = iso((b.ok && b.batch.submitted_at) || round.updated_at || round.created_at);
|
|
815
|
+
const ms = since ? nowMs - Date.parse(since) : null;
|
|
816
|
+
const surface = page ? page.surface : String(pageId).split(':')[0];
|
|
817
|
+
rows.push({
|
|
818
|
+
page_id: pageId,
|
|
819
|
+
surface,
|
|
820
|
+
surface_word: surfaceWord(surface),
|
|
821
|
+
title: page ? (page.title || page.id) : pageId,
|
|
822
|
+
path: page ? page.path || null : null,
|
|
823
|
+
task_id: String(round.id),
|
|
824
|
+
state: 'submitted',
|
|
825
|
+
state_word: IN_FLIGHT.submitted.word,
|
|
826
|
+
since,
|
|
827
|
+
waited_days: ms == null ? null : Math.max(0, Math.floor(ms / DAY_MS)),
|
|
828
|
+
line_count: b.ok ? b.batch.lines.length : 0,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
rows.sort((a, b) => String(a.since || '').localeCompare(String(b.since || '')) || Number(a.task_id) - Number(b.task_id));
|
|
832
|
+
return { not_applied: rows, not_applied_count: rows.length };
|
|
833
|
+
}
|
|
834
|
+
|
|
803
835
|
// GET /copy-desk/approvals — the queue, "1 of 2".
|
|
804
|
-
function composeApprovals({ inventory, tasks, builderId, crafts }) {
|
|
836
|
+
function composeApprovals({ inventory, tasks, builderId, crafts, now = Date.now() }) {
|
|
805
837
|
const byPage = indexRounds(tasks);
|
|
806
838
|
const invById = new Map(((inventory && inventory.pages) || []).filter((p) => p && p.id).map((p) => [p.id, p]));
|
|
807
839
|
const isArtist = (crafts || []).includes('artist');
|
|
@@ -825,6 +857,11 @@ function composeApprovals({ inventory, tasks, builderId, crafts }) {
|
|
|
825
857
|
is_artist: isArtist,
|
|
826
858
|
count: queue.length,
|
|
827
859
|
queue,
|
|
860
|
+
// task 1004765: rounds SUBMITTED but not yet applied. They sat unseen for
|
|
861
|
+
// weeks because this queue listed only applied rounds; they are listed
|
|
862
|
+
// apart, never in `queue`, so nothing can approve or send back a page that
|
|
863
|
+
// has not been applied, and `count` (the studio door) stays reviewable only.
|
|
864
|
+
...notAppliedFor({ byPage, invById, now }),
|
|
828
865
|
};
|
|
829
866
|
}
|
|
830
867
|
|
|
@@ -130,6 +130,20 @@
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// task 1004765: a submitted round nobody has applied yet. Listed under the
|
|
134
|
+
// reviewable pages, marked so, with how long it has waited; it carries no
|
|
135
|
+
// verb, since there is no applied page to judge.
|
|
136
|
+
function notAppliedRow(r) {
|
|
137
|
+
const d = r && r.waited_days;
|
|
138
|
+
const waited = d == null ? '' : (d === 0 ? 'today' : `${d} day${d === 1 ? '' : 's'}`);
|
|
139
|
+
return {
|
|
140
|
+
task_id: String(r.task_id),
|
|
141
|
+
title: displayTitle(r),
|
|
142
|
+
mark: 'not applied yet',
|
|
143
|
+
line: `${r.state_word}${waited ? ` · waiting ${waited}` : ''}. The words are waiting to be applied, so there is nothing to approve yet.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
function noteProblem(note) {
|
|
134
148
|
const n = String(note == null ? '' : note).replace(/\s+/g, ' ').trim();
|
|
135
149
|
if (n.length < MIN_NOTE) return `Say what needs changing in at least ${MIN_NOTE} characters.`;
|
|
@@ -157,7 +171,7 @@
|
|
|
157
171
|
const api = {
|
|
158
172
|
PHASES, DEVICES, MODES, MIN_NOTE, MAX_NOTE,
|
|
159
173
|
slotName, renderIndex, pickRender, hasAnyRender, displayTitle,
|
|
160
|
-
eyebrow, subLine, doorDetail, refusedWords, lineRow, noteProblem, approvedLine, sentBackLine,
|
|
174
|
+
eyebrow, subLine, doorDetail, refusedWords, lineRow, notAppliedRow, noteProblem, approvedLine, sentBackLine,
|
|
161
175
|
};
|
|
162
176
|
if (typeof window !== 'undefined') window.OTBApprovalQueueLib = api;
|
|
163
177
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -230,6 +230,15 @@ html.aq-open { overflow: hidden; }
|
|
|
230
230
|
.aq-line__pinned { font-size: 13px; line-height: 1.45; color: var(--aq-link); overflow-wrap: anywhere; }
|
|
231
231
|
.aq-lines__none { margin: 0; font-size: 14px; color: var(--aq-ink-faint); }
|
|
232
232
|
|
|
233
|
+
/* task 1004765: submitted rounds not applied yet, below the reviewable page. */
|
|
234
|
+
.aq-pending { padding: 14px 20px 18px; border-top: 1px solid var(--aq-rule); }
|
|
235
|
+
.aq-pending__h { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: var(--aq-ink-faint); }
|
|
236
|
+
.aq-pending__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
237
|
+
.aq-pending__row { display: flex; flex-wrap: wrap; gap: 4px 8px; font-size: 14px; }
|
|
238
|
+
.aq-pending__title { font-weight: 600; }
|
|
239
|
+
.aq-pending__mark { font-family: var(--aq-mono); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--aq-link); }
|
|
240
|
+
.aq-pending__line { flex-basis: 100%; font-size: 13px; color: var(--aq-ink-faint); overflow-wrap: anywhere; }
|
|
241
|
+
|
|
233
242
|
/* The decision row. */
|
|
234
243
|
.aq-foot {
|
|
235
244
|
display: flex;
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
state.loadError = null;
|
|
106
106
|
state.loaded = true;
|
|
107
107
|
state.queue = (res.data && res.data.queue) || [];
|
|
108
|
+
state.notApplied = (res.data && res.data.not_applied) || [];
|
|
108
109
|
if (state.idx >= state.queue.length) state.idx = 0;
|
|
109
110
|
tellCount();
|
|
110
111
|
}
|
|
@@ -183,6 +184,19 @@
|
|
|
183
184
|
return `<div class="aq-foot">${approve}${note}<span class="aq-hint">highlighted: the words you changed</span></div>${err}`;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// task 1004765: submitted rounds nobody has applied, below the reviewable
|
|
188
|
+
// pages. The server lists them apart; the panel only words them, and gives
|
|
189
|
+
// them no approve or send-back verb.
|
|
190
|
+
function notAppliedHtml() {
|
|
191
|
+
const rows = (state.notApplied || []).map((r) => L.notAppliedRow(r));
|
|
192
|
+
if (!rows.length) return '';
|
|
193
|
+
return `<section class="aq-pending" aria-label="Submitted, not applied yet">
|
|
194
|
+
<h3 class="aq-pending__h">Submitted, not applied yet (${rows.length})</h3>
|
|
195
|
+
<ul class="aq-pending__list">${rows.map((r) => `
|
|
196
|
+
<li class="aq-pending__row" data-task="${esc(r.task_id)}"><span class="aq-pending__title">${esc(r.title)}</span> <span class="aq-pending__mark">${esc(r.mark)}</span><span class="aq-pending__line">${esc(r.line)}</span></li>`).join('')}</ul>
|
|
197
|
+
</section>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
function paint() {
|
|
187
201
|
if (!state.open) return;
|
|
188
202
|
const keepNote = document.getElementById('aq-note');
|
|
@@ -193,7 +207,8 @@
|
|
|
193
207
|
const words = state.loadError || (state.loaded ? 'Nothing is waiting for you. A page appears here once /tweak has applied it and its grade has passed.' : 'Opening the Approval queue…');
|
|
194
208
|
dialog.innerHTML = `
|
|
195
209
|
<div class="aq-head"><span class="aq-eyebrow">approval queue</span><h2 class="aq-title" id="aq-title">${state.loaded ? 'Nothing waiting' : 'Approval queue'}</h2><span class="aq-head__gap"></span>${close}</div>
|
|
196
|
-
<div class="aq-body aq-body--empty"><div class="aq-frame"><p class="aq-frame__note">${esc(words)}</p></div></div
|
|
210
|
+
<div class="aq-body aq-body--empty"><div class="aq-frame"><p class="aq-frame__note">${esc(words)}</p></div></div>
|
|
211
|
+
${notAppliedHtml()}`;
|
|
197
212
|
return;
|
|
198
213
|
}
|
|
199
214
|
loadRenders(item);
|
|
@@ -212,7 +227,8 @@
|
|
|
212
227
|
${linesHtml(item)}
|
|
213
228
|
</aside>
|
|
214
229
|
</div>
|
|
215
|
-
${footHtml(item)}
|
|
230
|
+
${footHtml(item)}
|
|
231
|
+
${notAppliedHtml()}`;
|
|
216
232
|
const note = document.getElementById('aq-note');
|
|
217
233
|
if (note) { note.value = noteValue; }
|
|
218
234
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.170",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.170",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.170",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -10078,5 +10078,21 @@
|
|
|
10078
10078
|
"id": "1004777",
|
|
10079
10079
|
"text": "A page tweak that sits too long now tells someone. If a round waits too long for its artist's review, that artist gets a line in Needs attention on Home. If it waits to be applied or to go live, builders who can apply it get t"
|
|
10080
10080
|
}
|
|
10081
|
+
],
|
|
10082
|
+
"1.21.169": [
|
|
10083
|
+
{
|
|
10084
|
+
"id": "1003805",
|
|
10085
|
+
"text": "Projects that test new module versions now actually get them first, and everyone else only gets versions that have passed testing. Turning testing off keeps what you have and moves you to the tested versions from your next upd"
|
|
10086
|
+
}
|
|
10087
|
+
],
|
|
10088
|
+
"1.21.170": [
|
|
10089
|
+
{
|
|
10090
|
+
"id": "1004765",
|
|
10091
|
+
"text": "The Approval queue now also shows pages an artist submitted that nobody has applied yet, under the pages ready to review. Each is marked not applied yet and says how many days it has waited. They have no approve or send back b"
|
|
10092
|
+
},
|
|
10093
|
+
{
|
|
10094
|
+
"id": "1003806",
|
|
10095
|
+
"text": "When a new module version is being tested, how it went for the testing projects now counts toward its quality score — and a handful of friendly testers can no longer outscore hundreds of real ones."
|
|
10096
|
+
}
|
|
10081
10097
|
]
|
|
10082
10098
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// scripts/gds/module-assess-installs.js — the Install
|
|
3
|
-
// module's assessment, worked out from what real installs report (task 1003793,
|
|
2
|
+
// scripts/gds/module-assess-installs.js — the Install, Reliability and Tester
|
|
3
|
+
// feedback parts of a module's assessment, worked out from what real installs report (task 1003793,
|
|
4
4
|
// ADR 0343 D1; reports from core_273, signal rows into core_265).
|
|
5
5
|
//
|
|
6
6
|
// WHY these two parts. Tests and Docs are things an author writes; Install and
|
|
@@ -17,6 +17,21 @@
|
|
|
17
17
|
// reported), over the last ERROR_DAY_WINDOW days.
|
|
18
18
|
// No reports → no_data (ADR 0343 D3: left out of the average, never a zero).
|
|
19
19
|
//
|
|
20
|
+
// Tester feedback (task 1003806, ADR 0344 D5) is the same reports, narrowed to
|
|
21
|
+
// the version's TESTER WINDOW: from the first time it went on the tester channel
|
|
22
|
+
// to the first time it left it (promoted or withdrawn), or now while it is still
|
|
23
|
+
// there. Only tester projects are served a tester version (ADR 0344 D1), so a
|
|
24
|
+
// report inside that window is a tester's. A tester it went well for is one whose
|
|
25
|
+
// install did not fail and who reported no runtime errors in the window.
|
|
26
|
+
// Sample size has to weigh — two friendly testers must not outrank two hundred
|
|
27
|
+
// real ones when a buyer sorts by this part — so the count is blended with
|
|
28
|
+
// PRIOR_TESTERS imaginary testers it went half well for:
|
|
29
|
+
// Tester feedback = round(100 × (went well + PRIOR_TESTERS/2) / (testers + PRIOR_TESTERS)).
|
|
30
|
+
// 3 of 3 → 62, 10 of 10 → 75, 200 of 200 → 98. MIN_HOLDERS applies here too.
|
|
31
|
+
// An install report is one row per holder, re-stamped on each attempt, so a
|
|
32
|
+
// tester who reinstalls after promotion leaves the window; error counts are per
|
|
33
|
+
// day, so they are placed by their day, not by when they were sent.
|
|
34
|
+
//
|
|
20
35
|
// What the reports may contain is fixed by the owner's privacy posture and
|
|
21
36
|
// enforced at intake (src/bongos/module-usage-reports.js): counts and short codes
|
|
22
37
|
// only, and nothing from a holder who switched reporting off.
|
|
@@ -29,6 +44,7 @@
|
|
|
29
44
|
const { ERROR_DAY_WINDOW } = require('../../src/bongos/module-usage-reports');
|
|
30
45
|
|
|
31
46
|
const MIN_HOLDERS = 3;
|
|
47
|
+
const PRIOR_TESTERS = 10;
|
|
32
48
|
|
|
33
49
|
// The not-yet-scored outcomes, shared by both parts. Pure.
|
|
34
50
|
function notYet(holders, what) {
|
|
@@ -67,8 +83,41 @@ function assessReliability(days = []) {
|
|
|
67
83
|
};
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
// Tester feedback from what testers reported in the tester window: `installs` is
|
|
87
|
+
// { holder_kind, holder_ref, ok, error_code } per holder, `days` the holder-days
|
|
88
|
+
// as Reliability takes them. One verdict per tester, blended with the prior. Pure.
|
|
89
|
+
function assessTesterFeedback({ installs = [], days = [] } = {}) {
|
|
90
|
+
const testers = new Map();
|
|
91
|
+
const of = (r) => {
|
|
92
|
+
const k = `${r.holder_kind}:${r.holder_ref}`;
|
|
93
|
+
if (!testers.has(k)) testers.set(k, { failed: false, errors: 0 });
|
|
94
|
+
return testers.get(k);
|
|
95
|
+
};
|
|
96
|
+
for (const r of installs) of(r).failed = !r.ok;
|
|
97
|
+
for (const d of days) of(d).errors += Number(d.error_count);
|
|
98
|
+
const all = [...testers.values()];
|
|
99
|
+
const n = all.length;
|
|
100
|
+
const wentWell = all.filter((t) => !t.failed && t.errors === 0).length;
|
|
101
|
+
const detail = {
|
|
102
|
+
testers: n, went_well: wentWell,
|
|
103
|
+
install_failed: all.filter((t) => t.failed).length,
|
|
104
|
+
with_errors: all.filter((t) => t.errors > 0).length,
|
|
105
|
+
errors: all.reduce((sum, t) => sum + t.errors, 0),
|
|
106
|
+
prior_testers: PRIOR_TESTERS,
|
|
107
|
+
};
|
|
108
|
+
if (n < MIN_HOLDERS) return { ...notYet(n, 'from the tester channel'), detail };
|
|
109
|
+
return {
|
|
110
|
+
outcome: 'scored',
|
|
111
|
+
score: Math.round((100 * (wentWell + PRIOR_TESTERS / 2)) / (n + PRIOR_TESTERS)),
|
|
112
|
+
sample_size: n,
|
|
113
|
+
reason: `it went well for ${wentWell} of ${n} testers (weighed against ${PRIOR_TESTERS} neutral ones, so a small sample cannot score high)`,
|
|
114
|
+
detail,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
70
118
|
// The author's own reports never count. Shared by both reads.
|
|
71
|
-
const
|
|
119
|
+
const notAuthor = (a) => `NOT (${a}.holder_kind = 'builder' AND ${a}.holder_ref = m.author_id::text)`;
|
|
120
|
+
const NOT_AUTHOR = notAuthor('r');
|
|
72
121
|
|
|
73
122
|
async function readReports(versionId, pool) {
|
|
74
123
|
const { rows: latest } = await pool.query(
|
|
@@ -87,26 +136,62 @@ async function readReports(versionId, pool) {
|
|
|
87
136
|
return { latest, days };
|
|
88
137
|
}
|
|
89
138
|
|
|
139
|
+
// The version's tester window, as SQL over its channel history (core_276): `t.start`
|
|
140
|
+
// is when it first went on the tester channel, `t.stop` when it next left it (null
|
|
141
|
+
// while it is still there). A version that was never a tester version has no row.
|
|
142
|
+
const TESTER_WINDOW = `(SELECT s.start,
|
|
143
|
+
(SELECT min(c2.set_at) FROM store_module_version_channels c2
|
|
144
|
+
WHERE c2.version_id = $1 AND c2.channel <> 'tester' AND c2.set_at > s.start) AS stop
|
|
145
|
+
FROM (SELECT min(c1.set_at) AS start FROM store_module_version_channels c1
|
|
146
|
+
WHERE c1.version_id = $1 AND c1.channel = 'tester') s
|
|
147
|
+
WHERE s.start IS NOT NULL) t`;
|
|
148
|
+
|
|
149
|
+
async function readTesterReports(versionId, pool) {
|
|
150
|
+
const { rows: installs } = await pool.query(
|
|
151
|
+
`SELECT ir.holder_kind, ir.holder_ref, ir.ok, ir.error_code
|
|
152
|
+
FROM module_install_reports ir
|
|
153
|
+
JOIN store_module_versions v ON v.id = ir.version_id
|
|
154
|
+
JOIN store_modules m ON m.module_key = v.module_key
|
|
155
|
+
CROSS JOIN ${TESTER_WINDOW}
|
|
156
|
+
WHERE ir.version_id = $1 AND ir.reported_at >= t.start AND (t.stop IS NULL OR ir.reported_at < t.stop)
|
|
157
|
+
AND ${notAuthor('ir')}`, [versionId]);
|
|
158
|
+
const { rows: days } = await pool.query(
|
|
159
|
+
`SELECT ec.holder_kind, ec.holder_ref, ec.error_count
|
|
160
|
+
FROM module_runtime_error_counts ec
|
|
161
|
+
JOIN store_module_versions v ON v.id = ec.version_id
|
|
162
|
+
JOIN store_modules m ON m.module_key = v.module_key
|
|
163
|
+
CROSS JOIN ${TESTER_WINDOW}
|
|
164
|
+
WHERE ec.version_id = $1 AND ec.day >= t.start::date AND (t.stop IS NULL OR ec.day <= t.stop::date)
|
|
165
|
+
AND ${notAuthor('ec')}`, [versionId]);
|
|
166
|
+
return { installs, days };
|
|
167
|
+
}
|
|
168
|
+
|
|
90
169
|
const sameResult = (cur, s) => !!cur && cur.outcome === s.outcome
|
|
91
170
|
&& (cur.score == null ? null : Number(cur.score)) === s.score
|
|
92
171
|
&& Number(cur.sample_size) === s.sample_size;
|
|
93
172
|
|
|
94
|
-
|
|
173
|
+
const PARTS = ['install', 'reliability', 'tester_feedback'];
|
|
174
|
+
|
|
175
|
+
// Re-score Install, Reliability and Tester feedback for one version from its reports, append a
|
|
95
176
|
// signal row for each part whose result moved, then re-compose the overall score
|
|
96
177
|
// (module-assess-score.js). Resolves { ok, written: [part…], score }.
|
|
97
178
|
async function recordInstallSignals(versionId, { db } = {}) {
|
|
98
179
|
const pool = db || require('../../src/bongos/pool').pool;
|
|
99
180
|
const { latest, days } = await readReports(versionId, pool);
|
|
100
|
-
const next = {
|
|
181
|
+
const next = {
|
|
182
|
+
install: assessInstall(latest),
|
|
183
|
+
reliability: assessReliability(days),
|
|
184
|
+
tester_feedback: assessTesterFeedback(await readTesterReports(versionId, pool)),
|
|
185
|
+
};
|
|
101
186
|
|
|
102
187
|
const { rows: current } = await pool.query(
|
|
103
188
|
`SELECT DISTINCT ON (part) part, outcome, score, sample_size
|
|
104
|
-
FROM module_assessment_signals WHERE version_id = $1 AND part IN ('install', 'reliability')
|
|
189
|
+
FROM module_assessment_signals WHERE version_id = $1 AND part IN ('install', 'reliability', 'tester_feedback')
|
|
105
190
|
ORDER BY part, measured_at DESC, id DESC`, [versionId]);
|
|
106
191
|
const byPart = Object.fromEntries(current.map((r) => [r.part, r]));
|
|
107
192
|
|
|
108
193
|
const written = [];
|
|
109
|
-
for (const part of
|
|
194
|
+
for (const part of PARTS) {
|
|
110
195
|
const s = next[part];
|
|
111
196
|
if (sameResult(byPart[part], s)) continue;
|
|
112
197
|
await pool.query(
|
|
@@ -128,7 +213,7 @@ async function main(argv) {
|
|
|
128
213
|
return 2;
|
|
129
214
|
}
|
|
130
215
|
const res = await recordInstallSignals(id);
|
|
131
|
-
for (const part of
|
|
216
|
+
for (const part of PARTS) {
|
|
132
217
|
const s = res.signals[part];
|
|
133
218
|
console.log(`${part}: ${s.outcome}${s.score != null ? ` ${s.score}/100` : ''} — ${s.reason}${res.written.includes(part) ? '' : ' (unchanged)'}`);
|
|
134
219
|
}
|
|
@@ -140,4 +225,4 @@ if (require.main === module) {
|
|
|
140
225
|
main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(e.stack || e.message); process.exitCode = 1; });
|
|
141
226
|
}
|
|
142
227
|
|
|
143
|
-
module.exports = { MIN_HOLDERS, assessInstall, assessReliability, recordInstallSignals };
|
|
228
|
+
module.exports = { MIN_HOLDERS, PRIOR_TESTERS, assessInstall, assessReliability, assessTesterFeedback, recordInstallSignals };
|