@bongos/core 1.21.1 → 1.21.2
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 +51 -36
- package/docs/copy-inventory.md +50 -49
- package/docs/copy-registry.json +60 -51
- package/docs/module-api-changelog.md +3 -1
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +49 -41
- package/migrations/core_270_craft_achievements.sql +128 -0
- package/modules/economy/achievements.js +199 -4
- package/modules/economy/credits.js +16 -0
- package/modules/economy/reward.js +3 -0
- package/modules/hall-ui/public/builders.js +1 -1
- package/modules/hall-ui/public/hall-render.js +24 -2
- package/modules/hall-ui/public/roster.css +21 -0
- package/modules/hall-ui/public/roster.html +3 -0
- package/modules/hall-ui/public/roster.js +121 -21
- package/modules/hall-ui/public/roster.states.json +26 -1
- package/modules/hall-ui/records/builders-roster.md +20 -0
- package/modules/lifecycle/publish-reconciler.js +25 -0
- package/modules/status-ui/public/status.js +39 -7
- package/modules/status-ui/public/style.css +24 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/hall-preview/refresh-fixtures.js +2 -0
- package/src/bongos/db.js +13 -0
- package/src/bongos/role-stats.js +36 -2
- package/src/bongos/routes/public.js +13 -2
- package/src/module-api.js +1 -1
- package/tests/achievements_reconcile.mjs +13 -0
- package/tests/craft_achievements.mjs +349 -0
- package/tests/founding_builders.mjs +2 -1
- package/tests/hall_builders_page_pass.mjs +2 -1
- package/tests/leaderboard_roles.mjs +272 -0
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":40,"lines":
|
|
8
|
+
"counts": {"pages":40,"lines":4603,"placed":1619,"shared":245,"unplaced":2739,"omitted_data":2290},
|
|
9
9
|
"pages": [
|
|
10
10
|
{
|
|
11
11
|
"id": "builders:atlas",
|
|
@@ -1364,7 +1364,7 @@
|
|
|
1364
1364
|
"surface": "builders",
|
|
1365
1365
|
"title": "Builders' Hall",
|
|
1366
1366
|
"states_read": ["home","standing","standing-karma","out","founding","genesis","decide","demo"],
|
|
1367
|
-
"files_hash": "
|
|
1367
|
+
"files_hash": "8aae91616add6938",
|
|
1368
1368
|
"reading_hash": "baed6b11d9ad5db4",
|
|
1369
1369
|
"counts": {"lines":204,"placed":68,"shared":7,"unplaced":129},
|
|
1370
1370
|
"omitted": {"data":182},
|
|
@@ -1373,7 +1373,7 @@
|
|
|
1373
1373
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":884,"string_id":"87cee78ca134"},
|
|
1374
1374
|
{"key":"L0002","section":"top bar","text":"Standing","placement":"shared","state":"standing"},
|
|
1375
1375
|
{"key":"L0003","section":"top bar","text":"Home","placement":"shared"},
|
|
1376
|
-
{"key":"L0004","section":"top bar","text":"Welcome back, {…}.","placement":"shared","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1376
|
+
{"key":"L0004","section":"top bar","text":"Welcome back, {…}.","placement":"shared","file":"modules/hall-ui/public/hall-render.js","line":600,"string_id":"0658f0358ffd"},
|
|
1377
1377
|
{"key":"L0005","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":495,"string_id":"eba610d25523"},
|
|
1378
1378
|
{"key":"L0006","section":"top bar","text":"Ctrl K","placement":"shared"},
|
|
1379
1379
|
{"key":"L0007","section":"Good morning, {…}","text":"Good morning, {…}","placement":"unplaced"},
|
|
@@ -1390,14 +1390,14 @@
|
|
|
1390
1390
|
{"key":"L0018","section":"Good morning, {…}","text":"Member since {…}","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":566,"string_id":"df335a1224ae","state":"standing"},
|
|
1391
1391
|
{"key":"L0019","section":"Good morning, {…}","text":"+1 karma in 30d","placement":"unplaced","state":"standing"},
|
|
1392
1392
|
{"key":"L0020","section":"Good morning, {…}","text":"#2 on the leaderboard","placement":"unplaced"},
|
|
1393
|
-
{"key":"L0021","section":"Good morning, {…}","text":"Preferred work:","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1394
|
-
{"key":"L0022","section":"Good morning, {…}","text":"Change in Settings →","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1393
|
+
{"key":"L0021","section":"Good morning, {…}","text":"Preferred work:","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":581,"string_id":"ed8edd684547","state":"standing"},
|
|
1394
|
+
{"key":"L0022","section":"Good morning, {…}","text":"Change in Settings →","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":581,"string_id":"0279db21e29a","state":"standing"},
|
|
1395
1395
|
{"key":"L0023","section":"Good morning, {…}","text":"karma","placement":"unplaced","state":"standing"},
|
|
1396
1396
|
{"key":"L0024","section":"Standing","text":"Standing","placement":"unplaced"},
|
|
1397
1397
|
{"key":"L0025","section":"Standing","text":"Credits","placement":"unplaced"},
|
|
1398
|
-
{"key":"L0026","section":"Standing","text":"On the board","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1398
|
+
{"key":"L0026","section":"Standing","text":"On the board","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":630,"string_id":"11a1905c76b2"},
|
|
1399
1399
|
{"key":"L0027","section":"Standing","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
|
|
1400
|
-
{"key":"L0028","section":"Standing","text":"Preferred work","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1400
|
+
{"key":"L0028","section":"Standing","text":"Preferred work","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":630,"string_id":"f03d68e60416"},
|
|
1401
1401
|
{"key":"L0029","section":"Standing","text":"View full standing →","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":86,"string_id":"4babdfeaf27f"},
|
|
1402
1402
|
{"key":"L0030","section":"Goals","text":"Goals","placement":"unplaced"},
|
|
1403
1403
|
{"key":"L0031","section":"Goals","text":"The shared goals you’ve joined. Browse, join, and leave goals — and read their criteria — on the","placement":"placed","file":"modules/hall-ui/public/goals.js","line":94,"string_id":"fc422cced9e3"},
|
|
@@ -1406,9 +1406,9 @@
|
|
|
1406
1406
|
{"key":"L0034","section":"Goals","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":163,"string_id":"9a803563c948","occurrences":5},
|
|
1407
1407
|
{"key":"L0035","section":"Goals","text":"All 26 goals →","placement":"unplaced"},
|
|
1408
1408
|
{"key":"L0036","section":"Needs your attention13","text":"Needs your attention","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":597,"string_id":"7def635421ea"},
|
|
1409
|
-
{"key":"L0037","section":"From the system","text":"From the system","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1409
|
+
{"key":"L0037","section":"From the system","text":"From the system","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":708,"string_id":"d32c5e058d1e"},
|
|
1410
1410
|
{"key":"L0038","section":"From the system","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":2},
|
|
1411
|
-
{"key":"L0039","section":"From the system","text":"Open the Board Room","placement":"placed","file":"modules/hall-ui/public/genesis-home.js","line":
|
|
1411
|
+
{"key":"L0039","section":"From the system","text":"Open the Board Room","placement":"placed","file":"modules/hall-ui/public/genesis-home.js","line":198,"string_id":"fe9986a9d821"},
|
|
1412
1412
|
{"key":"L0040","section":"Ideas awaiting triage 151","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":897,"string_id":"e181637c5a5e"},
|
|
1413
1413
|
{"key":"L0041","section":"Ideas awaiting triage 151","text":"Triage","placement":"unplaced","occurrences":5},
|
|
1414
1414
|
{"key":"L0042","section":"Ideas awaiting triage 151","text":"…{…} more open ideas — triage clears them one at a time.","placement":"placed","file":"modules/hall-ui/public/builders.js","line":895,"string_id":"e35f11bdc5d7"},
|
|
@@ -1474,8 +1474,8 @@
|
|
|
1474
1474
|
{"key":"L0102","section":"Progress","text":"Progress","placement":"unplaced","state":"standing"},
|
|
1475
1475
|
{"key":"L0103","section":"Progress","text":"How your output has changed over time — work shipped, cost and time per work, and how reviews held up.","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":213,"string_id":"298ff22576ee","state":"standing"},
|
|
1476
1476
|
{"key":"L0104","section":"Progress","text":"You","placement":"placed","file":"modules/hall-ui/public/board-room.js","line":440,"string_id":"8e527ab12334","state":"standing","occurrences":3},
|
|
1477
|
-
{"key":"L0105","section":"Progress","text":"All builders","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1478
|
-
{"key":"L0106","section":"Progress","text":"Kind of work","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1477
|
+
{"key":"L0105","section":"Progress","text":"All builders","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":1181,"string_id":"8b736e365191","state":"standing","occurrences":3},
|
|
1478
|
+
{"key":"L0106","section":"Progress","text":"Kind of work","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":1146,"string_id":"e3b473af2f69","state":"standing"},
|
|
1479
1479
|
{"key":"L0107","section":"Progress","text":"All work","placement":"unplaced","state":"standing"},
|
|
1480
1480
|
{"key":"L0108","section":"Progress","text":"bug (11)","placement":"unplaced","state":"standing"},
|
|
1481
1481
|
{"key":"L0109","section":"Progress","text":"Works {…}","placement":"unplaced","state":"standing"},
|
|
@@ -1495,7 +1495,7 @@
|
|
|
1495
1495
|
{"key":"L0123","section":"Progress","text":"total $1241.96","placement":"unplaced","state":"standing"},
|
|
1496
1496
|
{"key":"L0124","section":"Progress","text":"no data in window","placement":"unplaced","state":"standing"},
|
|
1497
1497
|
{"key":"L0125","section":"Progress","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"standing"},
|
|
1498
|
-
{"key":"L0126","section":"Progress","text":"Kinds of work shipped","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":
|
|
1498
|
+
{"key":"L0126","section":"Progress","text":"Kinds of work shipped","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":1259,"string_id":"7d6eaa8d4514","state":"standing"},
|
|
1499
1499
|
{"key":"L0127","section":"Achievements","text":"Achievements","placement":"unplaced","state":"standing"},
|
|
1500
1500
|
{"key":"L0128","section":"Achievements","text":"Eight achievements recognize sustained contribution. Earned ones are highlighted; the rest show what’s left to do.","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":228,"string_id":"854a21d2eb39","state":"standing"},
|
|
1501
1501
|
{"key":"L0129","section":"Achievements","text":"Won {…}","placement":"placed","file":"modules/hall-ui/public/builders.js","line":477,"string_id":"c5fd76a9e03d","state":"standing","occurrences":6},
|
|
@@ -2490,11 +2490,11 @@
|
|
|
2490
2490
|
"id": "builders:roster",
|
|
2491
2491
|
"surface": "builders",
|
|
2492
2492
|
"title": "Builders",
|
|
2493
|
-
"states_read": ["roster","search","out"],
|
|
2494
|
-
"files_hash": "
|
|
2495
|
-
"reading_hash": "
|
|
2496
|
-
"counts": {"lines":
|
|
2497
|
-
"omitted": {"data":
|
|
2493
|
+
"states_read": ["roster","artists","search","out"],
|
|
2494
|
+
"files_hash": "a9ce996d0130159f",
|
|
2495
|
+
"reading_hash": "1cf32981ae9e428e",
|
|
2496
|
+
"counts": {"lines":36,"placed":8,"shared":5,"unplaced":23},
|
|
2497
|
+
"omitted": {"data":58},
|
|
2498
2498
|
"notes": [],
|
|
2499
2499
|
"lines": [
|
|
2500
2500
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":884,"string_id":"87cee78ca134"},
|
|
@@ -2503,28 +2503,36 @@
|
|
|
2503
2503
|
{"key":"L0004","section":"top bar","text":"Ctrl K","placement":"shared"},
|
|
2504
2504
|
{"key":"L0005","section":"Builders","text":"Builders","placement":"unplaced"},
|
|
2505
2505
|
{"key":"L0006","section":"Builders","text":"{…} project{…}","placement":"placed","file":"modules/hall-ui/public/fleet.js","line":39,"string_id":"771bef3d31b5"},
|
|
2506
|
-
{"key":"L0007","section":"The
|
|
2507
|
-
{"key":"L0008","section":"The
|
|
2508
|
-
{"key":"L0009","section":"The
|
|
2509
|
-
{"key":"L0010","section":"The
|
|
2510
|
-
{"key":"L0011","section":"The
|
|
2511
|
-
{"key":"L0012","section":"The
|
|
2512
|
-
{"key":"L0013","section":"The
|
|
2513
|
-
{"key":"L0014","section":"The
|
|
2514
|
-
{"key":"L0015","section":"The
|
|
2515
|
-
{"key":"L0016","section":"The
|
|
2516
|
-
{"key":"L0017","section":"The
|
|
2517
|
-
{"key":"L0018","section":"The
|
|
2518
|
-
{"key":"L0019","section":"The
|
|
2519
|
-
{"key":"L0020","section":"The
|
|
2520
|
-
{"key":"L0021","section":"The
|
|
2521
|
-
{"key":"L0022","section":"The
|
|
2522
|
-
{"key":"L0023","section":"The
|
|
2523
|
-
{"key":"L0024","section":"The
|
|
2524
|
-
{"key":"L0025","section":"The
|
|
2525
|
-
{"key":"L0026","section":"The
|
|
2526
|
-
{"key":"L0027","section":"The
|
|
2527
|
-
{"key":"L0028","section":"
|
|
2506
|
+
{"key":"L0007","section":"The roster15","text":"The roster","placement":"placed","file":"modules/hall-ui/public/roster.html","line":36,"string_id":"38ce5d68c182"},
|
|
2507
|
+
{"key":"L0008","section":"The roster15","text":"Everyone","placement":"unplaced"},
|
|
2508
|
+
{"key":"L0009","section":"The roster15","text":"Engineers","placement":"unplaced"},
|
|
2509
|
+
{"key":"L0010","section":"The roster15","text":"Artists","placement":"unplaced"},
|
|
2510
|
+
{"key":"L0011","section":"The roster15","text":"Ideators","placement":"unplaced"},
|
|
2511
|
+
{"key":"L0012","section":"The roster15","text":"Search","placement":"unplaced"},
|
|
2512
|
+
{"key":"L0013","section":"The roster15","text":"name or login","placement":"unplaced","attr":"placeholder"},
|
|
2513
|
+
{"key":"L0014","section":"The roster15","text":"Sort","placement":"unplaced"},
|
|
2514
|
+
{"key":"L0015","section":"The roster15","text":"place","placement":"unplaced"},
|
|
2515
|
+
{"key":"L0016","section":"The roster15","text":"rank","placement":"unplaced"},
|
|
2516
|
+
{"key":"L0017","section":"The roster15","text":"most karma","placement":"unplaced"},
|
|
2517
|
+
{"key":"L0018","section":"The roster15","text":"{…} tokens","placement":"placed","file":"modules/hall-ui/public/sessions.js","line":170,"string_id":"955499559f0a"},
|
|
2518
|
+
{"key":"L0019","section":"The roster15","text":"Place","placement":"unplaced"},
|
|
2519
|
+
{"key":"L0020","section":"The roster15","text":"Builder","placement":"unplaced"},
|
|
2520
|
+
{"key":"L0021","section":"The roster15","text":"Roles","placement":"unplaced"},
|
|
2521
|
+
{"key":"L0022","section":"The roster15","text":"Rank","placement":"unplaced"},
|
|
2522
|
+
{"key":"L0023","section":"The roster15","text":"Credits as {…}","placement":"unplaced","state":"artists"},
|
|
2523
|
+
{"key":"L0024","section":"The roster15","text":"Credits","placement":"unplaced"},
|
|
2524
|
+
{"key":"L0025","section":"The roster15","text":"Karma","placement":"unplaced"},
|
|
2525
|
+
{"key":"L0026","section":"The roster15","text":"Tokens","placement":"unplaced"},
|
|
2526
|
+
{"key":"L0027","section":"The roster15","text":"No builder on the roster matches that search.","placement":"unplaced","state":"search"},
|
|
2527
|
+
{"key":"L0028","section":"The roster15","text":"Ideator","placement":"unplaced","occurrences":5},
|
|
2528
|
+
{"key":"L0029","section":"The roster15","text":"Artist","placement":"unplaced","occurrences":3},
|
|
2529
|
+
{"key":"L0030","section":"The roster15","text":"B","placement":"unplaced","occurrences":4},
|
|
2530
|
+
{"key":"L0031","section":"The roster15","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
|
|
2531
|
+
{"key":"L0032","section":"The roster15","text":"Per page","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"4a46b478edaf"},
|
|
2532
|
+
{"key":"L0033","section":"The roster15","text":"‹ Prev","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"de8fa5223aee"},
|
|
2533
|
+
{"key":"L0034","section":"The roster15","text":"Page {…} of {…}","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":112,"string_id":"15a49bf15007"},
|
|
2534
|
+
{"key":"L0035","section":"The roster15","text":"Next ›","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"f52ba261d930"},
|
|
2535
|
+
{"key":"L0036","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":471,"string_id":"1883ad7f07fc"}
|
|
2528
2536
|
]
|
|
2529
2537
|
},
|
|
2530
2538
|
{
|
|
@@ -5027,7 +5035,7 @@
|
|
|
5027
5035
|
"surface": "status",
|
|
5028
5036
|
"title": "{{worldName}} — the public ledger",
|
|
5029
5037
|
"states_read": ["default"],
|
|
5030
|
-
"files_hash": "
|
|
5038
|
+
"files_hash": "c63f0262868bc3e4",
|
|
5031
5039
|
"reading_hash": "2853e52b66ba076c",
|
|
5032
5040
|
"counts": {"lines":87,"placed":49,"shared":14,"unplaced":24},
|
|
5033
5041
|
"omitted": {"data":89},
|
|
@@ -5036,7 +5044,7 @@
|
|
|
5036
5044
|
{"key":"L0001","section":"top bar","text":"The Public Ledger of the City's Making","placement":"shared","file":"modules/status-ui/public/index.html","line":125,"string_id":"acdf97805931"},
|
|
5037
5045
|
{"key":"L0002","section":"top bar","text":"A shipping container has washed ashore upon the coast south of Athens. About it a new civilization rises. Set out below: the city's affairs, openly recorded.","placement":"shared","file":"modules/status-ui/public/index.html","line":126,"string_id":"f4a664041da5"},
|
|
5038
5046
|
{"key":"L0003","section":"top bar","text":"Cross into the city →","placement":"shared","file":"modules/status-ui/public/index.html","line":128,"string_id":"3aa071ef1142"},
|
|
5039
|
-
{"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":
|
|
5047
|
+
{"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1201,"string_id":"c1e3c2212207"},
|
|
5040
5048
|
{"key":"L0005","section":"navigation","text":"Pulse","placement":"shared"},
|
|
5041
5049
|
{"key":"L0006","section":"navigation","text":"Watch","placement":"shared"},
|
|
5042
5050
|
{"key":"L0007","section":"navigation","text":"Architects","placement":"shared"},
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- core_270_craft_achievements.sql — achievements for artist and ideator moments
|
|
2
|
+
-- (task 1004438 / WA6.RP08, goal 1000095, criterion wa6-roles-read-true-in-public).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. Every achievement was a claim or ship event, and the one art badge
|
|
5
|
+
-- ("art-pipeline-shipper") fires when CODE touches art/ — it rewards engineering,
|
|
6
|
+
-- not art. An artist who rewrites pages in the studio and an ideator whose thoughts
|
|
7
|
+
-- become work earned nothing on the shelf. These seven are each keyed to a LEDGER
|
|
8
|
+
-- fact the economy already books (credit_log), so they are awarded once, by the
|
|
9
|
+
-- same reconcile every other achievement uses (modules/economy/achievements.js
|
|
10
|
+
-- STATE_EVALUATORS), and backfilled below for work that already happened.
|
|
11
|
+
--
|
|
12
|
+
-- credit_bonus is 0 on all seven: they recognise work the ledger has already
|
|
13
|
+
-- paid for, and a backfill that minted bonus credits for past work is a money
|
|
14
|
+
-- decision this migration does not make.
|
|
15
|
+
--
|
|
16
|
+
-- A core migration, not an economy_* one: `achievements` and
|
|
17
|
+
-- `builder_achievements` are core tables (modules/economy/CLAUDE.md, Tables).
|
|
18
|
+
|
|
19
|
+
INSERT INTO achievements (id, name, description, icon, credit_bonus, sort_order) VALUES
|
|
20
|
+
('first-thought-ignited', 'First Thought Ignited',
|
|
21
|
+
'A thought of yours became shipped work.',
|
|
22
|
+
'💡', 0, 90),
|
|
23
|
+
('first-full-idea', 'First Full Idea',
|
|
24
|
+
'A Full Idea of yours passed the completeness bar.',
|
|
25
|
+
'📜', 0, 100),
|
|
26
|
+
('first-board-ratification', 'Ratified by the Board',
|
|
27
|
+
'The board ratified an idea of yours.',
|
|
28
|
+
'🏛️', 0, 110),
|
|
29
|
+
('thought-built-ten', 'A Thought That Built Ten',
|
|
30
|
+
'One thought of yours led to ten or more shipped tasks.',
|
|
31
|
+
'🌟', 0, 120),
|
|
32
|
+
('first-page-approved', 'First Page Approved',
|
|
33
|
+
'A page you rewrote in Tweak Mode was approved and shipped.',
|
|
34
|
+
'🎨', 0, 130),
|
|
35
|
+
('ten-pages-approved', 'Ten Pages Approved',
|
|
36
|
+
'Ten pages you rewrote in Tweak Mode were approved and shipped.',
|
|
37
|
+
'🖼️', 0, 140),
|
|
38
|
+
('whole-surface-tweaked', 'A Whole Surface',
|
|
39
|
+
'Every page of one surface was rewritten by you and approved.',
|
|
40
|
+
'🗺️', 0, 150)
|
|
41
|
+
ON CONFLICT (id) DO UPDATE
|
|
42
|
+
SET name = EXCLUDED.name,
|
|
43
|
+
description = EXCLUDED.description,
|
|
44
|
+
icon = EXCLUDED.icon,
|
|
45
|
+
credit_bonus = EXCLUDED.credit_bonus,
|
|
46
|
+
sort_order = EXCLUDED.sort_order;
|
|
47
|
+
|
|
48
|
+
-- The misleading one, renamed rather than retired: builders who earned it keep it,
|
|
49
|
+
-- and it now says what it is — engineering work on the art pipeline. The palette
|
|
50
|
+
-- icon moves to First Page Approved, the artist's own badge.
|
|
51
|
+
UPDATE achievements
|
|
52
|
+
SET name = 'Art Pipeline Engineer',
|
|
53
|
+
description = 'Ship engineering work on the art/ pipeline (code, not art; an artist earns First Page Approved).',
|
|
54
|
+
icon = '🛠️'
|
|
55
|
+
WHERE id = 'art-pipeline-shipper';
|
|
56
|
+
|
|
57
|
+
-- ---- Backfill: everyone who already earned one, from the ledger ---------------
|
|
58
|
+
-- Each unlock is stamped with the FIRST qualifying credit's time and task, so the
|
|
59
|
+
-- shelf dates the moment, not this migration. ON CONFLICT DO NOTHING: re-running
|
|
60
|
+
-- converges, and an unlock the live reconcile already wrote is left alone.
|
|
61
|
+
-- "A Whole Surface" needs the page inventory, which is a file, not a table, so
|
|
62
|
+
-- SQL cannot award it. It is backfilled by the lifecycle's periodic sweep, which
|
|
63
|
+
-- runs economy's backfillCraftAchievements ONCE at every server start (so the
|
|
64
|
+
-- deploy that applies this migration awards it within the first sweep), and by
|
|
65
|
+
-- the reconcile on the artist's next approved page; by hand,
|
|
66
|
+
-- `node scripts/gds/diagnose-achievements.js --apply`.
|
|
67
|
+
|
|
68
|
+
-- the idea-stream and artist-lane prefixes, spelled as LIKE literals with the
|
|
69
|
+
-- underscore escaped (modules/economy/credits.js IDEA_STREAM_REASON_PREFIXES,
|
|
70
|
+
-- ARTIST_PAGE_REASON_PREFIX; tests/craft_achievements.mjs holds them together)
|
|
71
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
72
|
+
SELECT DISTINCT ON (builder_id) builder_id, 'first-thought-ignited', recorded_at, task_id
|
|
73
|
+
FROM credit_log
|
|
74
|
+
WHERE reason LIKE 'idea.credit.task\_ship:%' AND delta > 0
|
|
75
|
+
ORDER BY builder_id, recorded_at, id
|
|
76
|
+
ON CONFLICT DO NOTHING;
|
|
77
|
+
|
|
78
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
79
|
+
SELECT DISTINCT ON (builder_id) builder_id, 'first-full-idea', recorded_at, task_id
|
|
80
|
+
FROM credit_log
|
|
81
|
+
WHERE reason LIKE 'idea.credit.full\_pass:%' AND delta > 0
|
|
82
|
+
ORDER BY builder_id, recorded_at, id
|
|
83
|
+
ON CONFLICT DO NOTHING;
|
|
84
|
+
|
|
85
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
86
|
+
SELECT DISTINCT ON (builder_id) builder_id, 'first-board-ratification', recorded_at, task_id
|
|
87
|
+
FROM credit_log
|
|
88
|
+
WHERE reason LIKE 'idea.credit.governance\_pass:%' AND delta > 0
|
|
89
|
+
ORDER BY builder_id, recorded_at, id
|
|
90
|
+
ON CONFLICT DO NOTHING;
|
|
91
|
+
|
|
92
|
+
-- ten distinct shipped tasks paid under ONE idea (reason idea.credit.task_ship:<idea>:<task>);
|
|
93
|
+
-- stamped at the tenth task's payment
|
|
94
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
95
|
+
SELECT DISTINCT ON (builder_id) builder_id, 'thought-built-ten', recorded_at, task_id
|
|
96
|
+
FROM (
|
|
97
|
+
SELECT builder_id, recorded_at, task_id, id,
|
|
98
|
+
DENSE_RANK() OVER (PARTITION BY builder_id, split_part(reason, ':', 2)
|
|
99
|
+
ORDER BY first_paid) AS nth
|
|
100
|
+
FROM (
|
|
101
|
+
SELECT builder_id, recorded_at, task_id, id, reason,
|
|
102
|
+
MIN(recorded_at) OVER (PARTITION BY builder_id, split_part(reason, ':', 2), split_part(reason, ':', 3)) AS first_paid
|
|
103
|
+
FROM credit_log
|
|
104
|
+
WHERE reason LIKE 'idea.credit.task\_ship:%' AND delta > 0
|
|
105
|
+
) per_task
|
|
106
|
+
) ranked
|
|
107
|
+
WHERE nth >= 10
|
|
108
|
+
ORDER BY builder_id, recorded_at, id
|
|
109
|
+
ON CONFLICT DO NOTHING;
|
|
110
|
+
|
|
111
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
112
|
+
SELECT DISTINCT ON (builder_id) builder_id, 'first-page-approved', recorded_at, task_id
|
|
113
|
+
FROM credit_log
|
|
114
|
+
WHERE reason LIKE 'tweak.page\_approved:%' AND delta > 0
|
|
115
|
+
ORDER BY builder_id, recorded_at, id
|
|
116
|
+
ON CONFLICT DO NOTHING;
|
|
117
|
+
|
|
118
|
+
-- the tenth approved page (one row per round, keyed on its task)
|
|
119
|
+
INSERT INTO builder_achievements (builder_id, achievement_id, unlocked_at, unlocked_by_task_id)
|
|
120
|
+
SELECT builder_id, 'ten-pages-approved', recorded_at, task_id
|
|
121
|
+
FROM (
|
|
122
|
+
SELECT builder_id, recorded_at, task_id,
|
|
123
|
+
ROW_NUMBER() OVER (PARTITION BY builder_id ORDER BY recorded_at, id) AS nth
|
|
124
|
+
FROM credit_log
|
|
125
|
+
WHERE reason LIKE 'tweak.page\_approved:%' AND delta > 0
|
|
126
|
+
) pages
|
|
127
|
+
WHERE nth = 10
|
|
128
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -41,7 +41,7 @@ const streak = require('./streak');
|
|
|
41
41
|
// getLockedAchievementsProgress/renderProgressText), moved from src/bongos/db.js,
|
|
42
42
|
// reads the shared pool through the published doorway — a module never requires a
|
|
43
43
|
// core internal. The evaluators above take a tx `client` from the caller.
|
|
44
|
-
const { pool } = require('../../src/module-api');
|
|
44
|
+
const { pool, resolveOptional } = require('../../src/module-api');
|
|
45
45
|
|
|
46
46
|
async function alreadyUnlocked(client, builderId, achievementId) {
|
|
47
47
|
const r = await client.query(
|
|
@@ -367,6 +367,88 @@ async function eligible_streak7(client, builderId) {
|
|
|
367
367
|
return s.longest_streak >= 7;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// ---- the craft achievements (task 1004438 / WA6.RP08) --------------------------
|
|
371
|
+
// An artist and an ideator earn on the LEDGER, not on a claim: each predicate
|
|
372
|
+
// below is a fact credit_log already holds (the reason prefixes are economy's own,
|
|
373
|
+
// the same strings the writers book and migrations/core_270 backfills from). None
|
|
374
|
+
// needs a ship of the builder's own; reconcileAfterCraftCredit (below) runs them
|
|
375
|
+
// the moment such a credit is booked.
|
|
376
|
+
const IDEA_SHIP_LIKE = 'idea.credit.task\\_ship:%';
|
|
377
|
+
const IDEA_FULL_LIKE = 'idea.credit.full\\_pass:%';
|
|
378
|
+
const IDEA_BOARD_LIKE = 'idea.credit.governance\\_pass:%';
|
|
379
|
+
const PAGE_LIKE = 'tweak.page\\_approved:%';
|
|
380
|
+
const THOUGHT_TASKS = 10;
|
|
381
|
+
const PAGES_MANY = 10;
|
|
382
|
+
|
|
383
|
+
//
|
|
384
|
+
// ONE READ FOR ALL SEVEN. craftFacts asks the builder's craft ledger once —
|
|
385
|
+
// served by idx_credit_log_builder (builder_id, recorded_at), so it is that
|
|
386
|
+
// person's rows, never the whole ledger — and every craft rule is a pure test of
|
|
387
|
+
// its answer. The full reconcile and the craft-only one read the SAME facts
|
|
388
|
+
// through the SAME rules (CRAFT_RULES), so the two paths cannot disagree about
|
|
389
|
+
// who earned what. Within one reconcile the facts are read once and shared.
|
|
390
|
+
const CRAFT_FACTS_SQL = `
|
|
391
|
+
SELECT bool_or(reason LIKE $2) AS ignited,
|
|
392
|
+
bool_or(reason LIKE $3) AS full_idea,
|
|
393
|
+
bool_or(reason LIKE $4) AS ratified,
|
|
394
|
+
COUNT(*) FILTER (WHERE reason LIKE $5)::int AS pages,
|
|
395
|
+
(SELECT COALESCE(MAX(n), 0)::int FROM (
|
|
396
|
+
SELECT COUNT(DISTINCT split_part(reason, ':', 3)) AS n
|
|
397
|
+
FROM credit_log
|
|
398
|
+
WHERE builder_id = $1 AND reason LIKE $2 AND delta > 0
|
|
399
|
+
GROUP BY split_part(reason, ':', 2)) per_thought) AS best_thought_tasks
|
|
400
|
+
FROM credit_log
|
|
401
|
+
WHERE builder_id = $1 AND delta > 0`;
|
|
402
|
+
|
|
403
|
+
async function craftFacts(client, builderId) {
|
|
404
|
+
const r = await client.query(CRAFT_FACTS_SQL, [builderId, IDEA_SHIP_LIKE, IDEA_FULL_LIKE, IDEA_BOARD_LIKE, PAGE_LIKE]);
|
|
405
|
+
const row = r.rows[0] || {};
|
|
406
|
+
return {
|
|
407
|
+
ignited: row.ignited === true,
|
|
408
|
+
fullIdea: row.full_idea === true,
|
|
409
|
+
ratified: row.ratified === true,
|
|
410
|
+
pages: Number(row.pages) || 0,
|
|
411
|
+
bestThoughtTasks: Number(row.best_thought_tasks) || 0,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Every page of one surface rewritten by this artist and approved: the studio's
|
|
416
|
+
// own "N of M tweaked" per surface (copy-desk's `copy-desk.artistStats` port, the
|
|
417
|
+
// one the profile header reads), so the inventory has one reader. Asked ONLY of a
|
|
418
|
+
// builder with an approved page — anyone else structurally cannot earn it, and
|
|
419
|
+
// the port is not free. A port that is absent or fails is "could not tell", never
|
|
420
|
+
// a badge.
|
|
421
|
+
async function wholeSurfaceTweaked(builderId, facts) {
|
|
422
|
+
if (!(facts.pages > 0)) return false;
|
|
423
|
+
const port = resolveOptional('copy-desk.artistStats');
|
|
424
|
+
if (typeof port !== 'function') return false;
|
|
425
|
+
const stats = await port(builderId);
|
|
426
|
+
const surfaces = (stats && Array.isArray(stats.surfaces)) ? stats.surfaces : [];
|
|
427
|
+
return surfaces.some((s) => Number(s.total) > 0 && Number(s.tweaked) >= Number(s.total));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// id → (facts, builderId) → boolean | Promise<boolean>, in sort order.
|
|
431
|
+
const CRAFT_RULES = [
|
|
432
|
+
['first-thought-ignited', (f) => f.ignited],
|
|
433
|
+
['first-full-idea', (f) => f.fullIdea],
|
|
434
|
+
['first-board-ratification', (f) => f.ratified],
|
|
435
|
+
// one thought, ten shipped tasks: stream 1's reason is idea.credit.task_ship:<idea>:<task>
|
|
436
|
+
['thought-built-ten', (f) => f.bestThoughtTasks >= THOUGHT_TASKS],
|
|
437
|
+
['first-page-approved', (f) => f.pages > 0],
|
|
438
|
+
['ten-pages-approved', (f) => f.pages >= PAGES_MANY],
|
|
439
|
+
['whole-surface-tweaked', (f, builderId) => wholeSurfaceTweaked(builderId, f)],
|
|
440
|
+
];
|
|
441
|
+
const CRAFT_IDS = CRAFT_RULES.map(([id]) => id);
|
|
442
|
+
|
|
443
|
+
// The STATE_EVALUATORS form of a rule: read the facts once per reconcile (the
|
|
444
|
+
// shared `ctx`), then test. diagnose-achievements.js reaches the rules this way.
|
|
445
|
+
function craftEvaluator(rule) {
|
|
446
|
+
return async (client, builderId, ctx = {}) => {
|
|
447
|
+
if (!ctx.craftFacts) ctx.craftFacts = craftFacts(client, builderId);
|
|
448
|
+
return rule(await ctx.craftFacts, builderId);
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
370
452
|
// Ordered by achievements.sort_order so banners/backfill output read naturally.
|
|
371
453
|
const STATE_EVALUATORS = [
|
|
372
454
|
{ id: 'first-ship', fn: eligible_firstShip },
|
|
@@ -378,6 +460,7 @@ const STATE_EVALUATORS = [
|
|
|
378
460
|
{ id: 'subtractor', fn: eligible_subtractor },
|
|
379
461
|
{ id: 'gds-shipper', fn: eligible_gdsShipper },
|
|
380
462
|
{ id: 'breaking-the-100c-bar', fn: eligible_breaking100c },
|
|
463
|
+
...CRAFT_RULES.map(([id, rule]) => ({ id, fn: craftEvaluator(rule) })),
|
|
381
464
|
];
|
|
382
465
|
|
|
383
466
|
// Unlock every achievement the builder currently qualifies for but hasn't yet.
|
|
@@ -387,9 +470,10 @@ const STATE_EVALUATORS = [
|
|
|
387
470
|
async function reconcileBuilderAchievements(client, builderId, opts = {}) {
|
|
388
471
|
const { taskId = null } = opts;
|
|
389
472
|
const results = [];
|
|
473
|
+
const ctx = {}; // shared by the craft rules: their facts are read once per reconcile
|
|
390
474
|
for (const ev of STATE_EVALUATORS) {
|
|
391
475
|
if (await alreadyUnlocked(client, builderId, ev.id)) continue;
|
|
392
|
-
if (await ev.fn(client, builderId)) {
|
|
476
|
+
if (await ev.fn(client, builderId, ctx)) {
|
|
393
477
|
const unlocked = await unlockAchievement(client, builderId, ev.id, taskId);
|
|
394
478
|
if (unlocked) results.push(unlocked);
|
|
395
479
|
}
|
|
@@ -397,6 +481,83 @@ async function reconcileBuilderAchievements(client, builderId, opts = {}) {
|
|
|
397
481
|
return results;
|
|
398
482
|
}
|
|
399
483
|
|
|
484
|
+
// reconcileCraftAchievements — ONLY the seven craft rules, in two reads: what the
|
|
485
|
+
// builder already holds of them, then (only if anything is left) their craft
|
|
486
|
+
// facts. The payout hook below runs this, not the full sixteen-evaluator
|
|
487
|
+
// reconcile: an idea credit or an approved page cannot change a streak, a claim
|
|
488
|
+
// count or a shipped diff, so asking about those on every payment is waste.
|
|
489
|
+
async function reconcileCraftAchievements(client, builderId, opts = {}) {
|
|
490
|
+
const { taskId = null } = opts;
|
|
491
|
+
const held = await client.query(
|
|
492
|
+
'SELECT achievement_id FROM builder_achievements WHERE builder_id = $1 AND achievement_id = ANY($2::text[])',
|
|
493
|
+
[builderId, CRAFT_IDS]
|
|
494
|
+
);
|
|
495
|
+
const have = new Set((held.rows || []).map((r) => r.achievement_id));
|
|
496
|
+
const open = CRAFT_RULES.filter(([id]) => !have.has(id));
|
|
497
|
+
if (!open.length) return [];
|
|
498
|
+
const facts = await craftFacts(client, builderId);
|
|
499
|
+
const results = [];
|
|
500
|
+
for (const [id, rule] of open) {
|
|
501
|
+
if (await rule(facts, builderId)) {
|
|
502
|
+
const unlocked = await unlockAchievement(client, builderId, id, taskId);
|
|
503
|
+
if (unlocked) results.push(unlocked);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return results;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// backfillCraftAchievements — the part of the backfill a migration cannot do.
|
|
510
|
+
// migrations/core_270 backfills six badges from the ledger in SQL, but "A Whole
|
|
511
|
+
// Surface" needs the page inventory, which is a file the database never sees. So
|
|
512
|
+
// the lifecycle's periodic sweep (publish-reconciler.js) runs this ONCE per
|
|
513
|
+
// process start: every builder the craft ledger has paid, through the craft-only
|
|
514
|
+
// reconcile, in id order, bounded. Idempotent, so a restart that runs it again
|
|
515
|
+
// unlocks nothing twice. Returns { checked, unlocked }.
|
|
516
|
+
async function backfillCraftAchievements({ client = null, limit = 500 } = {}) {
|
|
517
|
+
const q = client || pool;
|
|
518
|
+
const { rows } = await q.query(
|
|
519
|
+
`SELECT DISTINCT builder_id FROM credit_log
|
|
520
|
+
WHERE delta > 0 AND (reason LIKE $1 OR reason LIKE $2 OR reason LIKE $3 OR reason LIKE $4)
|
|
521
|
+
ORDER BY builder_id
|
|
522
|
+
LIMIT $5`,
|
|
523
|
+
[IDEA_SHIP_LIKE, IDEA_FULL_LIKE, IDEA_BOARD_LIKE, PAGE_LIKE, limit]
|
|
524
|
+
);
|
|
525
|
+
let unlocked = 0;
|
|
526
|
+
for (const r of rows) {
|
|
527
|
+
const got = await reconcileAfterCraftCredit(q, r.builder_id);
|
|
528
|
+
unlocked += got.length;
|
|
529
|
+
}
|
|
530
|
+
return { checked: rows.length, unlocked };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// reconcileAfterCraftCredit — the craft writers' hook (task 1004438). An idea
|
|
534
|
+
// stream or an approved page pays a builder inside SOMEONE ELSE'S ship (or an
|
|
535
|
+
// idea route's own read), so the ship-time reconcile — which runs for the shipper
|
|
536
|
+
// — never reaches the ideator or the artist. credits.js calls this for each payee
|
|
537
|
+
// it just booked. It can never fail the payment: on a transaction client it runs
|
|
538
|
+
// under a SAVEPOINT and rolls back to it on any error; on a pool (an idea route
|
|
539
|
+
// pays through the pool, one idempotent statement) SAVEPOINT itself fails, nothing
|
|
540
|
+
// is open to poison, and the reconcile runs plainly inside a try. Returns the
|
|
541
|
+
// just-unlocked list, or [] on any failure.
|
|
542
|
+
async function reconcileAfterCraftCredit(client, builderId, opts = {}) {
|
|
543
|
+
if (builderId == null || !client || typeof client.query !== 'function') return [];
|
|
544
|
+
let saved = false;
|
|
545
|
+
try {
|
|
546
|
+
await client.query('SAVEPOINT ach_craft_credit');
|
|
547
|
+
saved = true;
|
|
548
|
+
} catch (_) { /* not in a transaction: a pool */ }
|
|
549
|
+
try {
|
|
550
|
+
const out = await reconcileCraftAchievements(client, builderId, opts);
|
|
551
|
+
if (saved) await client.query('RELEASE SAVEPOINT ach_craft_credit');
|
|
552
|
+
return out;
|
|
553
|
+
} catch (_) {
|
|
554
|
+
if (saved) {
|
|
555
|
+
try { await client.query('ROLLBACK TO SAVEPOINT ach_craft_credit'); } catch (_e) { /* the caller's tx decides */ }
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
400
561
|
// -------------------------------------------------------------------------
|
|
401
562
|
// Achievements — minimal getters added by migration 006_pms_v2.
|
|
402
563
|
// Full evaluator + unlock writes live in src/bongos/achievements.js (task K1).
|
|
@@ -436,6 +597,10 @@ async function getLeaderboardWithAchievements(limit = 10, perBuilderCap = 5) {
|
|
|
436
597
|
b.avatar_url,
|
|
437
598
|
b.total_credits,
|
|
438
599
|
b.rank,
|
|
600
|
+
-- task 1004437: karma too, as credits.leaderboard carries it, so the
|
|
601
|
+
-- hall's roster can read this one public list (with its roles) and
|
|
602
|
+
-- keep its karma column and lens. The profile already publishes it.
|
|
603
|
+
b.karma,
|
|
439
604
|
COALESCE((
|
|
440
605
|
SELECT array_agg(t.achievement_id ORDER BY t.sort_order)
|
|
441
606
|
FROM (
|
|
@@ -504,7 +669,14 @@ async function getLockedAchievementsProgress(builderId) {
|
|
|
504
669
|
AND a.outcome = 'shipped' AND a.released_at IS NOT NULL
|
|
505
670
|
AND b2.outcome = 'shipped' AND b2.released_at IS NOT NULL
|
|
506
671
|
AND tstzrange(a.claimed_at, a.released_at)
|
|
507
|
-
&& tstzrange(b2.claimed_at, b2.released_at)) AS overlap_pairs
|
|
672
|
+
&& tstzrange(b2.claimed_at, b2.released_at)) AS overlap_pairs,
|
|
673
|
+
-- task 1004438: the craft counters, from the ledger
|
|
674
|
+
(SELECT COUNT(*) FROM credit_log
|
|
675
|
+
WHERE builder_id = $1 AND reason LIKE 'tweak.page\\_approved:%' AND delta > 0) AS pages_approved,
|
|
676
|
+
(SELECT COALESCE(MAX(n), 0) FROM (
|
|
677
|
+
SELECT COUNT(DISTINCT split_part(reason, ':', 3)) AS n FROM credit_log
|
|
678
|
+
WHERE builder_id = $1 AND reason LIKE 'idea.credit.task\\_ship:%' AND delta > 0
|
|
679
|
+
GROUP BY split_part(reason, ':', 2)) t) AS best_thought_tasks
|
|
508
680
|
)
|
|
509
681
|
SELECT a.id AS achievement_id,
|
|
510
682
|
a.name,
|
|
@@ -516,7 +688,9 @@ async function getLockedAchievementsProgress(builderId) {
|
|
|
516
688
|
c.art_ships,
|
|
517
689
|
c.pms_ships,
|
|
518
690
|
c.total_credits,
|
|
519
|
-
c.overlap_pairs
|
|
691
|
+
c.overlap_pairs,
|
|
692
|
+
c.pages_approved,
|
|
693
|
+
c.best_thought_tasks
|
|
520
694
|
FROM achievements a
|
|
521
695
|
CROSS JOIN counters c
|
|
522
696
|
WHERE NOT EXISTS (
|
|
@@ -594,6 +768,25 @@ function renderProgressText(row) {
|
|
|
594
768
|
return artShips === 0
|
|
595
769
|
? 'needs one shipped task touching art/'
|
|
596
770
|
: 'keep building';
|
|
771
|
+
// task 1004438: the craft achievements
|
|
772
|
+
case 'first-thought-ignited':
|
|
773
|
+
return 'needs one thought of yours to become shipped work';
|
|
774
|
+
case 'first-full-idea':
|
|
775
|
+
return 'needs one Full Idea to pass the completeness bar';
|
|
776
|
+
case 'first-board-ratification':
|
|
777
|
+
return 'needs the board to ratify one idea of yours';
|
|
778
|
+
case 'thought-built-ten': {
|
|
779
|
+
const best = Math.max(0, Math.min(Number(row.best_thought_tasks || 0), 10));
|
|
780
|
+
return `${best} of 10 shipped tasks from one thought`;
|
|
781
|
+
}
|
|
782
|
+
case 'first-page-approved':
|
|
783
|
+
return 'needs one page you rewrote to be approved';
|
|
784
|
+
case 'ten-pages-approved': {
|
|
785
|
+
const pages = Math.max(0, Math.min(Number(row.pages_approved || 0), 10));
|
|
786
|
+
return `${pages} of 10 pages approved`;
|
|
787
|
+
}
|
|
788
|
+
case 'whole-surface-tweaked':
|
|
789
|
+
return 'needs every page of one surface rewritten by you and approved';
|
|
597
790
|
case 'gds-shipper':
|
|
598
791
|
return pmsShips === 0
|
|
599
792
|
? 'needs one shipped task touching src/bongos/ or scripts/gds/'
|
|
@@ -612,6 +805,8 @@ module.exports = {
|
|
|
612
805
|
evaluateAfterShip,
|
|
613
806
|
evaluateAfterCreditAward,
|
|
614
807
|
reconcileBuilderAchievements,
|
|
808
|
+
reconcileAfterCraftCredit,
|
|
809
|
+
backfillCraftAchievements,
|
|
615
810
|
STATE_EVALUATORS,
|
|
616
811
|
alreadyUnlocked,
|
|
617
812
|
unlockAchievement,
|