@bongos/core 1.20.52 → 1.20.53

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.20.52",
6
- "core_contract": "1.20.52",
7
- "source_commit": "e3143859f3882b836b0c86f59b126cb27cb8209d",
5
+ "core_version": "1.20.53",
6
+ "core_contract": "1.20.53",
7
+ "source_commit": "e6a11760630775fdd1246101b07f794fb3b2f572",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-01T04:55:11.621Z",
9
+ "built_at": "2026-10-01T05:09:33.431Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 569,
13
13
  "agent_docs_stubbed": 27,
14
- "functional_verbatim": 2732,
14
+ "functional_verbatim": 2735,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3329,
20
- "tree_sha256": "bc0888acafd3809d66ad4ad330f38258ff34918aef78a22ff590a15610cf8b15",
19
+ "file_count": 3332,
20
+ "tree_sha256": "c7ff8f3b580552be044ad09a0daa7676208ba3e7d268257f450ec96d2854be53",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2287,12 +2287,12 @@
2287
2287
  {
2288
2288
  "path": "docs/copy-inventory.md",
2289
2289
  "mode": "0000644",
2290
- "sha256": "b9a5cde749ecf4d1471688995410002faa73d1dab0bd06325129044502170774"
2290
+ "sha256": "f247a138ac562b034c51d465828c190c1fc666a581356692e57ac8e50068f1a9"
2291
2291
  },
2292
2292
  {
2293
2293
  "path": "docs/copy-registry.json",
2294
2294
  "mode": "0000644",
2295
- "sha256": "2ae3b36beb2428b1c8f7c313116ec9930a0fcd88754969eaac4c7744d46d8446"
2295
+ "sha256": "8d902cd0ae2116a2c6b6a6f989ccb9c2f37cd55f0e5a21213e21e5537842cd00"
2296
2296
  },
