@bongos/core 1.19.1063 → 1.19.1065
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 +136 -56
- package/.claude/skills/grade-recover/SKILL.md +1 -0
- package/.claude/skills/tweak/SKILL.md +70 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +21 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/api/openapi.json +492 -4
- package/docs/api-reference.md +9 -3
- package/docs/copy-inventory.md +60 -41
- package/docs/copy-registry.json +228 -47
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -2
- package/docs/page-readings.json +45 -22
- package/modules/copy-desk/page-decisions.js +71 -0
- package/modules/copy-desk/page-status.js +96 -0
- package/modules/copy-desk/pages.js +30 -0
- package/modules/copy-desk/routes/copy-desk.js +153 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +40 -4
- package/modules/hall-ui/public/approval-queue-lib.js +156 -0
- package/modules/hall-ui/public/approval-queue.css +286 -0
- package/modules/hall-ui/public/approval-queue.js +338 -0
- package/modules/hall-ui/public/studio.css +33 -0
- package/modules/hall-ui/public/studio.html +18 -1
- package/modules/hall-ui/public/studio.js +30 -0
- package/modules/hall-ui/public/studio.states.json +198 -0
- package/modules/hall-ui/records/approval-queue.md +32 -0
- package/modules/lifecycle/db-grade.js +96 -58
- package/modules/lifecycle/github-pr-close.js +41 -0
- package/modules/lifecycle/github-push.js +5 -2
- package/modules/lifecycle/lifecycle.js +12 -0
- package/modules/lifecycle/migrations/lifecycle_013_task_visual_slots.sql +52 -0
- package/modules/lifecycle/page-tweak-approve.js +242 -0
- package/modules/lifecycle/page-tweak-hold.js +74 -0
- package/modules/lifecycle/page-tweak-reads.js +33 -1
- package/modules/lifecycle/routes/gate-approvals.js +17 -2
- package/modules/lifecycle/routes/tasks.js +4 -2
- package/modules/lifecycle/routes/visuals.js +82 -0
- package/modules/lifecycle/task-visual-db.js +40 -1
- package/modules/lifecycle/task-visuals.js +24 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/copy-apply.js +277 -1
- package/scripts/gds/page-reader.js +2 -0
- package/scripts/gds/ship-finish.js +27 -2
- package/scripts/gds/ship-flow.js +17 -1
- package/scripts/gds/ship-land.js +30 -7
- package/scripts/gds/ship-merge.js +43 -15
- package/scripts/gds/strand-watch.js +41 -1
- package/scripts/gds/task.js +23 -5
- package/scripts/gds/tweak-renders.js +200 -0
- package/scripts/hall-preview/server.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_approve.mjs +307 -0
- package/tests/fixtures/page-ledger.mjs +41 -1
- package/tests/gate_approvals.mjs +93 -0
- package/tests/hall_approval_queue.mjs +327 -0
- package/tests/hall_audit.mjs +4 -0
- package/tests/page_tweak_hold.mjs +265 -0
- package/tests/publish_status_branch.mjs +23 -0
- package/tests/task_visual_slots.mjs +161 -0
- package/tests/tweak_batch_apply.mjs +265 -0
package/docs/page-inventory.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"pages": 38,
|
|
14
14
|
"surfaces": 3,
|
|
15
15
|
"with_states": 31,
|
|
16
|
-
"states":
|
|
16
|
+
"states": 161
|
|
17
17
|
},
|
|
18
18
|
"surfaces": [
|
|
19
19
|
{
|
|
@@ -842,11 +842,14 @@
|
|
|
842
842
|
"modules/hall-ui/public/studio.html",
|
|
843
843
|
"modules/hall-ui/public/style.css",
|
|
844
844
|
"modules/hall-ui/public/studio.css",
|
|
845
|
+
"modules/hall-ui/public/approval-queue.css",
|
|
845
846
|
"modules/hall-ui/public/shell.js",
|
|
846
847
|
"modules/hall-ui/public/palette.js",
|
|
847
848
|
"modules/hall-ui/public/dom-utils.js",
|
|
848
849
|
"modules/hall-ui/public/hall-visual.js",
|
|
849
850
|
"modules/hall-ui/public/hall-kit.js",
|
|
851
|
+
"modules/hall-ui/public/approval-queue-lib.js",
|
|
852
|
+
"modules/hall-ui/public/approval-queue.js",
|
|
850
853
|
"modules/hall-ui/public/studio.js"
|
|
851
854
|
],
|
|
852
855
|
"states_files": [
|
|
@@ -856,7 +859,13 @@
|
|
|
856
859
|
"focus",
|
|
857
860
|
"out",
|
|
858
861
|
"full",
|
|
859
|
-
"full-back"
|
|
862
|
+
"full-back",
|
|
863
|
+
"approvals",
|
|
864
|
+
"approvals-before",
|
|
865
|
+
"approvals-phone",
|
|
866
|
+
"approvals-note",
|
|
867
|
+
"approved",
|
|
868
|
+
"sent-back"
|
|
860
869
|
]
|
|
861
870
|
},
|
|
862
871
|
{
|
package/docs/page-readings.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"adr": "0341 D3",
|
|
6
6
|
"reread": "node scripts/gds/page-reader.js --stale",
|
|
7
7
|
"how": ["Generated by node scripts/gds/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
|
|
8
|
-
"counts": {"pages":38,"lines":
|
|
8
|
+
"counts": {"pages":38,"lines":3659,"placed":1139,"shared":247,"unplaced":2273,"omitted_data":2015},
|
|
9
9
|
"pages": [
|
|
10
10
|
{
|
|
11
11
|
"id": "builders:atlas",
|
|
@@ -2650,35 +2650,58 @@
|
|
|
2650
2650
|
"id": "builders:studio",
|
|
2651
2651
|
"surface": "builders",
|
|
2652
2652
|
"title": "Studio",
|
|
2653
|
-
"states_read": ["focus","out","full","full-back"],
|
|
2654
|
-
"files_hash": "
|
|
2655
|
-
"reading_hash": "
|
|
2656
|
-
"counts": {"lines":
|
|
2657
|
-
"omitted": {"data":
|
|
2653
|
+
"states_read": ["focus","out","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back"],
|
|
2654
|
+
"files_hash": "e1041dfa74aca8dd",
|
|
2655
|
+
"reading_hash": "e52bb7163191d85a",
|
|
2656
|
+
"counts": {"lines":45,"placed":23,"shared":5,"unplaced":17},
|
|
2657
|
+
"omitted": {"data":7},
|
|
2658
2658
|
"notes": [],
|
|
2659
2659
|
"lines": [
|
|
2660
2660
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":776,"string_id":"87cee78ca134"},
|
|
2661
2661
|
{"key":"L0002","section":"top bar","text":"Studio","placement":"shared"},
|
|
2662
2662
|
{"key":"L0003","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":496,"string_id":"eba610d25523"},
|
|
2663
2663
|
{"key":"L0004","section":"top bar","text":"Ctrl K","placement":"shared"},
|
|
2664
|
-
{"key":"L0005","section":"page top","text":"artist mode","placement":"placed","file":"modules/hall-ui/public/studio.html","line":
|
|
2664
|
+
{"key":"L0005","section":"page top","text":"artist mode","placement":"placed","file":"modules/hall-ui/public/studio.html","line":43,"string_id":"520853131591"},
|
|
2665
2665
|
{"key":"L0006","section":"Good morning, {…}","text":"Good morning, {…}","placement":"unplaced"},
|
|
2666
2666
|
{"key":"L0007","section":"Good morning, {…}","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
|
|
2667
|
-
{"key":"L0008","section":"Good morning, {…}","text":"
|
|
2668
|
-
{"key":"L0009","section":"
|
|
2669
|
-
{"key":"L0010","section":"
|
|
2670
|
-
{"key":"L0011","section":"
|
|
2671
|
-
{"key":"L0012","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2672
|
-
{"key":"L0013","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2673
|
-
{"key":"L0014","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2674
|
-
{"key":"L0015","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2675
|
-
{"key":"L0016","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2676
|
-
{"key":"L0017","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2677
|
-
{"key":"L0018","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2678
|
-
{"key":"L0019","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2679
|
-
{"key":"L0020","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2680
|
-
{"key":"L0021","section":"The copy and visuals that {…} in task #1899","text":"
|
|
2681
|
-
{"key":"L0022","section":"
|
|
2667
|
+
{"key":"L0008","section":"Good morning, {…}","text":"Approval queue","placement":"placed","file":"modules/hall-ui/public/studio.html","line":55,"string_id":"20c98a4b630f"},
|
|
2668
|
+
{"key":"L0009","section":"Good morning, {…}","text":"1 page applied, waiting for you","placement":"unplaced","state":"approved"},
|
|
2669
|
+
{"key":"L0010","section":"Good morning, {…}","text":"2 pages applied, waiting for you","placement":"unplaced"},
|
|
2670
|
+
{"key":"L0011","section":"Good morning, {…}","text":"to tend now · {…}","placement":"placed","file":"modules/hall-ui/public/studio.js","line":215,"string_id":"1b88ce51fd84"},
|
|
2671
|
+
{"key":"L0012","section":"The copy and visuals that {…} in task #1899","text":"The copy and visuals that {…} in task #1899","placement":"unplaced"},
|
|
2672
|
+
{"key":"L0013","section":"The copy and visuals that {…} in task #1899","text":"It shipped and it is live. Say what you see — your review coaches the grader and never blocks what shipped.","placement":"placed","file":"modules/hall-ui/public/studio.js","line":213,"string_id":"a28aa368453f"},
|
|
2673
|
+
{"key":"L0014","section":"The copy and visuals that {…} in task #1899","text":"What you saw","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"1d5bfd09068c"},
|
|
2674
|
+
{"key":"L0015","section":"The copy and visuals that {…} in task #1899","text":"— your words, in your voice. Needed for a note or a follow-up.","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"d3afa6d6dd9e"},
|
|
2675
|
+
{"key":"L0016","section":"The copy and visuals that {…} in task #1899","text":"The headline lands, but the sub-line reads like a brochure.","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"027e9420daa2","attr":"placeholder"},
|
|
2676
|
+
{"key":"L0017","section":"The copy and visuals that {…} in task #1899","text":"Looks good","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"73677b6839d7"},
|
|
2677
|
+
{"key":"L0018","section":"The copy and visuals that {…} in task #1899","text":"Note it","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"73c72974f4f9"},
|
|
2678
|
+
{"key":"L0019","section":"The copy and visuals that {…} in task #1899","text":"Needs work","placement":"placed","file":"modules/hall-ui/public/studio.js","line":173,"string_id":"3970d299c95a"},
|
|
2679
|
+
{"key":"L0020","section":"The copy and visuals that {…} in task #1899","text":"Open this review","placement":"placed","file":"modules/hall-ui/public/studio.js","line":215,"string_id":"1cb80282de09"},
|
|
2680
|
+
{"key":"L0021","section":"The copy and visuals that {…} in task #1899","text":"Let it be for now","placement":"placed","file":"modules/hall-ui/public/studio.js","line":215,"string_id":"d6a78b9e5368"},
|
|
2681
|
+
{"key":"L0022","section":"The copy and visuals that {…} in task #1899","text":"and {…} more waiting{…}","placement":"placed","file":"modules/hall-ui/public/studio.js","line":288,"string_id":"d86265f99668"},
|
|
2682
|
+
{"key":"L0023","section":"The copy and visuals that {…} in task #1899","text":"The copy desk — every flagged string","placement":"placed","file":"modules/hall-ui/public/studio.html","line":76,"string_id":"2dc061096c48"},
|
|
2683
|
+
{"key":"L0024","section":"The copy and visuals that {…} in task #1899","text":"The task board — all work","placement":"placed","file":"modules/hall-ui/public/studio.html","line":77,"string_id":"599fd0e82d1c"},
|
|
2684
|
+
{"key":"L0025","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":473,"string_id":"1883ad7f07fc"},
|
|
2685
|
+
{"key":"L0026","section":"The copy and visuals that {…} in task #1899","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"approvals"},
|
|
2686
|
+
{"key":"L0027","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"Home","placement":"unplaced","state":"approvals"},
|
|
2687
|
+
{"key":"L0028","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"· {…} · 3 lines rewritten, applied · 1 not applied","placement":"unplaced","state":"approvals"},
|
|
2688
|
+
{"key":"L0029","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"before","placement":"unplaced","state":"approvals"},
|
|
2689
|
+
{"key":"L0030","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"after","placement":"unplaced","state":"approvals"},
|
|
2690
|
+
{"key":"L0031","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"close","placement":"unplaced","state":"approvals"},
|
|
2691
|
+
{"key":"L0032","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"desktop","placement":"unplaced","state":"approvals"},
|
|
2692
|
+
{"key":"L0033","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"phone","placement":"unplaced","state":"approvals"},
|
|
2693
|
+
{"key":"L0034","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"light","placement":"unplaced","state":"approvals"},
|
|
2694
|
+
{"key":"L0035","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"dark","placement":"unplaced","state":"approvals"},
|
|
2695
|
+
{"key":"L0036","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5","state":"approvals-before","occurrences":12},
|
|
2696
|
+
{"key":"L0037","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"not applied: the page no longer shows it","placement":"unplaced","state":"approvals"},
|
|
2697
|
+
{"key":"L0038","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"Sent back with your note. It comes back to the queue once it is re-applied.","placement":"unplaced","state":"sent-back"},
|
|
2698
|
+
{"key":"L0039","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"Approved. It ships now, and Home reads text tweaked 3x. Next in the queue: Contact.","placement":"unplaced","state":"approved"},
|
|
2699
|
+
{"key":"L0040","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"open {…}","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":156,"string_id":"9a803563c948","state":"approved"},
|
|
2700
|
+
{"key":"L0041","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"approve and ship","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":165,"string_id":"ac37199ccdbb","state":"approvals"},
|
|
2701
|
+
{"key":"L0042","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"The hero line is too long on a phone.","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":167,"string_id":"f08587a38a2a","attr":"placeholder","state":"approvals-note"},
|
|
2702
|
+
{"key":"L0043","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"send","placement":"unplaced","state":"approvals-note"},
|
|
2703
|
+
{"key":"L0044","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"send it back","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":172,"string_id":"92775a3d8d0d","state":"approvals"},
|
|
2704
|
+
{"key":"L0045","section":"Home · {…} · 3 lines rewritten, applied · 1 not applied","text":"highlighted: the words you changed","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":161,"string_id":"c2267f78f931","state":"approvals"}
|
|
2682
2705
|
]
|
|
2683
2706
|
},
|
|
2684
2707
|
{
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// modules/copy-desk/page-decisions.js — the Approval queue's route helpers (task
|
|
2
|
+
// 1004323 / BV2.TW12, ADR 0341 D7): which round a decision is about, the port's
|
|
3
|
+
// refusals as the panel shows them, and the next page after a decision. The two
|
|
4
|
+
// routes that use them are in routes/copy-desk.js (POST /copy-desk/pages/:pageId/
|
|
5
|
+
// approve and /send-back); these sit apart only because that file is at the
|
|
6
|
+
// 1,500-line ratchet. They answer through `res` and read through the lifecycle
|
|
7
|
+
// port the route hands them; nothing here writes a row, a file or a string.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const pageStatus = require('./page-status');
|
|
12
|
+
|
|
13
|
+
// The page's round that is waiting for its artist, or the named refusal.
|
|
14
|
+
// `task_id` in the body, when sent, pins the round the artist looked at: a
|
|
15
|
+
// round sent back and re-applied since is another set of renders.
|
|
16
|
+
async function waitingRoundOrFail(res, lifecycle, pageId, body, openRoundOf) {
|
|
17
|
+
const rounds = await lifecycle.listPageTweakTasks({ pageId });
|
|
18
|
+
const round = openRoundOf(rounds, pageId);
|
|
19
|
+
const state = round ? pageStatus.roundState(round) : null;
|
|
20
|
+
if (!round || state !== 'waiting_for_artist') {
|
|
21
|
+
res.fail('not_waiting', 409, {
|
|
22
|
+
page_id: pageId,
|
|
23
|
+
round: round ? { task_id: String(round.id), status: round.status, state } : null,
|
|
24
|
+
what_this_means: 'This page is not waiting for approval. Only a page /tweak has applied, and whose grade passed, can be approved or sent back.',
|
|
25
|
+
});
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (body.task_id != null && String(body.task_id) !== String(round.id)) {
|
|
29
|
+
res.fail('round_moved', 409, {
|
|
30
|
+
page_id: pageId,
|
|
31
|
+
round: { task_id: String(round.id) },
|
|
32
|
+
what_this_means: 'The page waiting now is a different round from the one you looked at. Open the Approval queue again to see its renders; nothing was decided.',
|
|
33
|
+
});
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return { round, rounds };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The port's refusals, as the panel shows them.
|
|
40
|
+
function refuseDecision(res, pageId, r) {
|
|
41
|
+
switch (r.outcome) {
|
|
42
|
+
case 'not_an_artist':
|
|
43
|
+
return res.fail('not_an_artist', 403, {
|
|
44
|
+
page_id: pageId,
|
|
45
|
+
what_this_means: 'Only the artist who wrote this page, or another artist, can approve it or send it back. The project owner can still land it through an override request on the task.',
|
|
46
|
+
});
|
|
47
|
+
case 'not_waiting':
|
|
48
|
+
case 'wrong_source':
|
|
49
|
+
case 'not_found':
|
|
50
|
+
return res.fail('not_waiting', 409, {
|
|
51
|
+
page_id: pageId,
|
|
52
|
+
what_this_means: 'This page is no longer waiting for approval (it was decided a moment ago, or it is being applied again). Nothing was changed.',
|
|
53
|
+
});
|
|
54
|
+
case 'builder_inactive':
|
|
55
|
+
return res.fail('builder_inactive', 403);
|
|
56
|
+
case 'refused':
|
|
57
|
+
return res.fail((r.refusal && r.refusal.code) || 'send_back_refused', 400, (r.refusal && r.refusal.detail) || {});
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`page decision: unexpected outcome ${r.outcome}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// What comes after a decision: the next page in the queue, re-read, so the
|
|
64
|
+
// panel names it ("Next in the queue: Contact").
|
|
65
|
+
async function nextWaiting(lifecycle, invData, builderId, crafts) {
|
|
66
|
+
const q = pageStatus.composeApprovals({ inventory: invData, tasks: await lifecycle.listPageTweakTasks(), builderId, crafts });
|
|
67
|
+
const n = q.queue[0] || null;
|
|
68
|
+
return { count: q.count, page: n ? { page_id: n.page_id, title: n.title, path: n.path, task_id: n.task_id } : null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { waitingRoundOrFail, refuseDecision, nextWaiting };
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
// tweaked, and this week's gain.
|
|
26
26
|
// NEXT the recommendation (TW07): the page to tweak next, its why,
|
|
27
27
|
// and the next few for "pick another" (composeNext, below).
|
|
28
|
+
// APPROVALS the Approval queue (TW12, task 1004323): every round waiting
|
|
29
|
+
// for its artist, "1 of 2" (composeApprovals, below).
|
|
28
30
|
//
|
|
29
31
|
// Pure: no fs, no DB, no clock (the caller passes `now`). The route owns the I/O.
|
|
30
32
|
|
|
@@ -573,7 +575,101 @@ function composeEditor({ page, readingPage, round, builderId }) {
|
|
|
573
575
|
};
|
|
574
576
|
}
|
|
575
577
|
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// The Approval queue (task 1004323 / BV2.TW12, ADR 0341 D7): every round
|
|
580
|
+
// WAITING FOR ITS ARTIST, oldest applied first, with what the panel shows for
|
|
581
|
+
// each: the page, the lines the batch rewrote (before and after, and the ones
|
|
582
|
+
// the applier refused, by name), what the page will read once approved, and
|
|
583
|
+
// where its renders are. The renders themselves are the applied branch's
|
|
584
|
+
// images, read from GET /tasks/:id/visuals; nothing here builds a page from the
|
|
585
|
+
// batch (ADR 0341 builder pick 9).
|
|
586
|
+
//
|
|
587
|
+
// `crafts` is the caller's crafts (the lifecycle port's builderCrafts): an
|
|
588
|
+
// artist may decide any round, and anyone may decide a round they authored.
|
|
589
|
+
// The routes decide again; this only keeps the panel from offering a verb its
|
|
590
|
+
// route would refuse.
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
function appliedAtOf(round) {
|
|
594
|
+
const a = pages.parseAppliedBlock(round.description);
|
|
595
|
+
return a.ok ? iso(a.applied.applied_at) : null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
|
|
599
|
+
const batch = pages.parseBatchBlock(round.description);
|
|
600
|
+
const applied = pages.parseAppliedBlock(round.description);
|
|
601
|
+
const refusedBy = new Map();
|
|
602
|
+
if (applied.ok) for (const r of applied.applied.refused || []) if (r && r.key) refusedBy.set(String(r.key), String(r.code || 'refused'));
|
|
603
|
+
const lines = batch.ok ? batch.batch.lines.map((l) => ({
|
|
604
|
+
key: l.key,
|
|
605
|
+
section: l.section == null ? null : l.section,
|
|
606
|
+
before: l.before,
|
|
607
|
+
after: l.after,
|
|
608
|
+
placement: l.placement || null,
|
|
609
|
+
refused: refusedBy.get(String(l.key)) || null,
|
|
610
|
+
})) : [];
|
|
611
|
+
const shipped = (rounds || []).filter((r) => r.status === 'shipped');
|
|
612
|
+
const now = countsFor(rounds);
|
|
613
|
+
const after = countsFor([...shipped, { ...round, status: 'shipped' }]);
|
|
614
|
+
const author = person(round.artist_id, round.artist_login);
|
|
615
|
+
return {
|
|
616
|
+
page_id: pageId,
|
|
617
|
+
surface: page ? page.surface : String(pageId).split(':')[0],
|
|
618
|
+
surface_word: surfaceWord(page ? page.surface : String(pageId).split(':')[0]),
|
|
619
|
+
title: page ? (page.title || page.id) : pageId,
|
|
620
|
+
path: page ? page.path || null : null,
|
|
621
|
+
task_id: String(round.id),
|
|
622
|
+
round: round.round,
|
|
623
|
+
status: round.status,
|
|
624
|
+
state: roundState(round),
|
|
625
|
+
applied_at: appliedAtOf(round),
|
|
626
|
+
line_count: lines.length,
|
|
627
|
+
applied_count: lines.filter((l) => !l.refused).length,
|
|
628
|
+
refused_count: lines.filter((l) => l.refused).length,
|
|
629
|
+
lines,
|
|
630
|
+
...(batch.ok ? {} : { batch_unreadable: batch.code }),
|
|
631
|
+
author,
|
|
632
|
+
by_you: !!author && String(author.id) === String(builderId),
|
|
633
|
+
can_decide: isArtist || (!!author && String(author.id) === String(builderId)),
|
|
634
|
+
status_label: now.status_label,
|
|
635
|
+
status_after_approval: after.status_label,
|
|
636
|
+
sent_back_count: pages.blockPresence(round.description).sent_back_count,
|
|
637
|
+
visuals: { method: 'GET', path: `/tasks/${round.id}/visuals` },
|
|
638
|
+
approve: { method: 'POST', path: `/copy-desk/pages/${pageId}/approve` },
|
|
639
|
+
send_back: { method: 'POST', path: `/copy-desk/pages/${pageId}/send-back` },
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// GET /copy-desk/approvals — the queue, "1 of 2".
|
|
644
|
+
function composeApprovals({ inventory, tasks, builderId, crafts }) {
|
|
645
|
+
const byPage = indexRounds(tasks);
|
|
646
|
+
const invById = new Map(((inventory && inventory.pages) || []).filter((p) => p && p.id).map((p) => [p.id, p]));
|
|
647
|
+
const isArtist = (crafts || []).includes('artist');
|
|
648
|
+
const queue = [];
|
|
649
|
+
for (const [pageId, rounds] of byPage) {
|
|
650
|
+
const open = rounds.filter((r) => !TERMINAL.has(r.status));
|
|
651
|
+
const round = open.length ? open[open.length - 1] : null;
|
|
652
|
+
if (!round || roundState(round) !== 'waiting_for_artist') continue;
|
|
653
|
+
queue.push(approvalFor({ page: invById.get(pageId) || null, pageId, rounds, round, builderId, isArtist }));
|
|
654
|
+
}
|
|
655
|
+
// Oldest applied first, the order they reached the artist; a round with no
|
|
656
|
+
// readable applied block sorts by its task id after the dated ones.
|
|
657
|
+
queue.sort((a, b) => {
|
|
658
|
+
const at = a.applied_at ? Date.parse(a.applied_at) : Infinity;
|
|
659
|
+
const bt = b.applied_at ? Date.parse(b.applied_at) : Infinity;
|
|
660
|
+
return at - bt || Number(a.task_id) - Number(b.task_id);
|
|
661
|
+
});
|
|
662
|
+
queue.forEach((q, i) => { q.position = i + 1; q.of = queue.length; });
|
|
663
|
+
return {
|
|
664
|
+
builder_id: String(builderId),
|
|
665
|
+
is_artist: isArtist,
|
|
666
|
+
count: queue.length,
|
|
667
|
+
queue,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
576
671
|
module.exports = {
|
|
672
|
+
composeApprovals,
|
|
577
673
|
composeEditor,
|
|
578
674
|
TALLY_REASON_PREFIX,
|
|
579
675
|
NEXT_ALTERNATIVES,
|
|
@@ -420,6 +420,32 @@ function unplacedTaskBody(page, line, roundTaskId) {
|
|
|
420
420
|
].join('\n');
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// The send-back (task 1004323 / BV2.TW12, ADR 0341 D4, D7).
|
|
425
|
+
//
|
|
426
|
+
// The artist sends an applied page back with a one-line note of at least ten
|
|
427
|
+
// characters (the flag's own reason floor). Each send-back APPENDS its block,
|
|
428
|
+
// never replaces one, so a round sent back twice says so twice, and the
|
|
429
|
+
// round-state derivation reads the latest `at` (blockPresence above).
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
const MIN_NOTE_CHARS = 10;
|
|
433
|
+
const MAX_NOTE_CHARS = 500;
|
|
434
|
+
|
|
435
|
+
// The note, tidied (one line) and measured, or a named refusal.
|
|
436
|
+
function normalizeSentBackNote(raw) {
|
|
437
|
+
const note = tidy(raw);
|
|
438
|
+
if (note.length < MIN_NOTE_CHARS) return fail('note_too_short', { min: MIN_NOTE_CHARS, length: note.length });
|
|
439
|
+
if (note.length > MAX_NOTE_CHARS) return fail('note_too_long', { max: MAX_NOTE_CHARS, length: note.length });
|
|
440
|
+
return { ok: true, note };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The description with one more sent-back block at its end.
|
|
444
|
+
function appendSentBack(description, { at, by, note }) {
|
|
445
|
+
const src = String(description || '');
|
|
446
|
+
return { ok: true, description: src.trimEnd() + (src.trim() ? '\n\n' : '') + composeSentBackBlock({ at, by, note }) };
|
|
447
|
+
}
|
|
448
|
+
|
|
423
449
|
module.exports = {
|
|
424
450
|
SCHEMA,
|
|
425
451
|
SOURCE,
|
|
@@ -438,6 +464,10 @@ module.exports = {
|
|
|
438
464
|
parseAppliedBlock,
|
|
439
465
|
composeSentBackBlock,
|
|
440
466
|
parseSentBackBlocks,
|
|
467
|
+
MIN_NOTE_CHARS,
|
|
468
|
+
MAX_NOTE_CHARS,
|
|
469
|
+
normalizeSentBackNote,
|
|
470
|
+
appendSentBack,
|
|
441
471
|
blockPresence,
|
|
442
472
|
roundOpenForWriting,
|
|
443
473
|
MAX_LINE_CHARS,
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// 1004320) the Word round trip, GET and POST /copy-desk/pages/:pageId/draft.docx:
|
|
19
19
|
// the upload lands in the round's DRAFT, through the autosave's own port call.
|
|
20
20
|
// TW10 (task 1004321) added the editor's read, GET /copy-desk/pages/:pageId/draft.
|
|
21
|
+
// TW12 (task 1004323) added the Approval queue: GET /copy-desk/approvals and the
|
|
22
|
+
// two decisions, POST /copy-desk/pages/:pageId/approve and /send-back, which
|
|
23
|
+
// move the round TASK through the lifecycle port and write nothing here.
|
|
21
24
|
//
|
|
22
25
|
// THE RANK SHAPE, AND WHY IT IS ASYMMETRIC:
|
|
23
26
|
//
|
|
@@ -58,6 +61,7 @@ const pageData = require('../page-data');
|
|
|
58
61
|
const pageStatus = require('../page-status');
|
|
59
62
|
const pages = require('../pages');
|
|
60
63
|
const docx = require('../docx');
|
|
64
|
+
const { waitingRoundOrFail, refuseDecision, nextWaiting } = require('../page-decisions');
|
|
61
65
|
|
|
62
66
|
const { validateOrRespond } = api;
|
|
63
67
|
|
|
@@ -118,6 +122,18 @@ const DRAFT_BODY_SCHEMA = Object.freeze({
|
|
|
118
122
|
});
|
|
119
123
|
// The submit takes nothing: it freezes the draft already on the task.
|
|
120
124
|
const SUBMIT_BODY_SCHEMA = Object.freeze({});
|
|
125
|
+
// The Approval queue's two decisions (task 1004323 / TW12). Who decides is the
|
|
126
|
+
// session and which page is the path; `task_id` only pins the round the artist
|
|
127
|
+
// looked at, and is checked against the page's waiting round, never trusted.
|
|
128
|
+
// The note is the one piece of text a decision carries, and it is a judgement
|
|
129
|
+
// for the next applier, never wording for the page.
|
|
130
|
+
const APPROVE_BODY_SCHEMA = Object.freeze({
|
|
131
|
+
task_id: { type: 'string', maxLength: 20, minLength: 1 },
|
|
132
|
+
});
|
|
133
|
+
const SEND_BACK_BODY_SCHEMA = Object.freeze({
|
|
134
|
+
note: { required: true, type: 'string', maxLength: pages.MAX_NOTE_CHARS * 2, minLength: 1 },
|
|
135
|
+
task_id: { type: 'string', maxLength: 20, minLength: 1 },
|
|
136
|
+
});
|
|
121
137
|
|
|
122
138
|
// The refusals of pages.resolveDraft / freezeBatch, as HTTP statuses.
|
|
123
139
|
const PAGE_REFUSAL_STATUS = Object.freeze({
|
|
@@ -1338,6 +1354,143 @@ module.exports = function buildCopyDeskRouter() {
|
|
|
1338
1354
|
}
|
|
1339
1355
|
});
|
|
1340
1356
|
|
|
1357
|
+
// -------------------------------------------------------------------------
|
|
1358
|
+
// The Approval queue (task 1004323 / BV2.TW12, ADR 0341 D7 and D11).
|
|
1359
|
+
//
|
|
1360
|
+
// A passed page tweak WAITS at completed for its artist (page-tweak-hold.js).
|
|
1361
|
+
// The artist approves it (it lands) or sends it back with a note (the /tweak
|
|
1362
|
+
// queue), both through the lifecycle port. WHO DECIDES: the author or any
|
|
1363
|
+
// artist, checked by the port; the owner's escape hatch is the override
|
|
1364
|
+
// request. Anyone may READ the queue. Helpers: ../page-decisions.js.
|
|
1365
|
+
// -------------------------------------------------------------------------
|
|
1366
|
+
|
|
1367
|
+
// GET /copy-desk/approvals — every round waiting for its artist, oldest
|
|
1368
|
+
// applied first, with its position ("1 of 2"), the lines it rewrote (and the
|
|
1369
|
+
// ones the applier refused), and where its renders are. Any signed-in
|
|
1370
|
+
// builder; `can_decide` says whether the caller may approve or send back.
|
|
1371
|
+
router.get('/copy-desk/approvals', api.requireBuilder, async (req, res) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const inv = inventoryOrFail(res);
|
|
1374
|
+
if (!inv) return;
|
|
1375
|
+
const lifecycle = pageReadsOrFail(res);
|
|
1376
|
+
if (!lifecycle) return;
|
|
1377
|
+
const [tasks, crafts] = await Promise.all([
|
|
1378
|
+
lifecycle.listPageTweakTasks(),
|
|
1379
|
+
typeof lifecycle.builderCrafts === 'function' ? lifecycle.builderCrafts(req.builder.id) : Promise.resolve([]),
|
|
1380
|
+
]);
|
|
1381
|
+
const out = pageStatus.composeApprovals({ inventory: inv.data, tasks, builderId: req.builder.id, crafts });
|
|
1382
|
+
res.set('Cache-Control', 'no-store');
|
|
1383
|
+
res.json({ ok: true, ...out });
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
log.error({ err }, 'GET /copy-desk/approvals failed');
|
|
1386
|
+
if (!res.headersSent) res.fail('copy_approvals_failed', 500);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
function decisionPortOrFail(res, name) {
|
|
1391
|
+
const lifecycle = pageReadsOrFail(res);
|
|
1392
|
+
if (!lifecycle) return null;
|
|
1393
|
+
if (typeof lifecycle[name] !== 'function') {
|
|
1394
|
+
res.fail('lifecycle_unavailable', 503, { what_this_means: 'This instance cannot move a page tweak on from the Approval queue. Nothing was decided.' });
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
return lifecycle;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// -------------------------------------------------------------------------
|
|
1401
|
+
// POST /copy-desk/pages/:pageId/approve — the held pass goes on to land.
|
|
1402
|
+
// Body: { task_id? } (the round the artist looked at). The port promotes
|
|
1403
|
+
// completed -> confirmed with the rewards the hold skipped and nudges the
|
|
1404
|
+
// merge; the reconciler lands it otherwise. The count goes up when it ships.
|
|
1405
|
+
// 200 approved · 409 not_waiting · 409 round_moved · 403 not_an_artist
|
|
1406
|
+
// -------------------------------------------------------------------------
|
|
1407
|
+
router.post('/copy-desk/pages/:pageId/approve', api.requireBuilder, async (req, res) => {
|
|
1408
|
+
try {
|
|
1409
|
+
if (validateOrRespond(req, res, APPROVE_BODY_SCHEMA)) return;
|
|
1410
|
+
const found = pageOrFail(req, res);
|
|
1411
|
+
if (!found) return;
|
|
1412
|
+
const { pageId, inv } = found;
|
|
1413
|
+
const lifecycle = decisionPortOrFail(res, 'approveHeldTweak');
|
|
1414
|
+
if (!lifecycle) return;
|
|
1415
|
+
const waiting = await waitingRoundOrFail(res, lifecycle, pageId, req.body || {}, openRoundOf);
|
|
1416
|
+
if (!waiting) return;
|
|
1417
|
+
const { round } = waiting;
|
|
1418
|
+
// What the page will read once this round ships, from ALL its rounds, read
|
|
1419
|
+
// before the approve moves this one on.
|
|
1420
|
+
const before = pageStatus.composeApprovals({ inventory: inv.data, tasks: waiting.rounds, builderId: req.builder.id, crafts: [] }).queue[0] || null;
|
|
1421
|
+
const r = await lifecycle.approveHeldTweak({ taskId: round.id, builderId: req.builder.id });
|
|
1422
|
+
if (r.outcome !== 'approved') return refuseDecision(res, pageId, r);
|
|
1423
|
+
const crafts = typeof lifecycle.builderCrafts === 'function' ? await lifecycle.builderCrafts(req.builder.id) : [];
|
|
1424
|
+
return res.json({
|
|
1425
|
+
ok: true,
|
|
1426
|
+
outcome: 'approved',
|
|
1427
|
+
page_id: pageId,
|
|
1428
|
+
round: { task_id: String(r.task.id), state: 'landing', status: 'confirmed' },
|
|
1429
|
+
status_after: before ? before.status_after_approval : null,
|
|
1430
|
+
land: r.land || null,
|
|
1431
|
+
next: await nextWaiting(lifecycle, inv.data, req.builder.id, crafts),
|
|
1432
|
+
});
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
log.error({ err }, 'POST /copy-desk/pages/:pageId/approve failed');
|
|
1435
|
+
if (!res.headersSent) res.fail('copy_page_approve_failed', 500);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// -------------------------------------------------------------------------
|
|
1440
|
+
// POST /copy-desk/pages/:pageId/send-back — back to the /tweak queue with a
|
|
1441
|
+
// note (ADR 0341 D7).
|
|
1442
|
+
//
|
|
1443
|
+
// Body: { note, task_id? }. The note is one line of at least 10 characters
|
|
1444
|
+
// (pages.normalizeSentBackNote). The port appends a page-tweak-sent-back
|
|
1445
|
+
// block { at, by, note }, releases any open claim, drops the round's grade
|
|
1446
|
+
// row, returns the task to ready and closes the unmerged PR, so the round
|
|
1447
|
+
// reads `submitted` again.
|
|
1448
|
+
//
|
|
1449
|
+
// 200 sent_back · 400 note_too_short / note_too_long · 409 not_waiting ·
|
|
1450
|
+
// 409 round_moved · 403 not_an_artist
|
|
1451
|
+
// -------------------------------------------------------------------------
|
|
1452
|
+
router.post('/copy-desk/pages/:pageId/send-back', api.requireBuilder, async (req, res) => {
|
|
1453
|
+
try {
|
|
1454
|
+
if (validateOrRespond(req, res, SEND_BACK_BODY_SCHEMA)) return;
|
|
1455
|
+
const found = pageOrFail(req, res);
|
|
1456
|
+
if (!found) return;
|
|
1457
|
+
const { pageId, inv } = found;
|
|
1458
|
+
const note = pages.normalizeSentBackNote(req.body.note);
|
|
1459
|
+
if (!note.ok) {
|
|
1460
|
+
return res.fail(note.code, 400, {
|
|
1461
|
+
...note.detail,
|
|
1462
|
+
what_this_means: `Say in one line what needs changing, in at least ${pages.MIN_NOTE_CHARS} characters, so whoever applies it again knows what to do. Nothing was sent.`,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
const lifecycle = decisionPortOrFail(res, 'sendBackHeldTweak');
|
|
1466
|
+
if (!lifecycle) return;
|
|
1467
|
+
const waiting = await waitingRoundOrFail(res, lifecycle, pageId, req.body || {}, openRoundOf);
|
|
1468
|
+
if (!waiting) return;
|
|
1469
|
+
const { round } = waiting;
|
|
1470
|
+
const at = new Date().toISOString();
|
|
1471
|
+
const by = String(req.builder.id);
|
|
1472
|
+
const r = await lifecycle.sendBackHeldTweak({
|
|
1473
|
+
taskId: round.id,
|
|
1474
|
+
builderId: req.builder.id,
|
|
1475
|
+
compose: (task) => pages.appendSentBack(task.description, { at, by, note: note.note }),
|
|
1476
|
+
});
|
|
1477
|
+
if (r.outcome !== 'sent_back') return refuseDecision(res, pageId, r);
|
|
1478
|
+
const crafts = typeof lifecycle.builderCrafts === 'function' ? await lifecycle.builderCrafts(req.builder.id) : [];
|
|
1479
|
+
return res.json({
|
|
1480
|
+
ok: true,
|
|
1481
|
+
outcome: 'sent_back',
|
|
1482
|
+
page_id: pageId,
|
|
1483
|
+
round: { task_id: String(r.task.id), state: 'submitted', status: r.task.status },
|
|
1484
|
+
sent_back: { at, by, note: note.note },
|
|
1485
|
+
pr: r.pr || null,
|
|
1486
|
+
next: await nextWaiting(lifecycle, inv.data, req.builder.id, crafts),
|
|
1487
|
+
});
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
log.error({ err }, 'POST /copy-desk/pages/:pageId/send-back failed');
|
|
1490
|
+
if (!res.headersSent) res.fail('copy_page_send_back_failed', 500);
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1341
1494
|
return router;
|
|
1342
1495
|
};
|
|
1343
1496
|
|