@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.
@@ -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":4595,"placed":1619,"shared":245,"unplaced":2731,"omitted_data":2272},
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": "e1f443b10a615ee9",
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":599,"string_id":"0658f0358ffd"},
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":580,"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":580,"string_id":"0279db21e29a","state":"standing"},
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":629,"string_id":"11a1905c76b2"},
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":629,"string_id":"f03d68e60416"},
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":707,"string_id":"d32c5e058d1e"},
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":201,"string_id":"fe9986a9d821"},
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":1180,"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":1145,"string_id":"e3b473af2f69","state":"standing"},
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":1258,"string_id":"7d6eaa8d4514","state":"standing"},
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": "df9d73fa272f78f0",
2495
- "reading_hash": "e96fecce27a9bed3",
2496
- "counts": {"lines":28,"placed":8,"shared":5,"unplaced":15},
2497
- "omitted": {"data":40},
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 roster13","text":"The roster","placement":"placed","file":"modules/hall-ui/public/roster.html","line":36,"string_id":"38ce5d68c182"},
2507
- {"key":"L0008","section":"The roster13","text":"Search","placement":"unplaced"},
2508
- {"key":"L0009","section":"The roster13","text":"name or login","placement":"unplaced","attr":"placeholder"},
2509
- {"key":"L0010","section":"The roster13","text":"Sort","placement":"unplaced"},
2510
- {"key":"L0011","section":"The roster13","text":"place","placement":"unplaced"},
2511
- {"key":"L0012","section":"The roster13","text":"rank","placement":"unplaced"},
2512
- {"key":"L0013","section":"The roster13","text":"most karma","placement":"unplaced"},
2513
- {"key":"L0014","section":"The roster13","text":"{…} tokens","placement":"placed","file":"modules/hall-ui/public/sessions.js","line":170,"string_id":"955499559f0a"},
2514
- {"key":"L0015","section":"The roster13","text":"Place","placement":"unplaced"},
2515
- {"key":"L0016","section":"The roster13","text":"Builder","placement":"unplaced"},
2516
- {"key":"L0017","section":"The roster13","text":"Rank","placement":"unplaced"},
2517
- {"key":"L0018","section":"The roster13","text":"Credits","placement":"unplaced"},
2518
- {"key":"L0019","section":"The roster13","text":"Karma","placement":"unplaced"},
2519
- {"key":"L0020","section":"The roster13","text":"Tokens","placement":"unplaced"},
2520
- {"key":"L0021","section":"The roster13","text":"No builder on the roster matches that search.","placement":"unplaced","state":"search"},
2521
- {"key":"L0022","section":"The roster13","text":"B","placement":"unplaced","occurrences":2},
2522
- {"key":"L0023","section":"The roster13","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9"},
2523
- {"key":"L0024","section":"The roster13","text":"Per page","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"4a46b478edaf"},
2524
- {"key":"L0025","section":"The roster13","text":"‹ Prev","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"de8fa5223aee"},
2525
- {"key":"L0026","section":"The roster13","text":"Page {…} of {…}","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":112,"string_id":"15a49bf15007"},
2526
- {"key":"L0027","section":"The roster13","text":"Next ›","placement":"placed","file":"modules/hall-ui/public/hall-kit.js","line":74,"string_id":"f52ba261d930"},
2527
- {"key":"L0028","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":471,"string_id":"1883ad7f07fc"}
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": "35acac6a3253f014",
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":1169,"string_id":"c1e3c2212207"},
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,