2297
2297
  {
2298
2298
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2797,7 +2797,7 @@
2797
2797
  {
2798
2798
  "path": "docs/module-api-changelog.md",
2799
2799
  "mode": "0000644",
2800
- "sha256": "86c99fc203ccb2c42644e11910bc8f47b85f11a8bd17dd7bd8d5047abd61bd94"
2800
+ "sha256": "0f4ca228b39781de497fde96d2a682991fc015240de825e682b1cf04d7bb6916"
2801
2801
  },
2802
2802
  {
2803
2803
  "path": "docs/modules-contract.md",
@@ -2887,7 +2887,7 @@
2887
2887
  {
2888
2888
  "path": "docs/page-readings.json",
2889
2889
  "mode": "0000644",
2890
- "sha256": "8e520dffc4d00a1cc4734aa4dd207e28c3ed5349735d75a98a17e6453529f359"
2890
+ "sha256": "1c368f143a3353ccf77f647dfd04ed0dd51fee212693c1674ff892b99694c339"
2891
2891
  },
2892
2892
  {
2893
2893
  "path": "docs/project-context.template.md",
@@ -3984,6 +3984,11 @@
3984
3984
  "mode": "0000644",
3985
3985
  "sha256": "e7dcf016492ab9d011bd0021ac866edd502bf68f2fe06a04365ecb2b49ccbfaa"
3986
3986
  },
3987
+ {
3988
+ "path": "migrations/core_267_page_tweak_author_of_record.sql",
3989
+ "mode": "0000644",
3990
+ "sha256": "09070e79b3e4781a185263d1bb2be364dd1225dcf6f79bdf9c21cc498fb009dd"
3991
+ },
3987
3992
  {
3988
3993
  "path": "modules/agents/CLAUDE.md",
3989
3994
  "mode": "0000644",
@@ -5182,7 +5187,7 @@
5182
5187
  {
5183
5188
  "path": "modules/hall-ui/public/builders.js",
5184
5189
  "mode": "0000644",
5185
- "sha256": "2dc6e44e447a83abc9c8c62d44cd284140ee8e85b5d40f0e432956abbbef1107"
5190
+ "sha256": "565573d6b3a055e9ae18751c954b3d0d7da0383fe0e37c17a24edfec9529c69f"
5186
5191
  },
5187
5192
  {
5188
5193
  "path": "modules/hall-ui/public/bulk-toolbar.js",
@@ -6377,7 +6382,7 @@
6377
6382
  {
6378
6383
  "path": "modules/lifecycle/db-analytics.js",
6379
6384
  "mode": "0000644",
6380
- "sha256": "8b2141ba76f276cbf279a5686a582a87ab70e3a7800861b01a39c96d3845d643"
6385
+ "sha256": "8e8aedb44b8773760ebe0ea87b8c4f0aa98b838a69be6ae1a40c3c2e7e28b656"
6381
6386
  },
6382
6387
  {
6383
6388
  "path": "modules/lifecycle/db-claim-reads.js",
@@ -6432,7 +6437,7 @@
6432
6437
  {
6433
6438
  "path": "modules/lifecycle/db-ship.js",
6434
6439
  "mode": "0000644",
6435
- "sha256": "094fbbfce3de775b0ef616ee57a4d8c0bbd68b35cfb74d683650ad6c2066c515"
6440
+ "sha256": "1b2c55becb653ff92eea3bc202bdef855b0b46c2cdb247a2e738eb5b626d2c22"
6436
6441
  },
6437
6442
  {
6438
6443
  "path": "modules/lifecycle/db-tasks.js",
@@ -6669,6 +6674,11 @@
6669
6674
  "mode": "0000644",
6670
6675
  "sha256": "ffd912f5d88e729f7d6cb86235737788b62deebde24551a21e349d164f24d0cf"
6671
6676
  },
6677
+ {
6678
+ "path": "modules/lifecycle/page-tweak-artist.js",
6679
+ "mode": "0000644",
6680
+ "sha256": "78c704a21d784de68b61826cca9f1faa1a30065c326746b4d75c80feed7bddda"
6681
+ },
6672
6682
  {
6673
6683
  "path": "modules/lifecycle/page-tweak-claim.js",
6674
6684
  "mode": "0000644",
@@ -6682,7 +6692,7 @@
6682
6692
  {
6683
6693
  "path": "modules/lifecycle/page-tweak-reads.js",
6684
6694
  "mode": "0000644",
6685
- "sha256": "6301f5c7bda0da7fcea7b08fd9680275e08fe3ca99d802377995b5ad408bc704"
6695
+ "sha256": "7606154d0aa95475e0cd59b753d38bd00c2ce65928d843191738dcf9bdd93f72"
6686
6696
  },
6687
6697
  {
6688
6698
  "path": "modules/lifecycle/page-tweak-write.js",
@@ -6847,7 +6857,7 @@
6847
6857
  {
6848
6858
  "path": "modules/lifecycle/routes/tasks.js",
6849
6859
  "mode": "0000644",
6850
- "sha256": "de2d4b6fb05237a0c480b80a0d636033cd8263dce1ece1b35865e92a0f64451b"
6860
+ "sha256": "4019166678703aee51179d0b92a5aaefa728a5f992b7b472703e61c1e55c30f7"
6851
6861
  },
6852
6862
  {
6853
6863
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -8822,7 +8832,7 @@
8822
8832
  {
8823
8833
  "path": "modules/sessions/db.js",
8824
8834
  "mode": "0000644",
8825
- "sha256": "f29e4a36b163b7d7e1fbbf5b36daa963325919dd55fe1a2f7654fee6095dc4b7"
8835
+ "sha256": "3957caae2ab5028096f74428503db5915ff72eb945bb965b518a0be2791b9e57"
8826
8836
  },
8827
8837
  {
8828
8838
  "path": "modules/sessions/module.json",
@@ -8952,7 +8962,7 @@
8952
8962
  {
8953
8963
  "path": "modules/status-ui/public/status.js",
8954
8964
  "mode": "0000644",
8955
- "sha256": "70cdf97bbd268e3dd74ce504f48a07b73d1ed91e079e1c96e32e7bd7e4f1008b"
8965
+ "sha256": "1a056ec1ddd690dc41057193fe18af6dda6cdf2ad211875b198f945ff1ca5317"
8956
8966
  },
8957
8967
  {
8958
8968
  "path": "modules/status-ui/public/style.css",
@@ -9227,12 +9237,12 @@
9227
9237
  {
9228
9238
  "path": "package-lock.json",
9229
9239
  "mode": "0000644",
9230
- "sha256": "d098937dc15b84158548b81a92be4a66c53852088099d5ecf18d8412923f52de"
9240
+ "sha256": "7e6d09d690c1d39dd1ad215ac5161f039e5d4bee561faa5b4fc4e3cd3246b847"
9231
9241
  },
9232
9242
  {
9233
9243
  "path": "package.json",
9234
9244
  "mode": "0000644",
9235
- "sha256": "2823bd18e053bf6c233177c8b96fd56ec5018e4c2d85c310c1320ecb287f77de"
9245
+ "sha256": "8c63174a9497687918aa75f1029fe8f5fe945dc2441ed5683839b5966c03041f"
9236
9246
  },
9237
9247
  {
9238
9248
  "path": "public-docs/index.html",
@@ -9252,7 +9262,7 @@
9252
9262
  {
9253
9263
  "path": "release-notes.json",
9254
9264
  "mode": "0000644",
9255
- "sha256": "2f0bc73b1cab48056c8c76bb5f14dbd8d6f85549aa6f11c2478371033f5715d4"
9265
+ "sha256": "5792ec50e5a46bb72a99fdb11e03dd90b73020cab737bc19f5edc2b088266b4d"
9256
9266
  },
9257
9267
  {
9258
9268
  "path": "scripts/bongos-mcp.js",
@@ -11417,7 +11427,7 @@
11417
11427
  {
11418
11428
  "path": "src/module-api.js",
11419
11429
  "mode": "0000644",
11420
- "sha256": "be64d41f259aa175bb0fc4e971e38af38a86b5fef5b1c0a700fe6106dd40c04f"
11430
+ "sha256": "1d38bd032ec252b7c257b5fa60375363d18c4158d12f641913e2f2d0d824da6c"
11421
11431
  },
11422
11432
  {
11423
11433
  "path": "src/module-loader/catalog.js",
@@ -14744,6 +14754,11 @@
14744
14754
  "mode": "0000644",
14745
14755
  "sha256": "2d86f24827327f8015feec80de6fb1089d7f5d5589847a1c0cf4fad59412d307"
14746
14756
  },
14757
+ {
14758
+ "path": "tests/page_tweak_artist.mjs",
14759
+ "mode": "0000644",
14760
+ "sha256": "bd2127c6133c89d4a6aaa761f7aa70bc4cc01c0e29560b01ed70f85bde667ea6"
14761
+ },
14747
14762
  {
14748
14763
  "path": "tests/page_tweak_hold.mjs",
14749
14764
  "mode": "0000644",
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **2278 strings** across 3 surfaces · 1962 distinct pieces of copy · 2278 certain, 0 heuristic · 220 shown in more than one place.
7
+ **2279 strings** across 3 surfaces · 1963 distinct pieces of copy · 2279 certain, 0 heuristic · 220 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 114 | 1687 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 114 | 1688 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 6 | 491 |
16
16
 
@@ -63,7 +63,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
63
63
  | `08afb1257b71` | 3x | Back to home | modules/hall-ui/public/drachmae.html:87, modules/hall-ui/public/primer.html:90, modules/hall-ui/public/primer.js:282 |
64
64
  | `118026595c16` | 3x | Board Room | modules/hall-ui/public/board-room.html:19, modules/hall-ui/public/board-room.html:19, modules/hall-ui/public/board-room.html:31 |
65
65
  | `a78477a24a48` | 3x | Builders' Hall | modules/hall-ui/public/index.html:12, modules/hall-ui/public/index.html:12, modules/hall-ui/public/index.html:49 |
66
- | `c10806b50c61` | 3x | Claim task {…} | modules/hall-ui/public/builders.js:971, modules/hall-ui/public/goals-render.js:366, modules/hall-ui/public/work.js:482 |
66
+ | `c10806b50c61` | 3x | Claim task {…} | modules/hall-ui/public/builders.js:973, modules/hall-ui/public/goals-render.js:366, modules/hall-ui/public/work.js:482 |
67
67
  | `12ee050d0180` | 3x | Copy desk | modules/hall-ui/public/copy-desk.html:15, modules/hall-ui/public/copy-desk.html:15, modules/hall-ui/public/copy-desk.html:27 |
68
68
  | `a873e7c29d80` | 3x | Copy prompt | modules/hall-ui/public/task.js:818, modules/hall-ui/public/task.js:840, modules/hall-ui/public/work.js:615 |
69
69
  | `9c728558d6e7` | 3x | Drachmae & karma | modules/hall-ui/public/drachmae.html:22, modules/hall-ui/public/drachmae.html:22, modules/hall-ui/public/drachmae.html:37 |
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `4e25e6872321` | 3x | Your projects | modules/public-landing/public/index.html:691, modules/public-landing/public/projects.html:547, modules/public-landing/public/projects.html:709 |
79
79
  | … | | _180 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1687 strings
81
+ ## `builders-hall` — 1688 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -147,7 +147,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
147
147
  | `daf73127afff` | certain | A word on why | `modules/hall-ui/public/hall-render.js:297` |
148
148
  | `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:46` |
149
149
  | `996edffc1966` | certain | Access requests | `modules/hall-ui/public/watch.html:61` |
150
- | `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:1194` |
150
+ | `303f8d90266f` | certain | Achievements could not be loaded. | `modules/hall-ui/public/builders.js:1196` |
151
151
  | `6ca31dd5eb91` | certain | Acknowledge the primer | `modules/hall-ui/public/primer.html:99` |
152
152
  | `74cc57f76b42` | certain | Active recently | `modules/hall-ui/public/watch.js:458` |
153
153
  | `524188fe0078` | certain | Active sessions | `modules/hall-ui/public/settings.html:151` |
@@ -273,7 +273,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
273
273
  | `6c88cdf417f1` | certain | Choose which sound plays for each build event — one when Claude waits for your input, another when a task ship | `modules/hall-ui/public/settings.html:352` |
274
274
  | `b7d5648b66db` | certain | Claim it | `modules/hall-ui/public/task.js:731` |
275
275
  | `b7d5648b66db` | certain | Claim it | `modules/hall-ui/public/task.js:856` |
276
- | `c10806b50c61` | certain | Claim task {…} | `modules/hall-ui/public/builders.js:971` |
276
+ | `c10806b50c61` | certain | Claim task {…} | `modules/hall-ui/public/builders.js:973` |
277
277
  | `c10806b50c61` | certain | Claim task {…} | `modules/hall-ui/public/goals-render.js:366` |
278
278
  | `c10806b50c61` | certain | Claim task {…} | `modules/hall-ui/public/work.js:482` |
279
279
  | `84664050c372` | certain | Claimed by another builder | `modules/hall-ui/public/goals-render.js:372` |
@@ -423,7 +423,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
423
423
  | `3e561393f4e9` | certain | Display name | `modules/hall-ui/public/settings.html:77` |
424
424
  | `3e561393f4e9` | certain | Display name | `modules/hall-ui/public/settings.html:83` |
425
425
  | `5a95583f9bfa` | certain | Download Claude | `modules/hall-ui/public/hall-render.js:809` |
426
- | `9cf07334a5a9` | certain | Downvote (−1) | `modules/hall-ui/public/builders.js:775` |
426
+ | `9cf07334a5a9` | certain | Downvote (−1) | `modules/hall-ui/public/builders.js:777` |
427
427
  | `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:22` |
428
428
  | `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:22` |
429
429
  | `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:37` |
@@ -581,7 +581,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
581
581
  | `a4ff1e9c34e3` | certain | Idea {…} | `modules/hall-ui/public/idea.js:487` |
582
582
  | `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
583
583
  | `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
584
- | `e181637c5a5e` | certain | Ideas awaiting triage | `modules/hall-ui/public/builders.js:895` |
584
+ | `e181637c5a5e` | certain | Ideas awaiting triage | `modules/hall-ui/public/builders.js:897` |
585
585
  | `d4e21769ce1c` | certain | Ideas filed, and what became of them | `modules/hall-ui/public/profile.html:152` |
586
586
  | `379b20d548ad` | certain | Ideas whose author has said "I had the thought but not the full picture". Anyone can develop one — the author | `modules/hall-ui/public/ideas.html:127` |
587
587
  | `ef224c9de7c8` | certain | Ideas — {{worldName}} | `modules/hall-ui/public/ideas.html:6` |
@@ -684,7 +684,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
684
684
  | `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/profile.js:94` |
685
685
  | `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:489` |
686
686
  | `f3ba5b1b7849` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this project onto a newe | `modules/hall-ui/public/deploy.html:54` |
687
- | `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:779` |
687
+ | `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:781` |
688
688
  | `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:96` |
689
689
  | `d7ce4a3bb745` | certain | Minimize the welcome panel | `modules/hall-ui/public/hall-widgets.js:52` |
690
690
  | `19b0ed5781ae` | certain | Minimum rank required to claim this task | `modules/hall-ui/public/task-lib.js:85` |
@@ -1142,7 +1142,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1142
1142
  | `4b20ecf5528b` | certain | The next page could not be read. | `modules/hall-ui/public/tweak-editor.js:575` |
1143
1143
  | `b88dd8b96b72` | certain | The next person to notice this sentence reads your note instead of re-litigating it. | `modules/hall-ui/public/copy-desk.html:135` |
1144
1144
  | `329a1d350f07` | certain | The page changed after your draft was saved, so the draft is not shown over its new words. Rewrite the lines a | `modules/hall-ui/public/tweak-editor.js:310` |
1145
- | `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:1465` |
1145
+ | `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:1467` |
1146
1146
  | `28e0c1457b08` | certain | The page is in. | `modules/hall-ui/public/tweak-editor.html:94` |
1147
1147
  | `4821b0e74460` | certain | The page, rendered | `modules/hall-ui/public/approval-queue.js:198` |
1148
1148
  | `74bc22f38140` | certain | The platform confirmed this request came from the account it names. | `modules/hall-ui/public/watch.js:396` |
@@ -1261,7 +1261,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1261
1261
  | `13ced9b2fa12` | certain | Update ready | `modules/hall-ui/public/settings-software-update.js:145` |
1262
1262
  | `1e091df06692` | certain | Update rule: {…} | `modules/hall-ui/public/deploy.js:294` |
1263
1263
  | `55fde21d907b` | certain | Update to {…}{…} | `modules/hall-ui/public/settings-software-update.js:85` |
1264
- | `67fbc770b3b3` | certain | Upvote (+1) | `modules/hall-ui/public/builders.js:775` |
1264
+ | `67fbc770b3b3` | certain | Upvote (+1) | `modules/hall-ui/public/builders.js:777` |
1265
1265
  | `16d7f809be8a` | certain | Use the control below to take it. | `modules/hall-ui/public/shell.js:844` |
1266
1266
  | `e7e2a99a2bea` | certain | Use this | `modules/hall-ui/public/settings-specialities.js:395` |
1267
1267
  | `e1ecd543a2ca` | certain | Using this speciality. | `modules/hall-ui/public/settings-specialities.js:454` |
@@ -1271,7 +1271,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1271
1271
  | `f8546b3e285e` | certain | View tasks {…} | `modules/hall-ui/public/roadmap.js:147` |
1272
1272
  | `71c09cda8246` | certain | Voice for {…} | `modules/hall-ui/public/settings.js:74` |
1273
1273
  | `2940b7828d26` | certain | Volume {…}%. | `modules/hall-ui/public/settings.js:216` |
1274
- | `6905f2ee5702` | certain | Vote on this work | `modules/hall-ui/public/builders.js:775` |
1274
+ | `6905f2ee5702` | certain | Vote on this work | `modules/hall-ui/public/builders.js:777` |
1275
1275
  | `b2937e14ee9b` | certain | Vote yes | `modules/hall-ui/public/board-room.js:368` |
1276
1276
  | `8cc425582984` | certain | Vulnerability reports | `modules/hall-ui/public/watch.html:125` |
1277
1277
  | `b1e63e33f739` | certain | Vulnerability reports, engagement reports, override requests, and the audit log. | `modules/hall-ui/public/watch.html:122` |
@@ -1650,6 +1650,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1650
1650
  | `57f45f7812ba` | certain | why (optional) | `modules/hall-ui/public/gate.js:303` |
1651
1651
  | `143f77e2b782` | certain | why these are the same | `modules/hall-ui/public/task-detail.js:565` |
1652
1652
  | `ec7c228cb4ce` | certain | why this isn’t worth doing | `modules/hall-ui/public/task-detail.js:616` |
1653
+ | `98297cf0fa4d` | certain | with help from {…} | `modules/hall-ui/public/builders.js:767` |
1653
1654
  | `c612ab426c2f` | certain | your sky | `modules/hall-ui/public/thinking.html:72` |
1654
1655
  | `3491d859e66a` | certain | your thinking | `modules/hall-ui/public/thinking.html:77` |
1655
1656
  | `66d979d0db89` | certain | your vote | `modules/hall-ui/public/board-room.js:168` |
@@ -1748,7 +1749,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1748
1749
  | `b2a04abfead0` | certain | …{…} more awaiting a nod. | `modules/hall-ui/public/goal-inbox.js:316` |
1749
1750
  | `f16717b3e122` | certain | …{…} more awaiting an answer. | `modules/hall-ui/public/goal-inbox.js:396` |
1750
1751
  | `616fb9adf3cf` | certain | …{…} more going quiet. | `modules/hall-ui/public/goal-inbox.js:485` |
1751
- | `e35f11bdc5d7` | certain | …{…} more open ideas — triage clears them one at a time. | `modules/hall-ui/public/builders.js:893` |
1752
+ | `e35f11bdc5d7` | certain | …{…} more open ideas — triage clears them one at a time. | `modules/hall-ui/public/builders.js:895` |
1752
1753
  | `5f1d914ad45e` | certain | …{…} more — | `modules/hall-ui/public/goal-inbox.js:561` |
1753
1754
  | `de8fa5223aee` | certain | ‹ Prev | `modules/hall-ui/public/hall-kit.js:74` |
1754
1755
  | `d4b84b7fbdeb` | certain | ← All goals | `modules/hall-ui/public/goals.html:75` |
@@ -1787,7 +1788,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1787
1788
  | `a072071b893c` | certain | All time | `modules/status-ui/public/index.html:313` |
1788
1789
  | `1fb3f6a598cd` | certain | Architects of {{worldName}} | `modules/status-ui/public/index.html:200` |
1789
1790
  | `4b16eed738cf` | certain | Attributed to builders: {…} · Shared keep (unattributed): {…} ({…}%). | `modules/status-ui/public/status.js:718` |
1790
- | `20a83501bba6` | certain | Avg time per work (min) | `modules/status-ui/public/status.js:1223` |
1791
+ | `20a83501bba6` | certain | Avg time per work (min) | `modules/status-ui/public/status.js:1225` |
1791
1792
  | `c8959bdc0e59` | certain | Breakdown filtered to {…}: {…} of {…} ({…}% of all spend). | `modules/status-ui/public/status.js:659` |
1792
1793
  | `1fe8937399f5` | certain | By order of the Senate | `modules/status-ui/public/index.html:354` |
1793
1794
  | `21f644463d57` | certain | Citizens who have set the most stone. | `modules/status-ui/public/index.html:201` |
@@ -1813,7 +1814,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1813
1814
  | `dc9eab0a02e2` | certain | Newest entries set down upon the ledger. | `modules/status-ui/public/index.html:305` |
1814
1815
  | `9ef3aaa7b441` | certain | Next page | `modules/status-ui/public/index.html:323` |
1815
1816
  | `37b171a6195a` | certain | No architects yet — be the first to lay a stone. | `modules/status-ui/public/index.html:205` |
1816
- | `7816fbe0add8` | certain | No builders yet — be the first. | `modules/status-ui/public/status.js:1117` |
1817
+ | `7816fbe0add8` | certain | No builders yet — be the first. | `modules/status-ui/public/status.js:1119` |
1817
1818
  | `0b097f051d6f` | certain | No endeavors underway just now. | `modules/status-ui/public/status.js:516` |
1818
1819
  | `ed9b06f79234` | certain | No projects on the map yet — check back soon. | `modules/status-ui/public/explore.html:108` |
1819
1820
  | `4b1a11bfdf10` | certain | No works yet inscribed in this window. | `modules/status-ui/public/status.js:947` |
@@ -1845,7 +1846,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1845
1846
  | `d1add9aa4db0` | certain | The standing of each pillar — whether it holds or has fallen. | `modules/status-ui/public/index.html:188` |
1846
1847
  | `c1ab82f376ec` | certain | The work, in brief | `modules/status-ui/public/status.js:594` |
1847
1848
  | `6cc811f31b3a` | certain | Works in Progress | `modules/status-ui/public/index.html:231` |
1848
- | `4abea0f631eb` | certain | Works shipped / day | `modules/status-ui/public/status.js:1222` |
1849
+ | `4abea0f631eb` | certain | Works shipped / day | `modules/status-ui/public/status.js:1224` |
1849
1850
  | `b2f58817e34e` | certain | World snapshots over time | `modules/status-ui/public/index.html:343` |
1850
1851
  | `ebdc5ecf315c` | certain | avg time / work | `modules/status-ui/public/index.html:220` |
1851
1852
  | `774e102fc137` | certain | city's standing | `modules/status-ui/public/index.html:163` |
@@ -1856,9 +1857,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1856
1857
  | `5b653a87c53c` | certain | of the purse | `modules/status-ui/public/index.html:263` |
1857
1858
  | `c10ffea69d1c` | certain | of the year's purse spent | `modules/status-ui/public/index.html:171` |
1858
1859
  | `89ef611c7d37` | certain | page 1 of 1 | `modules/status-ui/public/index.html:322` |
1859
- | `4a00bdbfd9be` | certain | page {…} of {…} | `modules/status-ui/public/status.js:1010` |
1860
+ | `4a00bdbfd9be` | certain | page {…} of {…} | `modules/status-ui/public/status.js:1012` |
1860
1861
  | `1d0fbe3e5927` | certain | passed the inspector, last 7 days | `modules/status-ui/public/index.html:178` |
1861
- | `c1e3c2212207` | certain | tablet last consulted at {…} | `modules/status-ui/public/status.js:1167` |
1862
+ | `c1e3c2212207` | certain | tablet last consulted at {…} | `modules/status-ui/public/status.js:1169` |
1862
1863
  | `fefb0130c7ae` | certain | value wrought | `modules/status-ui/public/index.html:255` |
1863
1864
  | `b97f429c33fe` | certain | works shipped (90d) | `modules/status-ui/public/index.html:218` |
1864
1865
  | `71a303e1ba09` | certain | works wrought, last 7 days | `modules/status-ui/public/index.html:167` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2278,
7
- "unique_ids": 1962,
8
- "certain": 2278,
6
+ "strings": 2279,
7
+ "unique_ids": 1963,
8
+ "certain": 2279,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 220
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 114,
16
- "strings": 1687
16
+ "strings": 1688
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -394,7 +394,7 @@
394
394
  "text": "Claim task {…}",
395
395
  "occurrences": 3,
396
396
  "at": [
397
- "modules/hall-ui/public/builders.js:971",
397
+ "modules/hall-ui/public/builders.js:973",
398
398
  "modules/hall-ui/public/goals-render.js:366",
399
399
  "modules/hall-ui/public/work.js:482"
400
400
  ]
@@ -2926,7 +2926,7 @@
2926
2926
  "surface": "builders-hall",
2927
2927
  "text": "Achievements could not be loaded.",
2928
2928
  "file": "modules/hall-ui/public/builders.js",
2929
- "line": 1194,
2929
+ "line": 1196,
2930
2930
  "origin": "js-markup",
2931
2931
  "confidence": "certain"
2932
2932
  },
@@ -4060,7 +4060,7 @@
4060
4060
  "surface": "builders-hall",
4061
4061
  "text": "Claim task {…}",
4062
4062
  "file": "modules/hall-ui/public/builders.js",
4063
- "line": 971,
4063
+ "line": 973,
4064
4064
  "origin": "js-markup",
4065
4065
  "confidence": "certain"
4066
4066
  },
@@ -5410,7 +5410,7 @@
5410
5410
  "surface": "builders-hall",
5411
5411
  "text": "Downvote (−1)",
5412
5412
  "file": "modules/hall-ui/public/builders.js",
5413
- "line": 775,
5413
+ "line": 777,
5414
5414
  "origin": "js-markup",
5415
5415
  "confidence": "certain"
5416
5416
  },
@@ -6832,7 +6832,7 @@
6832
6832
  "surface": "builders-hall",
6833
6833
  "text": "Ideas awaiting triage",
6834
6834
  "file": "modules/hall-ui/public/builders.js",
6835
- "line": 895,
6835
+ "line": 897,
6836
6836
  "origin": "js-markup",
6837
6837
  "confidence": "certain"
6838
6838
  },
@@ -7759,7 +7759,7 @@
7759
7759
  "surface": "builders-hall",
7760
7760
  "text": "Metic rank and above may vote",
7761
7761
  "file": "modules/hall-ui/public/builders.js",
7762
- "line": 779,
7762
+ "line": 781,
7763
7763
  "origin": "js-markup",
7764
7764
  "confidence": "certain"
7765
7765
  },
@@ -11881,7 +11881,7 @@
11881
11881
  "surface": "builders-hall",
11882
11882
  "text": "The page could not be loaded. Try again.",
11883
11883
  "file": "modules/hall-ui/public/builders.js",
11884
- "line": 1465,
11884
+ "line": 1467,
11885
11885
  "origin": "js-notice",
11886
11886
  "confidence": "certain"
11887
11887
  },
@@ -12952,7 +12952,7 @@
12952
12952
  "surface": "builders-hall",
12953
12953
  "text": "Upvote (+1)",
12954
12954
  "file": "modules/hall-ui/public/builders.js",
12955
- "line": 775,
12955
+ "line": 777,
12956
12956
  "origin": "js-markup",
12957
12957
  "confidence": "certain"
12958
12958
  },
@@ -13042,7 +13042,7 @@
13042
13042
  "surface": "builders-hall",
13043
13043
  "text": "Vote on this work",
13044
13044
  "file": "modules/hall-ui/public/builders.js",
13045
- "line": 775,
13045
+ "line": 777,
13046
13046
  "origin": "js-markup",
13047
13047
  "confidence": "certain"
13048
13048
  },
@@ -16448,6 +16448,15 @@
16448
16448
  "origin": "js-markup",
16449
16449
  "confidence": "certain"
16450
16450
  },
16451
+ {
16452
+ "id": "98297cf0fa4d",
16453
+ "surface": "builders-hall",
16454
+ "text": "with help from {…}",
16455
+ "file": "modules/hall-ui/public/builders.js",
16456
+ "line": 767,
16457
+ "origin": "js-markup",
16458
+ "confidence": "certain"
16459
+ },
16451
16460
  {
16452
16461
  "id": "c612ab426c2f",
16453
16462
  "surface": "builders-hall",
@@ -17335,7 +17344,7 @@
17335
17344
  "surface": "builders-hall",
17336
17345
  "text": "…{…} more open ideas — triage clears them one at a time.",
17337
17346
  "file": "modules/hall-ui/public/builders.js",
17338
- "line": 893,
17347
+ "line": 895,
17339
17348
  "origin": "js-markup",
17340
17349
  "confidence": "certain"
17341
17350
  },
@@ -22060,7 +22069,7 @@
22060
22069
  "surface": "status-dashboard",
22061
22070
  "text": "Avg time per work (min)",
22062
22071
  "file": "modules/status-ui/public/status.js",
22063
- "line": 1223,
22072
+ "line": 1225,
22064
22073
  "origin": "js-markup",
22065
22074
  "confidence": "certain"
22066
22075
  },
@@ -22294,7 +22303,7 @@
22294
22303
  "surface": "status-dashboard",
22295
22304
  "text": "No builders yet — be the first.",
22296
22305
  "file": "modules/status-ui/public/status.js",
22297
- "line": 1117,
22306
+ "line": 1119,
22298
22307
  "origin": "js-text-assign",
22299
22308
  "confidence": "certain"
22300
22309
  },
@@ -22582,7 +22591,7 @@
22582
22591
  "surface": "status-dashboard",
22583
22592
  "text": "Works shipped / day",
22584
22593
  "file": "modules/status-ui/public/status.js",
22585
- "line": 1222,
22594
+ "line": 1224,
22586
22595
  "origin": "js-markup",
22587
22596
  "confidence": "certain"
22588
22597
  },
@@ -22681,7 +22690,7 @@
22681
22690
  "surface": "status-dashboard",
22682
22691
  "text": "page {…} of {…}",
22683
22692
  "file": "modules/status-ui/public/status.js",
22684
- "line": 1010,
22693
+ "line": 1012,
22685
22694
  "origin": "js-text-assign",
22686
22695
  "confidence": "certain"
22687
22696
  },
