@bongos/core 1.21.169 → 1.21.171
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 +30 -30
- package/docs/adr/0344-a-tester-is-a-project-that-opts-in-once.md +12 -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 +1 -0
- package/docs/page-readings.json +16 -16
- 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/modules/hall-ui/public/brand-apply.js +13 -6
- package/modules/hall-ui/public/sky-data.js +5 -4
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/lifecycle/sky.js +8 -1
- 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/module-api.js +1 -1
- package/tests/goal_map_page.mjs +5 -5
- package/tests/hall_approval_queue.mjs +35 -0
- package/tests/hall_sky.mjs +16 -4
- package/tests/module_assess_installs.mjs +72 -6
- package/tests/reward_mode_surfacing.mjs +16 -3
- package/tests/sky_read.mjs +13 -2
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.
|
package/docs/page-readings.json
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"surface": "builders",
|
|
51
51
|
"title": "Blockers",
|
|
52
52
|
"states_read": ["default"],
|
|
53
|
-
"files_hash": "
|
|
53
|
+
"files_hash": "f5bf3c2244e3fbdd",
|
|
54
54
|
"reading_hash": "2b3fd98b7ba1bff6",
|
|
55
55
|
"counts": {"lines":28,"placed":12,"shared":8,"unplaced":8},
|
|
56
56
|
"omitted": {"data":9},
|
|
@@ -1141,7 +1141,7 @@
|
|
|
1141
1141
|
"surface": "builders",
|
|
1142
1142
|
"title": "The sky",
|
|
1143
1143
|
"states_read": ["tour","sky","wells","ledger","full","full-back","out"],
|
|
1144
|
-
"files_hash": "
|
|
1144
|
+
"files_hash": "5f2c2f22c2fc9dc6",
|
|
1145
1145
|
"reading_hash": "b83eb8c92a23fd3c",
|
|
1146
1146
|
"counts": {"lines":47,"placed":23,"shared":7,"unplaced":17},
|
|
1147
1147
|
"omitted": {"data":8},
|
|
@@ -1201,7 +1201,7 @@
|
|
|
1201
1201
|
"surface": "builders",
|
|
1202
1202
|
"title": "Goals",
|
|
1203
1203
|
"states_read": ["list","filters","cards","detail","dependencies","out","uat","uat-panel","author"],
|
|
1204
|
-
"files_hash": "
|
|
1204
|
+
"files_hash": "4f6f4eb5e9f00412",
|
|
1205
1205
|
"reading_hash": "7aa1a6e1946dc077",
|
|
1206
1206
|
"counts": {"lines":156,"placed":52,"shared":8,"unplaced":96},
|
|
1207
1207
|
"omitted": {"data":268},
|
|
@@ -1326,7 +1326,7 @@
|
|
|
1326
1326
|
{"key":"L0117","section":"Goals","text":"Version","placement":"unplaced"},
|
|
1327
1327
|
{"key":"L0118","section":"Goals","text":"— any —","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":173,"string_id":"729d4ce22b99","occurrences":2},
|
|
1328
1328
|
{"key":"L0119","section":"Goals","text":"Status","placement":"unplaced"},
|
|
1329
|
-
{"key":"L0120","section":"Goals","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":
|
|
1329
|
+
{"key":"L0120","section":"Goals","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":166,"string_id":"9a803563c948","occurrences":16},
|
|
1330
1330
|
{"key":"L0121","section":"Goals","text":"Sort","placement":"unplaced"},
|
|
1331
1331
|
{"key":"L0122","section":"Goals","text":"priority (version order)","placement":"unplaced"},
|
|
1332
1332
|
{"key":"L0123","section":"Goals","text":"newest first","placement":"unplaced"},
|
|
@@ -1439,7 +1439,7 @@
|
|
|
1439
1439
|
"surface": "builders",
|
|
1440
1440
|
"title": "Builders' Hall",
|
|
1441
1441
|
"states_read": ["home","standing","standing-karma","out","founding","genesis","decide","demo"],
|
|
1442
|
-
"files_hash": "
|
|
1442
|
+
"files_hash": "e32eb071e46bec38",
|
|
1443
1443
|
"reading_hash": "42dbab4f3948fe66",
|
|
1444
1444
|
"counts": {"lines":117,"placed":63,"shared":10,"unplaced":44},
|
|
1445
1445
|
"omitted": {"data":101},
|
|
@@ -1569,7 +1569,7 @@
|
|
|
1569
1569
|
"surface": "builders",
|
|
1570
1570
|
"title": "Idea",
|
|
1571
1571
|
"states_read": ["record","missing"],
|
|
1572
|
-
"files_hash": "
|
|
1572
|
+
"files_hash": "7456ea53572161d4",
|
|
1573
1573
|
"reading_hash": "c9f9a84ab844c704",
|
|
1574
1574
|
"counts": {"lines":52,"placed":23,"shared":9,"unplaced":20},
|
|
1575
1575
|
"omitted": {"data":18},
|
|
@@ -1609,7 +1609,7 @@
|
|
|
1609
1609
|
{"key":"L0032","section":"Everything that fell out of it","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","state":"missing"},
|
|
1610
1610
|
{"key":"L0033","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc","state":"missing","occurrences":2},
|
|
1611
1611
|
{"key":"L0034","section":"Everything that fell out of it","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":674,"string_id":"fd1903586e8e","state":"missing"},
|
|
1612
|
-
{"key":"L0035","section":"12A changelog every builder can read","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":
|
|
1612
|
+
{"key":"L0035","section":"12A changelog every builder can read","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":166,"string_id":"9a803563c948"},
|
|
1613
1613
|
{"key":"L0036","section":"12A changelog every builder can read","text":"had the thought · {…}","placement":"unplaced"},
|
|
1614
1614
|
{"key":"L0037","section":"12A changelog every builder can read","text":"{…}/100 complete","placement":"placed","file":"modules/hall-ui/public/idea.js","line":87,"string_id":"c43050b62297"},
|
|
1615
1615
|
{"key":"L0038","section":"12A changelog every builder can read","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":4},
|
|
@@ -1719,7 +1719,7 @@
|
|
|
1719
1719
|
"surface": "builders",
|
|
1720
1720
|
"title": "Modules",
|
|
1721
1721
|
"states_read": ["browse","search-empty","lens","details","out"],
|
|
1722
|
-
"files_hash": "
|
|
1722
|
+
"files_hash": "6dc5460706966349",
|
|
1723
1723
|
"reading_hash": "a17f501b72321b7b",
|
|
1724
1724
|
"counts": {"lines":62,"placed":29,"shared":8,"unplaced":25},
|
|
1725
1725
|
"omitted": {"data":106},
|
|
@@ -2325,7 +2325,7 @@
|
|
|
2325
2325
|
"surface": "builders",
|
|
2326
2326
|
"title": "Profile",
|
|
2327
2327
|
"states_read": ["own","works","activity","activity-artist","activity-ideator","other","artist","ideator","out"],
|
|
2328
|
-
"files_hash": "
|
|
2328
|
+
"files_hash": "82a35b34a5115325",
|
|
2329
2329
|
"reading_hash": "8088d60a4e93bb91",
|
|
2330
2330
|
"counts": {"lines":196,"placed":70,"shared":8,"unplaced":118},
|
|
2331
2331
|
"omitted": {"data":197},
|
|
@@ -2721,7 +2721,7 @@
|
|
|
2721
2721
|
"surface": "builders",
|
|
2722
2722
|
"title": "Builders",
|
|
2723
2723
|
"states_read": ["roster","artists","search","out"],
|
|
2724
|
-
"files_hash": "
|
|
2724
|
+
"files_hash": "88e2f7aca1994346",
|
|
2725
2725
|
"reading_hash": "5c7e507e9600e59c",
|
|
2726
2726
|
"counts": {"lines":47,"placed":13,"shared":8,"unplaced":26},
|
|
2727
2727
|
"omitted": {"data":60},
|
|
@@ -2781,7 +2781,7 @@
|
|
|
2781
2781
|
"surface": "builders",
|
|
2782
2782
|
"title": "Sessions",
|
|
2783
2783
|
"states_read": ["ledger","out"],
|
|
2784
|
-
"files_hash": "
|
|
2784
|
+
"files_hash": "cd14ebd4d35e16bf",
|
|
2785
2785
|
"reading_hash": "5c18aaed7f2e8e0f",
|
|
2786
2786
|
"counts": {"lines":331,"placed":21,"shared":8,"unplaced":302},
|
|
2787
2787
|
"omitted": {"data":303},
|
|
@@ -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},
|
|
@@ -4021,7 +4021,7 @@
|
|
|
4021
4021
|
"surface": "builders",
|
|
4022
4022
|
"title": "Task",
|
|
4023
4023
|
"states_read": ["record","from-goal","missing"],
|
|
4024
|
-
"files_hash": "
|
|
4024
|
+
"files_hash": "5535b9790b56985e",
|
|
4025
4025
|
"reading_hash": "3e80d092af1d7e2c",
|
|
4026
4026
|
"counts": {"lines":132,"placed":35,"shared":10,"unplaced":87},
|
|
4027
4027
|
"omitted": {"data":75},
|
|
@@ -4166,7 +4166,7 @@
|
|
|
4166
4166
|
"surface": "builders",
|
|
4167
4167
|
"title": "Your thinking",
|
|
4168
4168
|
"states_read": ["tour","sky","list","simple","full-idea","full","full-list","full-back","out"],
|
|
4169
|
-
"files_hash": "
|
|
4169
|
+
"files_hash": "a7b04635b758d676",
|
|
4170
4170
|
"reading_hash": "532805c03c3a8a4c",
|
|
4171
4171
|
"counts": {"lines":48,"placed":23,"shared":7,"unplaced":18},
|
|
4172
4172
|
"omitted": {"data":8},
|
|
@@ -4263,7 +4263,7 @@
|
|
|
4263
4263
|
"surface": "builders",
|
|
4264
4264
|
"title": "Watch",
|
|
4265
4265
|
"states_read": ["board","out","operations","security"],
|
|
4266
|
-
"files_hash": "
|
|
4266
|
+
"files_hash": "3980ad82a03bc297",
|
|
4267
4267
|
"reading_hash": "d698ac62c904aa65",
|
|
4268
4268
|
"counts": {"lines":88,"placed":38,"shared":8,"unplaced":42},
|
|
4269
4269
|
"omitted": {"data":166},
|
|
@@ -4364,7 +4364,7 @@
|
|
|
4364
4364
|
"surface": "builders",
|
|
4365
4365
|
"title": "Tasks",
|
|
4366
4366
|
"states_read": ["ready","in-progress","backlog","completed","filters-open","out","founding"],
|
|
4367
|
-
"files_hash": "
|
|
4367
|
+
"files_hash": "1dcb08711f496752",
|
|
4368
4368
|
"reading_hash": "a425c7a090d8bf03",
|
|
4369
4369
|
"counts": {"lines":125,"placed":54,"shared":8,"unplaced":63},
|
|
4370
4370
|
"omitted": {"data":316},
|
|
@@ -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
|
}
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
// estimate with a "<Currency> Est." label; settled → the actual ledger payout
|
|
92
92
|
// with a plain "<Currency>" label.
|
|
93
93
|
//
|
|
94
|
-
// task 1002469 — on a cost-plus-only instance that second case
|
|
94
|
+
// task 1002469 (superseded by task 1004781) — on a cost-plus-only instance that second case was a LIE. The
|
|
95
95
|
// per-task estimate is never paid, so credits_awarded is 0 on every shipped
|
|
96
96
|
// task, taskCreditValue's zero-fallback hands back the estimate, and the plain
|
|
97
97
|
// label then presents that estimate as the actual payout. So the "Est."
|
|
@@ -105,18 +105,25 @@
|
|
|
105
105
|
function taskCreditValue(task) {
|
|
106
106
|
if (!task) return null;
|
|
107
107
|
if (task.status === 'abandoned' || task.merged_into_task_id != null) return 0;
|
|
108
|
+
// task 1004781 — a settled task is worth what the ledger PAID for it and
|
|
109
|
+
// nothing else. It used to fall back to the estimate when the ledger held
|
|
110
|
+
// 0, and on a cost-plus-only instance that is every shipped task, so goal
|
|
111
|
+
// totals summed guesses as money (a goal read 3,373 for a builder paid
|
|
112
|
+
// 776). A settled 0 is the truth: the task itself paid nothing. Only
|
|
113
|
+
// unsettled work carries the estimate, and taskCreditLabel marks it "Est.".
|
|
114
|
+
// The decision the task asked for: estimates are KEPT, for live work only —
|
|
115
|
+
// they still rank claimable work (task-lib's best-value sort) and tell a
|
|
116
|
+
// builder roughly how big a task is — but they never reach a total.
|
|
108
117
|
if (taskSettled(task)) {
|
|
109
118
|
var awarded = Number(task.credits_awarded);
|
|
110
|
-
|
|
111
|
-
// ledger record (0 — legacy pre-ledger tasks, or reward-exempt), fall back
|
|
112
|
-
// to the estimate rather than showing a misleading "0".
|
|
113
|
-
if (isFinite(awarded) && awarded > 0) return awarded;
|
|
119
|
+
return isFinite(awarded) && awarded > 0 ? awarded : 0;
|
|
114
120
|
}
|
|
115
121
|
return task.credits_reward;
|
|
116
122
|
}
|
|
117
123
|
function taskCreditLabel(task, opts) {
|
|
124
|
+
// A settled figure is now always the actual payout (see taskCreditValue),
|
|
125
|
+
// so it takes the plain label on every reward mode; only an estimate is "Est.".
|
|
118
126
|
var base = currencyLabel(opts);
|
|
119
|
-
if (!perTaskCredit()) return base + ' Est.';
|
|
120
127
|
return taskSettled(task) ? base : base + ' Est.';
|
|
121
128
|
}
|
|
122
129
|
|
|
@@ -142,15 +142,16 @@
|
|
|
142
142
|
// The prototype read a snapshot (docs/design/mocks/sky/snapshot.js). Each
|
|
143
143
|
// adaptor maps one real read onto the same field names, so the world below
|
|
144
144
|
// is the prototype's world line for line.
|
|
145
|
+
const SETTLED = new Set(['confirmed', 'shipped']);
|
|
145
146
|
function goalFromSky(g) {
|
|
146
147
|
return {
|
|
147
148
|
id: String(g.goal_id), title: g.title || 'A private goal', subtitle: g.subtitle || null, version: g.version_id, status: g.status,
|
|
148
149
|
taskTotal: Number(g.task_total) || 0, taskDone: Number(g.task_done) || 0, createdAt: g.created_at || null,
|
|
149
150
|
crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true, uat: c.uat_state || null, uatLabel: uatLabel(c.uat_state), backendOnly: c.backend_only === true })),
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: t.status === 'abandoned' || t.credits_reward == null ?
|
|
151
|
+
// `credits` is what the ledger PAID — settled work only (task 1004781) — so a well's mass and a ring's weight are
|
|
152
|
+
// money paid. Live work carries its guess as `estimate`, shown "Est." and never added to a total. Abandoned (or
|
|
153
|
+
// merged-away, which is always abandoned — CHECK-constrained) work has neither (task 1004782).
|
|
154
|
+
tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: SETTLED.has(t.status) ? Number(t.credits_awarded) || 0 : 0, estimate: SETTLED.has(t.status) || t.status === 'abandoned' || t.credits_reward == null ? null : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
157
|
const taskRow = (t) => ({ id: String(t.task_id ?? t.id), title: t.title || 'a task', status: t.status, goalId: t.goal_id != null ? String(t.goal_id) : null, origin: t.origin || null });
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
<div class="facts">
|
|
99
99
|
${g ? fact('well', esc(g.title)) : ''}
|
|
100
100
|
${crit ? fact('gates the ring', esc(plain(crit.text).slice(0, 90))) : g ? fact('gates the ring', 'none — outer orbit') : ''}
|
|
101
|
-
${t.credits != null ? fact('credits', String(t.credits), true) : ''}
|
|
101
|
+
${t.estimate != null ? fact('credits', `${t.estimate} <span class="lbl">Est.</span>`, true) : t.credits != null ? fact('credits paid', String(t.credits), true) : ''}
|
|
102
102
|
${t.createdAt ? fact('born', esc(t.createdAt)) : ''}
|
|
103
103
|
${t.sourceRef ? fact('born of a thought', `<a href="${HALL}/idea/${esc(t.sourceRef)}" target="_blank" rel="noopener">${esc(ideaTitle(t.sourceRef))}</a>`) : ''}
|
|
104
104
|
</div>
|
package/modules/lifecycle/sky.js
CHANGED
|
@@ -44,6 +44,7 @@ const goalGraph = require('./goal-graph');
|
|
|
44
44
|
const goalCategory = require('./goal-category');
|
|
45
45
|
const { isPendingReview, uatState, uatReadColumnsSql } = require('./done-when');
|
|
46
46
|
const { TERMINAL_STATUSES } = require('./task-status');
|
|
47
|
+
const { CREDITS_AWARDED_SUBQUERY } = require('./db-tasks');
|
|
47
48
|
|
|
48
49
|
const SKY_CACHE_TTL_MS = 60 * 1000;
|
|
49
50
|
|
|
@@ -92,9 +93,12 @@ async function skyRows({ versionId = null, viewerId = null, viewerIsArchon = fal
|
|
|
92
93
|
// Compact by construction: no description, no value summary, no notes.
|
|
93
94
|
// created_at is truncated to the DAY in SQL so the payload carries ten
|
|
94
95
|
// characters per task, not twenty-seven, and the clock draws by day anyway.
|
|
96
|
+
// credits_awarded (task 1004781) is the shared settled-only payout subquery; its
|
|
97
|
+
// per-row lookup rides idx_credit_log_task (core_189), never a scan of the log.
|
|
95
98
|
`SELECT t.id::text AS id, t.goal_id::text AS goal_id, t.title, t.status,
|
|
96
99
|
t.credits_reward, t.created_at::date::text AS created_on,
|
|
97
|
-
CASE WHEN t.source_ref ~ '^idea:[0-9]+$' THEN substring(t.source_ref from 6) END AS idea_id
|
|
100
|
+
CASE WHEN t.source_ref ~ '^idea:[0-9]+$' THEN substring(t.source_ref from 6) END AS idea_id,
|
|
101
|
+
${CREDITS_AWARDED_SUBQUERY} AS credits_awarded
|
|
98
102
|
FROM tasks t
|
|
99
103
|
JOIN goals g ON g.id = t.goal_id
|
|
100
104
|
WHERE ($1::text IS NULL OR g.version_id = $1)
|
|
@@ -189,6 +193,9 @@ function shapeSky({ goals = [], edges = [], extras = [], criteria = [], tasks =
|
|
|
189
193
|
title: visible ? t.title : null,
|
|
190
194
|
status: t.status,
|
|
191
195
|
credits_reward: t.credits_reward == null ? null : Number(t.credits_reward),
|
|
196
|
+
// task 1004781 — what the ledger actually paid (0 until settled), so the
|
|
197
|
+
// sky sizes a well by money paid, never by the estimate.
|
|
198
|
+
credits_awarded: t.credits_awarded == null ? 0 : Number(t.credits_awarded),
|
|
192
199
|
created_on: t.created_on || null,
|
|
193
200
|
idea_id: t.idea_id != null ? String(t.idea_id) : null,
|
|
194
201
|
});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.171",
|
|
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.171",
|
|
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.171",
|
|
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
|
@@ -10084,5 +10084,21 @@
|
|
|
10084
10084
|
"id": "1003805",
|
|
10085
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
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
|
+
}
|
|
10097
|
+
],
|
|
10098
|
+
"1.21.171": [
|
|
10099
|
+
{
|
|
10100
|
+
"id": "1004781",
|
|
10101
|
+
"text": "Credit figures in the hall now show money that was actually paid. A finished task shows its real payout instead of the old guess, so goal totals, member stats and the goal map stop adding up estimates as if they were earnings."
|
|
10102
|
+
}
|
|
10087
10103
|
]
|
|
10088
10104
|
}
|
|
@@ -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 };
|