@@ -22699,7 +22708,7 @@
22699
22708
  "surface": "status-dashboard",
22700
22709
  "text": "tablet last consulted at {…}",
22701
22710
  "file": "modules/status-ui/public/status.js",
22702
- "line": 1167,
22711
+ "line": 1169,
22703
22712
  "origin": "js-text-assign",
22704
22713
  "confidence": "certain"
22705
22714
  },
@@ -2761,5 +2761,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2761
2761
  landed since 1.20.50 with no explicit bump. run 36816320939. (task 1002620)
2762
2762
  1.20.52 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2763
2763
  landed since 1.20.51 with no explicit bump. run 36817286385. (task 1002620)
2764
+ 1.20.53 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2765
+ landed since 1.20.52 with no explicit bump. run 36818406554. (task 1002620)
2764
2766
  ---------------------------------------------------------------------------
2765
2767
  ```
@@ -1353,7 +1353,7 @@
1353
1353
  "surface": "builders",
1354
1354
  "title": "Builders' Hall",
1355
1355
  "states_read": ["home","standing","standing-karma","out"],
1356
- "files_hash": "2a773a6907072f93",
1356
+ "files_hash": "959a729d1d7eba2b",
1357
1357
  "reading_hash": "1debe0f979ce3b59",
1358
1358
  "counts": {"lines":204,"placed":68,"shared":7,"unplaced":129},
1359
1359
  "omitted": {"data":182},
@@ -1398,9 +1398,9 @@
1398
1398
  {"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"},
1399
1399
  {"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},
1400
1400
  {"key":"L0039","section":"From the system","text":"Open {…}","placement":"placed","file":"modules/hall-ui/public/goals.js","line":67,"string_id":"f6f68ba322be"},
1401
- {"key":"L0040","section":"Ideas awaiting triage 151","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":895,"string_id":"e181637c5a5e"},
1401
+ {"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"},
1402
1402
  {"key":"L0041","section":"Ideas awaiting triage 151","text":"Triage","placement":"unplaced","occurrences":5},
1403
- {"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":893,"string_id":"e35f11bdc5d7"},
1403
+ {"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"},
1404
1404
  {"key":"L0043","section":"Was this removed? 2","text":"Was this removed?","placement":"unplaced"},
1405
1405
  {"key":"L0044","section":"Was this removed? 2","text":"{…}{…} — open it to see what went","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":374,"string_id":"7f79bedecf60","occurrences":2},
1406
1406
  {"key":"L0045","section":"Was this removed? 2","text":"Confirm","placement":"unplaced","occurrences":2},
@@ -4318,7 +4318,7 @@
4318
4318
  "surface": "status",
4319
4319
  "title": "{{worldName}} — the public ledger",
4320
4320
  "states_read": ["default"],
4321
- "files_hash": "49c104486ef09ecf",
4321
+ "files_hash": "35acac6a3253f014",
4322
4322
  "reading_hash": "2853e52b66ba076c",
4323
4323
  "counts": {"lines":87,"placed":49,"shared":14,"unplaced":24},
4324
4324
  "omitted": {"data":89},
@@ -4327,7 +4327,7 @@
4327
4327
  {"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"},
4328
4328
  {"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"},
4329
4329
  {"key":"L0003","section":"top bar","text":"Cross into the city →","placement":"shared","file":"modules/status-ui/public/index.html","line":128,"string_id":"3aa071ef1142"},
4330
- {"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1167,"string_id":"c1e3c2212207"},
4330
+ {"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1169,"string_id":"c1e3c2212207"},
4331
4331
  {"key":"L0005","section":"navigation","text":"Pulse","placement":"shared"},
4332
4332
  {"key":"L0006","section":"navigation","text":"Watch","placement":"shared"},
4333
4333
  {"key":"L0007","section":"navigation","text":"Architects","placement":"shared"},
@@ -4403,7 +4403,7 @@
4403
4403
  {"key":"L0077","section":"Lately Inscribed","text":"inscribed by {…} · {…}","placement":"unplaced","occurrences":6},
4404
4404
  {"key":"L0078","section":"Lately Inscribed","text":"The site now has the three pages every real website needs — privacy, terms and a contact page — linked from the footer of the front page and the projects hub. The privacy page says plainly what is kept about a GitHub account and a project, who else sees it, and how long it stays; the contact page points at the two doors that actually exist, the repository and the builders' hall. Every project built on the platform gets the same pages under its own name, with the claims that are only true of the platform itself removed.","placement":"unplaced"},
4405
4405
  {"key":"L0079","section":"Lately Inscribed","text":"‹ earlier page","placement":"placed","file":"modules/status-ui/public/index.html","line":321,"string_id":"6d0d64f10bd9"},
4406
- {"key":"L0080","section":"Lately Inscribed","text":"page {…} of {…}","placement":"placed","file":"modules/status-ui/public/status.js","line":1010,"string_id":"4a00bdbfd9be"},
4406
+ {"key":"L0080","section":"Lately Inscribed","text":"page {…} of {…}","placement":"placed","file":"modules/status-ui/public/status.js","line":1012,"string_id":"4a00bdbfd9be"},
4407
4407
  {"key":"L0081","section":"Lately Inscribed","text":"later page ›","placement":"placed","file":"modules/status-ui/public/index.html","line":323,"string_id":"9b019d632d3e"},
4408
4408
  {"key":"L0082","section":"The World","text":"The World","placement":"placed","file":"modules/status-ui/public/index.html","line":331,"string_id":"3e465b62cea8"},
4409
4409
  {"key":"L0083","section":"The World","text":"A true-to-life portrait of {{worldName}} as she stands today, rendered from the city's own stones.","placement":"placed","file":"modules/status-ui/public/index.html","line":332,"string_id":"623d6b7ae6b7"},
@@ -0,0 +1,41 @@
1
+ -- core_267 — an approved page tweak already shipped names its artist (task 1004432,
2
+ -- WA6.RP02, goal 1000095)
3
+ --
4
+ -- From this release, shipTask (modules/lifecycle/db-ship.js) writes tasks.shipped_by
5
+ -- as the round's ARTIST when a page tweak ships, instead of the session that applied
6
+ -- it. Everything that counts or lists shipped work reads shipped_by (the profile,
7
+ -- the shipped count, the activity log, the status page's recent list, streaks,
8
+ -- achievements, the hub rollup), so that one write moves every one of them.
9
+ --
10
+ -- The rounds that shipped BEFORE this release still name their applier, so on every
11
+ -- one of those surfaces the artist who wrote the page is absent and the applier is
12
+ -- credited with work that was not theirs. This brings them in line, once.
13
+ --
14
+ -- The artist is page-tweak-artist.js's rule, spelled out here because a migration
15
+ -- cannot call it: the builder of the round's LATEST WEB claim (worktree_name and
16
+ -- creator_session_id both NULL), the same person economy's awardArtistPageCredit
17
+ -- already paid. A round with no web claim keeps its applier, exactly as the ship
18
+ -- path does. tests/page_tweak_artist.mjs holds this predicate to the helper's.
19
+ --
20
+ -- Data only, no column or constraint: forward-safe against a live database, and an
21
+ -- older release reading the rows just sees a different builder id. Idempotent: a
22
+ -- second run matches nothing, because the IS DISTINCT FROM guard skips rows that
23
+ -- already name their artist. Money is untouched: credit_log is not read or written,
24
+ -- and the artist was paid at the land.
25
+
26
+ UPDATE tasks t
27
+ SET shipped_by = w.builder_id
28
+ FROM (
29
+ SELECT DISTINCT ON (c.task_id) c.task_id, c.builder_id
30
+ FROM claims c
31
+ JOIN tasks pt ON pt.id = c.task_id
32
+ WHERE pt.source = 'page-tweak'
33
+ AND pt.status = 'shipped'
34
+ AND c.worktree_name IS NULL AND c.creator_session_id IS NULL
35
+ ORDER BY c.task_id, c.claimed_at DESC, c.id DESC
36
+ ) w
37
+ WHERE t.id = w.task_id
38
+ AND t.shipped_by IS DISTINCT FROM w.builder_id;
39
+
40
+ INSERT INTO schema_migrations (version) VALUES ('core_267_page_tweak_author_of_record')
41
+ ON CONFLICT DO NOTHING;
@@ -762,7 +762,9 @@
762
762
  if (!tbody) return;
763
763
  const slice = window.OTBKit.paginate(acclaimState.tasks, acclaimPager);
764
764
  tbody.innerHTML = slice.map((t) => {
765
- const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`));
765
+ // task 1004432: an approved page is its artist's; the applying session rides along as a helper.
766
+ const helperName = t.helper_display_name || (t.helper_login ? `@${t.helper_login}` : '');
767
+ const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`)) + (helperName ? ` <span class="acclaim__evidence">with help from ${escapeHtml(helperName)}</span>` : '');
766
768
  const net = Number(t.net_votes) || 0;
767
769
  const netCls = signClasses(net, 'acclaim__net');
768
770
  const netText = net > 0 ? `+${net}` : `${net}`;
@@ -14,6 +14,7 @@ const api = require('../../src/module-api');
14
14
  const { pool, withTx } = api;
15
15
  const { SMOKE_NOT_LIKE, gradePassCount, gradeQualityWhere, median } = require('./db-shared.js');
16
16
  const { CREDITS_AWARDED_SUBQUERY } = require('./db-tasks.js');
17
+ const { helperJoinSql } = require('./page-tweak-artist.js');
17
18
 
18
19
  // -------------------------------------------------------------------------
19
20
  // Cost log + summary carved into modules/economy/cost.js (BV1.R77 / ADR 0093
@@ -47,9 +48,13 @@ async function recentShippedTasks(limit = 30, { days = null } = {}) {
47
48
  CASE WHEN t.security_sensitive THEN NULL ELSE t.visual_url END AS visual_url, CASE WHEN t.security_sensitive THEN NULL ELSE t.visual_alt END AS visual_alt,
48
49
  ${CREDITS_AWARDED_SUBQUERY} AS credits_awarded,
49
50
  t.est_minutes, t.actual_minutes,
50
- b.github_login, b.display_name
51
+ b.github_login, b.display_name,
52
+ -- task 1004432: an approved page is its artist's (shipped_by); the session
53
+ -- that applied it rides along as a helper, so a reader still sees both.
54
+ helper.helper_login, helper.helper_display_name
51
55
  FROM tasks t
52
56
  LEFT JOIN builders b ON b.id = t.shipped_by
57
+ ${helperJoinSql('t')}
53
58
  WHERE ${whereClause}
54
59
  ORDER BY t.shipped_at DESC NULLS LAST
55
60
  LIMIT $1`,
@@ -19,6 +19,7 @@ const { THETES_GRADUATION_THRESHOLD, maybeAutoGraduateToThetes, foldWorktreeName
19
19
  const { mostRecentClaimHolderId, sealSessionLog, staleClaimHourPredicate } = require('./db-shared.js');
20
20
  const { LIVE_STATUSES } = require('./dead-deps.js');
21
21
  const { isPageTweak } = require('./page-tweak-hold.js');
22
+ const pageTweakArtist = require('./page-tweak-artist.js');
22
23
 
23
24
  // shouldRefuseClaimResolve — may THIS caller resolve THIS claim? (task 1003767)
24
25
  //
@@ -512,6 +513,9 @@ async function needsRebaseTasksForBuilder(builderId, client = pool) {
512
513
  // The ship gate (task 1002483, ADR 0184) — see ./ship-assurance for the why.
513
514
  const { normalizeShipAssurance, assertShipAssurance, recordShipAssurance, SHIP_ASSURANCE_KINDS } = require('./ship-assurance');
514
515
 
516
+ // The author of record (task 1004432): a page tweak names its ARTIST at this flip only.
517
+ const SHIPPED_BY_SQL = pageTweakArtist.shippedBySql('$2');
518
+
515
519
  async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
516
520
  const shipAssuranceValue = assertShipAssurance(landAssurance); // FAIL-CLOSED, ADR 0184
517
521
  // #538 test seam (mirrors confirmTask): a caller-supplied pool is honored ONLY
@@ -523,7 +527,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
523
527
  `UPDATE tasks
524
528
  SET status = 'shipped',
525
529
  shipped_at = now(),
526
- shipped_by = COALESCE(shipped_by, $2),
530
+ shipped_by = ${SHIPPED_BY_SQL},
527
531
  needs_rebase = false,
528
532
  needs_rebase_reason = NULL,
529
533
  needs_rebase_at = NULL,
@@ -642,10 +646,13 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
642
646
  // credit it keys off (breaking-the-100c-bar etc.) now lands here.
643
647
  let justUnlocked = [];
644
648
  const rewardPort = seams.resolveOptional('reward');
645
- if (rewardPort && rewardPort.reconcileBuilderAchievements && creditedBuilderId != null) {
649
+ // A page tweak's ship count and credit moved to its artist (above), so the
650
+ // artist is whose achievements this ship can unlock; the applier's did not move.
651
+ const achievementBuilderId = isPageTweak(rows[0]) ? actualShipperId : creditedBuilderId;
652
+ if (rewardPort && rewardPort.reconcileBuilderAchievements && achievementBuilderId != null) {
646
653
  await client.query('SAVEPOINT ach_reconcile_ship');
647
654
  try {
648
- justUnlocked = await rewardPort.reconcileBuilderAchievements(client, creditedBuilderId, { taskId });
655
+ justUnlocked = await rewardPort.reconcileBuilderAchievements(client, achievementBuilderId, { taskId });
649
656
  await client.query('RELEASE SAVEPOINT ach_reconcile_ship');
650
657
  } catch (_) {
651
658
  await client.query('ROLLBACK TO SAVEPOINT ach_reconcile_ship');
@@ -0,0 +1,86 @@
1
+ // modules/lifecycle/page-tweak-artist.js — WHO a page-tweak round belongs to,
2
+ // written once (task 1004432 / WA6.RP02, ADR 0341 D10).
3
+ //
4
+ // A round has two people. The ARTIST wrote the words in the studio and holds the
5
+ // round's WEB claim (no worktree, no creating session: the claim the submit
6
+ // released). The APPLIER is the session that put the words into the page, on an
7
+ // ordinary CLI claim in a worktree. The artist is the author of record: they are
8
+ // the one paid (economy's awardArtistPageCredit), the one the changelog names
9
+ // (page-tweak-reads.js), and, since this task, the one `tasks.shipped_by` names
10
+ // once the round ships. The applier is a helper.
11
+ //
12
+ // Pure: SQL fragments and nothing else, so every reader takes the rule from here
13
+ // instead of re-typing it. Economy cannot require this file (a module reaches
14
+ // another only through a port), so credits.js keeps its own copy of the
15
+ // predicate and tests/page_tweak_artist.mjs holds the two strings together.
16
+
17
+ 'use strict';
18
+
19
+ const PAGE_TWEAK_SOURCE = 'page-tweak';
20
+
21
+ // A WEB claim: the artist's. Neither a worktree (a CLI applier) nor a creating
22
+ // session (an agent-made claim) is on it.
23
+ function webClaimPredicate(alias = 'c') {
24
+ return `${alias}.worktree_name IS NULL AND ${alias}.creator_session_id IS NULL`;
25
+ }
26
+
27
+ // The newest claim first; the id breaks a same-instant tie the same way in
28
+ // every reader.
29
+ function latestFirst(alias = 'c') {
30
+ return `${alias}.claimed_at DESC, ${alias}.id DESC`;
31
+ }
32
+
33
+ // The round's artist: the builder of its latest web claim, as a scalar
34
+ // subquery over `taskIdExpr` (a column or a bind parameter). NULL when the
35
+ // round never had a web claim, which a caller treats as "no artist".
36
+ function artistIdSql(taskIdExpr) {
37
+ return `(SELECT c.builder_id FROM claims c
38
+ WHERE c.task_id = ${taskIdExpr} AND ${webClaimPredicate('c')}
39
+ ORDER BY ${latestFirst('c')}
40
+ LIMIT 1)`;
41
+ }
42
+
43
+ // THE AUTHOR OF RECORD, as shipTask writes it (db-ship.js). Everywhere shipped
44
+ // work is counted or listed reads tasks.shipped_by: the profile, the shipped
45
+ // count, the activity log, the status page's recent list, streaks,
46
+ // achievements, the hub rollup. For an ordinary task that is the claim holder,
47
+ // set at confirm, and this keeps it (COALESCE with `fallbackParam`, the caller's
48
+ // builder). For a page tweak it is the ARTIST, written at the SHIPPED flip and
49
+ // nowhere earlier: until a round ships, shipped_by still names the applier,
50
+ // because the rebase gate (needsRebaseTasksForBuilder, status='confirmed') must
51
+ // refuse the session that can actually rebase, never an artist with no
52
+ // worktree. A round with no web claim keeps its applier. Used inside
53
+ // `UPDATE tasks`, so the row is `tasks`.
54
+ function shippedBySql(fallbackParam) {
55
+ return `CASE WHEN source = '${PAGE_TWEAK_SOURCE}'
56
+ THEN COALESCE(${artistIdSql('tasks.id')}, shipped_by, ${fallbackParam})
57
+ ELSE COALESCE(shipped_by, ${fallbackParam}) END`;
58
+ }
59
+
60
+ // The helper beside a shipped row, for a list that shows people. The helper is
61
+ // the round's APPLIER, the builder of its latest WORKTREE claim: display only
62
+ // ("with help from"), never attribution or pay. A LEFT JOIN
63
+ // LATERAL yielding helper_login and helper_display_name on a page tweak whose
64
+ // applier is someone other than the author of record, and nothing otherwise
65
+ // (an ordinary task, a self-applied round, a round with no worktree claim).
66
+ // `alias` is the tasks alias of the surrounding query.
67
+ //
68
+ // THE COST, since it runs once per row of a feed. The source test sits inside
69
+ // the claims scan and names only the outer row, so Postgres plans it as a
70
+ // one-time filter: an ordinary task reads no claim at all. A page tweak's lookup
71
+ // is idx_claims_task_all (core_263, claims (task_id)) plus a sort of that one
72
+ // task's few claims, and the feeds are LIMITed.
73
+ function helperJoinSql(alias = 't') {
74
+ return `LEFT JOIN LATERAL (
75
+ SELECT hb.github_login AS helper_login, hb.display_name AS helper_display_name
76
+ FROM (SELECT c.builder_id FROM claims c
77
+ WHERE ${alias}.source = '${PAGE_TWEAK_SOURCE}'
78
+ AND c.task_id = ${alias}.id AND c.worktree_name IS NOT NULL
79
+ ORDER BY ${latestFirst('c')}
80
+ LIMIT 1) ap
81
+ JOIN builders hb ON hb.id = ap.builder_id
82
+ WHERE ap.builder_id IS DISTINCT FROM ${alias}.shipped_by
83
+ ) helper ON true`;
84
+ }
85
+
86
+ module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
@@ -13,6 +13,7 @@
13
13
 
14
14
  const api = require('../../src/module-api');
15
15
  const hold = require('./page-tweak-hold');
16
+ const artistRule = require('./page-tweak-artist');
16
17
 
17
18
  // Every page-tweak task, oldest first, with what the round state (D7) and the
18
19
  // changelog (D8) need beside the task itself:
@@ -35,9 +36,10 @@ const hold = require('./page-tweak-hold');
35
36
  // * the active claim is a plain join: uniq_active_claim_per_task allows at
36
37
  // most one row per task, and its partial index serves it.
37
38
  // * the latest web claim is ONE pass over claims (the `web` CTE, DISTINCT ON
38
- // per task), joined once, rather than a correlated subquery per task:
39
- // claims has no full index on task_id, so a per-row LATERAL would scan it
40
- // once per round.
39
+ // per task), joined once, rather than a correlated subquery per task.
40
+ // (When this was written claims had no full index on task_id; core_263 has
41
+ // since added idx_claims_task_all, so a per-task lookup is an index probe.
42
+ // The single pass stays: it is still the cheaper plan for every round at once.)
41
43
  //
42
44
  // `pageId` narrows it to ONE page's rounds (task 1004319 / TW08): the draft
43
45
  // autosave runs this on every debounced save, and a page's rounds are a handful
@@ -52,8 +54,8 @@ async function listPageTweakTasks({ pageId = null } = {}) {
52
54
  ), web AS (
53
55
  SELECT DISTINCT ON (c.task_id) c.task_id, c.builder_id
54
56
  FROM claims c JOIN pt ON pt.id = c.task_id
55
- WHERE c.worktree_name IS NULL AND c.creator_session_id IS NULL
56
- ORDER BY c.task_id, c.claimed_at DESC, c.id DESC
57
+ WHERE ${artistRule.webClaimPredicate('c')}
58
+ ORDER BY c.task_id, ${artistRule.latestFirst('c')}
57
59
  )
58
60
  SELECT t.id, t.status, t.title, t.description, t.source_ref, t.touches,
59
61
  t.shipped_at, t.created_at, t.updated_at,
@@ -33,6 +33,7 @@ const { depRefusalRow } = require('../dead-deps.js');
33
33
  // quality verdict rather than an outage or an errored worker (task 1002652).
34
34
  // The acclaim feed reads it so a grader outage never reaches a voter as 0.0.
35
35
  const { gradeQualityWhere } = require('../db-shared');
36
+ const { helperJoinSql } = require('../page-tweak-artist'); // task 1004432: a page tweak's applier as a helper
36
37
  const auth = api;
37
38
  const db = require('../db');
38
39
  const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
@@ -783,6 +784,7 @@ module.exports = function buildTasksRouter() {
783
784
  `SELECT t.id, t.title, t.value_summary, t.version_id, t.status,
784
785
  t.shipped_at, t.shipped_by,
785
786
  b.github_login, b.display_name,
787
+ helper.helper_login, helper.helper_display_name,
786
788
  COALESCE(pv.net, 0)::int AS net_votes,
787
789
  mine.vote AS my_vote,
788
790
  -- task 1002835: a karma vote is uninformed without the two facts
@@ -801,6 +803,7 @@ module.exports = function buildTasksRouter() {
801
803
  cost.total_usd AS cost_usd
802
804
  FROM tasks t
803
805
  LEFT JOIN builders b ON b.id = t.shipped_by
806
+ ${helperJoinSql('t')}
804
807
  LEFT JOIN task_grades g ON g.task_id = t.id
805
808
  LEFT JOIN LATERAL (
806
809
  SELECT SUM(amount_usd) AS total_usd
@@ -741,6 +741,10 @@ async function sessionUploadCoverage({ days, windowHours } = {}) {
741
741
  AND t.shipped_by IS NOT NULL
742
742
  AND t.shipped_at IS NOT NULL
743
743
  AND t.shipped_at >= now() - ($1 * interval '1 day')
744
+ -- task 1004432: a shipped page tweak names its ARTIST, who writes in the
745
+ -- studio and runs no session, so it can never be "covered"; the session
746
+ -- that applied it is the applier's, on an ordinary task of its own kind.
747
+ AND t.source IS DISTINCT FROM 'page-tweak'
744
748
  GROUP BY t.shipped_by, b.github_login
745
749
  ORDER BY 4 DESC`,
746
750
  [d, w]
@@ -765,6 +769,10 @@ async function shipsMissingSessionRecord({ days, windowHours, limit, builderId }
765
769
  AND t.shipped_by IS NOT NULL
766
770
  AND t.shipped_at IS NOT NULL
767
771
  AND t.shipped_at >= now() - ($1 * interval '1 day')
772
+ -- task 1004432: a shipped page tweak names its ARTIST, who writes in the
773
+ -- studio and runs no session, so it can never be "covered"; the session
774
+ -- that applied it is the applier's, on an ordinary task of its own kind.
775
+ AND t.source IS DISTINCT FROM 'page-tweak'
768
776
  AND ($3::bigint IS NULL OR t.shipped_by = $3)
769
777
  AND NOT EXISTS (
770
778
  SELECT 1
@@ -984,7 +984,9 @@ function renderShippedPage() {
984
984
  const meta = document.createElement('p');
985
985
  meta.className = 'shipped__meta';
986
986
  const by = t.display_name || t.github_login;
987
- meta.textContent = `${by ? `inscribed by ${by} · ` : ''}${fmtRelativeTime(t.shipped_at)}`;
987
+ // task 1004432: an approved page is the artist's; the session that applied it is a helper.
988
+ const helper = t.helper_display_name || t.helper_login;
989
+ meta.textContent = `${by ? `inscribed by ${by} · ` : ''}${helper ? `with help from ${helper} · ` : ''}${fmtRelativeTime(t.shipped_at)}`;
988
990
  li.appendChild(title);
989
991
  if (thumb) li.appendChild(thumb);
990
992
  li.appendChild(meta);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.52",
3
+ "version": "1.20.53",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.52",
9
+ "version": "1.20.53",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.52",
3
+ "version": "1.20.53",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -8429,5 +8429,11 @@
8429
8429
  "id": "1004475",
8430
8430
  "text": "Equity or shares is now also greyed out for a single-owner company and for a research entity, each with its own short reason. Only a regular for-profit company or a division of a corporation is offered equity. As before, it's"
8431
8431
  }
8432
+ ],
8433
+ "1.20.53": [
8434
+ {
8435
+ "id": "1004432",
8436
+ "text": "An approved page now counts as the artist's own work everywhere the project shows who did what: their profile, their shipped count, their activity, the status page and the leaderboard. Before, the page was credited to the sess"
8437
+ }
8432
8438
  ]
8433
8439
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.20.52'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.53'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,236 @@
1
+ // tests/page_tweak_artist.mjs
2
+ //
3
+ // Task 1004432 / WA6.RP02: an approved page tweak counts as the ARTIST's work,
4
+ // not the applier's.
5
+ //
6
+ // The fixture round the task's DONE WHEN names: artist A writes the page (a WEB
7
+ // claim), session B applies it (a CLI claim in a worktree), A approves. Through
8
+ // the REAL db.shipTask over a small stateful ledger:
9
+ // * the shipped row names A (tasks.shipped_by), not B, even though confirm had
10
+ // already attributed it to B;
11
+ // * the onboarding count, first_ship_at and the achievement reconcile follow A;
12
+ // * A's credit is the one artist row it always was, and B is paid nothing more;
13
+ // * an ordinary task and a round with no web claim keep their shipper;
14
+ // * before the round ships, shipped_by still names B, so the rebase gate keeps
15
+ // asking the session that can do the rebase.
16
+ // Then the READERS, run for real against the ledger: A's profile works, shipped
17
+ // count, activity and the status page's recent list show the page as A's, and
18
+ // B's do not count it; the recent list names B as the helper.
19
+ // And the one rule: the helper's predicate is the one economy pays on, the one
20
+ // the Tweak Mode reads use, and the one the backfill migration spells out.
21
+ //
22
+ // Run: node tests/page_tweak_artist.mjs
23
+
24
+ import { strict as assert } from 'node:assert';
25
+ import { readFileSync } from 'node:fs';
26
+ import { createRequire } from 'node:module';
27
+ import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
28
+
29
+ process.env.NODE_ENV = 'test';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const achievements = require('../modules/economy/achievements.js');
33
+ achievements.evaluateAfterShip = async () => [];
34
+ achievements.evaluateAfterCreditAward = async () => [];
35
+ const reconciled = [];
36
+ achievements.reconcileBuilderAchievements = async (_client, builderId) => { reconciled.push(String(builderId)); return []; };
37
+ const seams = require('../src/module-seams.js');
38
+ if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
39
+ const api = require('../src/module-api.js');
40
+ const db = require('../modules/lifecycle/db.js');
41
+ const activity = require('../modules/lifecycle/activity.js');
42
+ const rule = require('../modules/lifecycle/page-tweak-artist.js');
43
+
44
+ const { test, summary } = makeRunner();
45
+
46
+ const A = '41'; // the artist
47
+ const B = '42'; // the applier
48
+ const RECONCILER = '999';
49
+ const LAND_PROOF = { kind: 'proof', reason: 'test fixture: land proven' };
50
+ const norm = (s) => String(s).replace(/\s+/g, ' ').trim();
51
+
52
+ const web = (builder, at) => ({ builder_id: builder, claimed_at: at, worktree_name: null, creator_session_id: null });
53
+ const cli = (builder, at) => ({ builder_id: builder, claimed_at: at, worktree_name: `tw-${builder}-${at}`, creator_session_id: `s-${builder}` });
54
+
55
+ // A one-task ledger. The ship UPDATE is evaluated from the SQL it is sent: the
56
+ // page-tweak branch takes the artist only when the statement actually carries
57
+ // the helper's artist subquery, so reverting db-ship.js to a plain COALESCE
58
+ // makes the shipped row keep B and this file red.
59
+ function ledger({ source = 'page-tweak', shippedBy = B, claims = [web(A, 1), cli(B, 2)] } = {}) {
60
+ const state = {
61
+ task: { id: 7001, title: 'Tweak: Home (landing:index)', source, status: 'confirmed', credits_reward: 30, kind: 'feature', shipped_by: shippedBy },
62
+ claims: claims.map((c, i) => ({ id: 9000 + i, ...c })),
63
+ credit_log: [], firstShip: [], counted: [],
64
+ };
65
+ const latest = (list) => list.slice().sort((x, y) => (x.claimed_at - y.claimed_at) || (x.id - y.id)).slice(-1)[0] || null;
66
+ const artistSubquery = norm(rule.artistIdSql('tasks.id'));
67
+ const pool = makeSqlAwareClient((s, p) => {
68
+ const t = state.task;
69
+ if (/^UPDATE tasks SET status = 'shipped'/.test(s)) {
70
+ if (t.status !== 'confirmed') return { rows: [] };
71
+ t.status = 'shipped';
72
+ const pageBranch = s.includes(`CASE WHEN source = '${rule.PAGE_TWEAK_SOURCE}' THEN COALESCE(${artistSubquery}, shipped_by, $2)`);
73
+ const w = latest(state.claims.filter((c) => c.worktree_name == null && c.creator_session_id == null));
74
+ t.shipped_by = (pageBranch && t.source === 'page-tweak' && w) ? w.builder_id : (t.shipped_by ?? p[1]);
75
+ return { rows: [{ id: t.id, title: t.title, status: 'shipped', shipped_at: new Date().toISOString(), shipped_by: t.shipped_by,
76
+ security_sensitive: false, value_summary: null, credits_reward: t.credits_reward, kind: t.kind, source: t.source }] };
77
+ }
78
+ if (/^UPDATE claims/.test(s)) return { rows: [] };
79
+ if (/^UPDATE builders SET first_ship_at/.test(s)) { state.firstShip.push(String(p[0])); return { rows: [] }; }
80
+ if (/^SELECT count\(\*\)::int AS n FROM tasks WHERE shipped_by = \$1/.test(s)) { state.counted.push(String(p[0])); return { rows: [{ n: 1 }] }; }
81
+ if (/^SELECT builder_id FROM claims WHERE task_id = \$1/.test(s)) {
82
+ const c = latest(state.claims);
83
+ return { rows: c ? [{ builder_id: c.builder_id }] : [] };
84
+ }
85
+ if (/^SELECT t\.source, t\.status, t\.credits_reward, t\.kind/.test(s)) {
86
+ const w = latest(state.claims.filter((c) => c.worktree_name == null && c.creator_session_id == null));
87
+ return { rows: [{ source: t.source, status: t.status, credits_reward: t.credits_reward, kind: t.kind, artist_id: w ? w.builder_id : null }] };
88
+ }
89
+ if (/^INSERT INTO credit_log/.test(s)) {
90
+ const [builder_id, task_id, delta, reason] = p;
91
+ if (state.credit_log.some((r) => r.task_id === task_id && r.reason === reason)) return { rows: [], rowCount: 0 };
92
+ state.credit_log.push({ builder_id: String(builder_id), task_id, delta, reason });
93
+ return { rows: [], rowCount: 1 };
94
+ }
95
+ return { rows: [] };
96
+ });
97
+ return { pool, state };
98
+ }
99
+
100
+ const ship = (pool) => db.shipTask({ taskId: 7001, builderId: RECONCILER, landAssurance: LAND_PROOF }, { pool });
101
+
102
+ // =========================================================================
103
+ await test('A writes, B applies, A approves: the shipped row names A, not B', async () => {
104
+ reconciled.length = 0;
105
+ const { pool, state } = ledger();
106
+ assert.equal(state.task.shipped_by, B, 'precondition: confirm attributed the round to the applier');
107
+ const out = await ship(pool);
108
+ assert.equal(String(out.shipped_by), A, 'the author of record is the artist');
109
+ assert.equal(String(state.task.shipped_by), A);
110
+ assert.deepEqual(state.firstShip, [A], 'first_ship_at is stamped on the artist');
111
+ assert.ok(state.counted.every((id) => id === A) && state.counted.length > 0, 'the ship count that drives onboarding is the artist\'s');
112
+ assert.deepEqual(reconciled, [A], 'the achievements this ship can unlock are the artist\'s');
113
+ });
114
+
115
+ await test('A\'s credits are unchanged: one artist row of 30, nothing to B', async () => {
116
+ const { pool, state } = ledger();
117
+ await ship(pool);
118
+ assert.deepEqual(state.credit_log.map((r) => [r.builder_id, r.delta, r.reason]), [[A, 30, 'tweak.page_approved:7001']]);
119
+ assert.ok(!state.credit_log.some((r) => r.builder_id === B), 'the applier is paid nothing on the land');
120
+ });
121
+
122
+ await test('a self-applied round names its one author', async () => {
123
+ const { pool, state } = ledger({ shippedBy: A, claims: [web(A, 1), cli(A, 2)] });
124
+ await ship(pool);
125
+ assert.equal(String(state.task.shipped_by), A);
126
+ });
127
+
128
+ await test('a round with no web claim keeps its applier; an ordinary task keeps its shipper', async () => {
129
+ const noWeb = ledger({ claims: [cli(B, 2)] });
130
+ await ship(noWeb.pool);
131
+ assert.equal(String(noWeb.state.task.shipped_by), B, 'no artist on record: the applier stays');
132
+ const ordinary = ledger({ source: null, claims: [web(A, 1), cli(B, 2)] });
133
+ await ship(ordinary.pool);
134
+ assert.equal(String(ordinary.state.task.shipped_by), B, 'an ordinary task is never re-attributed, web claim or not');
135
+ });
136
+
137
+ await test('the rewrite happens only at the shipped flip: confirm still names the applier', () => {
138
+ const src = readFileSync(new URL('../modules/lifecycle/db-ship.js', import.meta.url), 'utf8');
139
+ const confirmUpdate = src.slice(src.indexOf('async function confirmTask'));
140
+ assert.match(confirmUpdate, /shipped_by = COALESCE\(shipped_by, \$2\)/, 'confirmTask attributes to the claim holder, unchanged');
141
+ for (const f of ['db-grade.js', 'db-overrides.js']) {
142
+ const s = readFileSync(new URL(`../modules/lifecycle/${f}`, import.meta.url), 'utf8');
143
+ assert.ok(!s.includes('artistIdSql'), `${f} confirms; it must not move attribution before the round ships`);
144
+ }
145
+ assert.match(src, /WHERE needs_rebase = true AND status = 'confirmed' AND shipped_by = \$1/, 'the rebase gate still reads the confirmed row\'s shipper');
146
+ });
147
+
148
+ // =========================================================================
149
+ // The readers, for real. A shipped page tweak is now a row whose shipped_by is
150
+ // A; every reader below must key on shipped_by for that to reach the person.
151
+ const shippedRow = { id: 7001, title: 'Tweak: Home (landing:index)', value_summary: 'Home reads in the artist\'s words', visual_url: null, visual_alt: null,
152
+ version_id: 'BONGOS-V2', goal_id: null, kind: 'feature', status: 'shipped', shipped_at: '2026-09-30T12:00:00Z', discipline: 'artist',
153
+ credits_reward: 30, credits_awarded: 30, source: 'page-tweak', shipped_by: A, github_login: 'artist-a', display_name: 'Artist A',
154
+ helper_login: 'applier-b', helper_display_name: 'Applier B' };
155
+ const readerSql = [];
156
+ api.pool.query = async (sql, params = []) => {
157
+ const s = norm(sql);
158
+ readerSql.push(s);
159
+ const mine = /shipped_by = \$1/.test(s) ? String(params[0]) === A : true;
160
+ if (/^SELECT count\(\*\)::int AS (n|total) FROM tasks/.test(s)) return { rows: [{ n: mine ? 1 : 0, total: mine ? 1 : 0 }] };
161
+ return { rows: mine ? [shippedRow] : [] };
162
+ };
163
+
164
+ await test('A\'s profile works, shipped count and activity hold the page; B\'s do not', async () => {
165
+ assert.equal((await db.recentShippedTasksByBuilder(A)).length, 1, 'A\'s profile lists the page');
166
+ assert.equal((await db.recentShippedTasksByBuilder(B)).length, 0, 'B\'s profile does not');
167
+ assert.equal(await db.countShippedTasksForBuilder(A), 1);
168
+ assert.equal(await db.countShippedTasksForBuilder(B), 0, 'B\'s shipped count does not count it');
169
+ assert.equal((await activity.activityTasksByBuilder(A)).total, 1, 'A\'s activity holds it');
170
+ assert.equal((await activity.activityTasksByBuilder(B)).total, 0, 'B\'s activity does not');
171
+ assert.ok((await activity.activityTasksByBuilder(A, { role: 'artist' })).total === 1, 'and under the Artist craft');
172
+ });
173
+
174
+ await test('the status page\'s recent list shows the page as A\'s, with B as the helper', async () => {
175
+ readerSql.length = 0;
176
+ const rows = await db.recentShippedTasks(30);
177
+ assert.equal(rows[0].github_login, 'artist-a');
178
+ const sql = readerSql[0];
179
+ assert.match(sql, /LEFT JOIN builders b ON b\.id = t\.shipped_by/, 'the shipper column is the author of record');
180
+ assert.ok(sql.includes(norm(rule.helperJoinSql('t'))), 'the helper is the shared join, not a second derivation');
181
+ assert.match(sql, /helper\.helper_login, helper\.helper_display_name/);
182
+ });
183
+
184
+ await test('the helper join names only a DIFFERENT applier, and only on a page tweak', () => {
185
+ const j = norm(rule.helperJoinSql('t'));
186
+ assert.match(j, /t\.source = 'page-tweak'/, 'never on an ordinary task');
187
+ assert.match(j, /c\.worktree_name IS NOT NULL/, 'the applier is the worktree claim');
188
+ assert.match(j, /ap\.builder_id IS DISTINCT FROM t\.shipped_by/, 'a self-applied round names no helper');
189
+ assert.match(j, /WHERE t\.source = 'page-tweak' AND c\.task_id = t\.id/, 'the source test guards the claims scan itself, so an ordinary row reads no claim');
190
+ const idx = readFileSync(new URL('../migrations/core_263_closeness_history_indexes.sql', import.meta.url), 'utf8');
191
+ assert.match(idx, /CREATE INDEX IF NOT EXISTS idx_claims_task_all\s+ON claims \(task_id\)/, 'a page tweak\'s per-row lookup is an index probe');
192
+ });
193
+
194
+ await test('the status page and the acclaim feed render the helper', () => {
195
+ const status = readFileSync(new URL('../modules/status-ui/public/status.js', import.meta.url), 'utf8');
196
+ assert.match(status, /t\.helper_display_name \|\| t\.helper_login/);
197
+ assert.match(status, /with help from \$\{helper\}/);
198
+ const hall = readFileSync(new URL('../modules/hall-ui/public/builders.js', import.meta.url), 'utf8');
199
+ assert.match(hall, /with help from \$\{escapeHtml\(helperName\)\}/, 'escaped before it reaches innerHTML');
200
+ const feed = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
201
+ assert.match(feed, /helper\.helper_login, helper\.helper_display_name/, 'the acclaim feed carries the helper');
202
+ });
203
+
204
+ await test('session upload coverage skips page tweaks: an artist runs no session', () => {
205
+ const s = readFileSync(new URL('../modules/sessions/db.js', import.meta.url), 'utf8');
206
+ const n = (s.match(/AND t\.source IS DISTINCT FROM 'page-tweak'/g) || []).length;
207
+ assert.equal(n, 2, 'both the coverage verdict and the missing-record list');
208
+ });
209
+
210
+ // =========================================================================
211
+ // ONE RULE. Economy cannot require the lifecycle helper, so its copy of the
212
+ // predicate is held to the helper's here; so are the Tweak Mode reads and the
213
+ // backfill migration.
214
+ await test('economy pays the same artist the ship now names', () => {
215
+ const credits = norm(readFileSync(new URL('../modules/economy/credits.js', import.meta.url), 'utf8'));
216
+ assert.ok(credits.includes(`WHERE c.task_id = t.id AND ${rule.webClaimPredicate('c')} ORDER BY ${rule.latestFirst('c')} LIMIT 1`),
217
+ 'awardArtistPageCredit reads the payee by the helper\'s predicate and order');
218
+ });
219
+
220
+ await test('the Tweak Mode reads take the predicate from the helper, not a literal', () => {
221
+ const reads = readFileSync(new URL('../modules/lifecycle/page-tweak-reads.js', import.meta.url), 'utf8');
222
+ assert.match(reads, /\$\{artistRule\.webClaimPredicate\('c'\)\}/);
223
+ assert.match(reads, /\$\{artistRule\.latestFirst\('c'\)\}/);
224
+ assert.ok(!/worktree_name IS NULL AND c\.creator_session_id IS NULL/.test(reads), 'no second spelling of the rule');
225
+ });
226
+
227
+ await test('the backfill brings already-shipped rounds to the same artist, idempotently', () => {
228
+ const mig = norm(readFileSync(new URL('../migrations/core_267_page_tweak_author_of_record.sql', import.meta.url), 'utf8').replace(/--[^\n]*/g, ''));
229
+ assert.ok(mig.includes(rule.webClaimPredicate('c')), 'the web-claim predicate');
230
+ assert.ok(mig.includes(`ORDER BY c.task_id, ${rule.latestFirst('c')}`), 'the latest-first order');
231
+ assert.match(mig, /pt\.source = 'page-tweak' AND pt\.status = 'shipped'/, 'shipped page tweaks only');
232
+ assert.match(mig, /t\.shipped_by IS DISTINCT FROM w\.builder_id/, 'a second run matches nothing');
233
+ assert.ok(!/credit_log/.test(mig), 'money is not touched');
234
+ });
235
+
236
+ summary();