@bongos/core 1.21.170 → 1.21.172

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.170",
6
- "core_contract": "1.21.170",
7
- "source_commit": "dd8ede0d02e94bf42ebeecabda31e50d04207496",
5
+ "core_version": "1.21.172",
6
+ "core_contract": "1.21.172",
7
+ "source_commit": "a5b5fc89c2bddcd402af46f6271b80c329759d8f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-23T17:21:55.040Z",
9
+ "built_at": "2026-10-23T17:54:54.483Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 609,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3831,
20
- "tree_sha256": "e66d52e5a8929945674ce60631b73e08c435660bf0228c776306ec586cc77464",
20
+ "tree_sha256": "3f3467a2aa833a4abbd6faf317d2656fb98738368a7376cdd163964d5f34a529",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2412,12 +2412,12 @@
2412
2412
  {
2413
2413
  "path": "docs/copy-inventory.md",
2414
2414
  "mode": "0000644",
2415
- "sha256": "6342b5c569de415d86db07c9e557eee201292149a4fe69d65bbee1b66eee2a70"
2415
+ "sha256": "eec10b588ffe284a8e52e0f5e167b24ffee253b336454d980906d1d299f789c2"
2416
2416
  },
2417
2417
  {
2418
2418
  "path": "docs/copy-registry.json",
2419
2419
  "mode": "0000644",
2420
- "sha256": "e2c97496bcaef11d81aba576f2dc6d50128ec1f04a5023ef02d7a67aabce07a0"
2420
+ "sha256": "f537f321c123a47d6c8a7e026293bac969b79ab648462f1fbaa51929c8282584"
2421
2421
  },
2422
2422
  {
2423
2423
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/module-api-changelog.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "69e717f42c7d4a55a1c985e886807078c1cc320c4650993ad7ecd5aba27d3d2c"
2930
+ "sha256": "af6395d2e0df7248fff770ebe525441b4af0ba73ff474328bfb69ace36f3180b"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/modules-contract.md",
@@ -3017,7 +3017,7 @@
3017
3017
  {
3018
3018
  "path": "docs/page-readings.json",
3019
3019
  "mode": "0000644",
3020
- "sha256": "60976ab4a0fa35bcdc1dcb4c14ddcd1221f7a6f98bd31f79c9b8bc458edb8ba8"
3020
+ "sha256": "4a1fa9bbf1b952ebabc5587a38f634de6fdee2678ba6b9c8ef6a6e0862dff820"
3021
3021
  },
3022
3022
  {
3023
3023
  "path": "docs/project-context.template.md",
@@ -4487,7 +4487,7 @@
4487
4487
  {
4488
4488
  "path": "modules/copy-desk/page-status.js",
4489
4489
  "mode": "0000644",
4490
- "sha256": "bf482381365aac38d983048f87597cc7d498ff320915502e4d5cb8ac3387d3e4"
4490
+ "sha256": "59a1cf0aeee19aee9595ab59b08bae6423bd98a60743eb3edec89e0dbe799c69"
4491
4491
  },
4492
4492
  {
4493
4493
  "path": "modules/copy-desk/pages.js",
@@ -4552,7 +4552,7 @@
4552
4552
  {
4553
4553
  "path": "modules/copy-desk/tests/copy_page_next.mjs",
4554
4554
  "mode": "0000644",
4555
- "sha256": "d82db6db97b22a88a83b4cf9bac7724a411dc7831b08cacd5548c0ac367ad3b9"
4555
+ "sha256": "29faf19e0c0c99cb749a048d4fb73c33dfdd9e363ee46fa6e00465b286cfb096"
4556
4556
  },
4557
4557
  {
4558
4558
  "path": "modules/copy-desk/tests/copy_page_status.mjs",
@@ -5627,7 +5627,7 @@
5627
5627
  {
5628
5628
  "path": "modules/hall-ui/public/brand-apply.js",
5629
5629
  "mode": "0000644",
5630
- "sha256": "0bc95cee336ca3c2f5c6666fe944100f01232c51a9f4d595ebdb410f8c514ed3"
5630
+ "sha256": "311edb9f328f02ee1d30a3711a149f9e168dd7d3b7a83ab4403250653ef8a8fe"
5631
5631
  },
5632
5632
  {
5633
5633
  "path": "modules/hall-ui/public/brand-holes.js",
@@ -6522,7 +6522,7 @@
6522
6522
  {
6523
6523
  "path": "modules/hall-ui/public/sky-data.js",
6524
6524
  "mode": "0000644",
6525
- "sha256": "7d509c43ab2d09f9b50ff588e86bab7ccdad7d28fafad023a13b1080cc745c37"
6525
+ "sha256": "a92f2804b755a0d4b3a28d4fdb9f8bc50e274cf04c4627b220f34c84a45a08ed"
6526
6526
  },
6527
6527
  {
6528
6528
  "path": "modules/hall-ui/public/sky-draw.js",
@@ -6542,7 +6542,7 @@
6542
6542
  {
6543
6543
  "path": "modules/hall-ui/public/sky-panel.js",
6544
6544
  "mode": "0000644",
6545
- "sha256": "257ec3fa3c0010d93ccddcb29e2ff0f635e566cd79a978307adfc89169ecaafe"
6545
+ "sha256": "a34bc56a1ebe3c65282f9f2e1d36b1102eda4cbf5fc26192239281aec0bf8f81"
6546
6546
  },
6547
6547
  {
6548
6548
  "path": "modules/hall-ui/public/sky-tour.js",
@@ -6627,7 +6627,7 @@
6627
6627
  {
6628
6628
  "path": "modules/hall-ui/public/studio-lib.js",
6629
6629
  "mode": "0000644",
6630
- "sha256": "2973743795c05e42824338c8b6ae9dbd046d112ab2bf3c10590d1ce478dba414"
6630
+ "sha256": "e8ff3230c35edc9315fa850f3875cdebb0d6f776a6d51a9b6cba18c0dd0ae303"
6631
6631
  },
6632
6632
  {
6633
6633
  "path": "modules/hall-ui/public/studio.css",
@@ -6642,7 +6642,7 @@
6642
6642
  {
6643
6643
  "path": "modules/hall-ui/public/studio.js",
6644
6644
  "mode": "0000644",
6645
- "sha256": "dec03b8694432f5add99b8dea4b304d02813fb29d59e116216bb7ae99cd77b6c"
6645
+ "sha256": "2b1adabd2d219c418ea335ad348668df75cc1f49673df3c5472c4107e8595691"
6646
6646
  },
6647
6647
  {
6648
6648
  "path": "modules/hall-ui/public/studio.states.json",
@@ -7807,7 +7807,7 @@
7807
7807
  {
7808
7808
  "path": "modules/lifecycle/sky.js",
7809
7809
  "mode": "0000644",
7810
- "sha256": "8945c7d22f8d0a4a094013dd1aa02791bdaf547467451fd0e827b87cb5a5d4ff"
7810
+ "sha256": "98011414e3e393667766a499bc8a7ad3f145d2c42563dca9ff6f35b8c4da16a2"
7811
7811
  },
7812
7812
  {
7813
7813
  "path": "modules/lifecycle/task-classifier.js",
@@ -10602,12 +10602,12 @@
10602
10602
  {
10603
10603
  "path": "package-lock.json",
10604
10604
  "mode": "0000644",
10605
- "sha256": "612aedb1a80183da059e896d4a3325c4672d4159009eda6329159b0cc23d9c34"
10605
+ "sha256": "11e80d0bbd4cbe40847b4b7b3185243caca27ca1eb62a23d16af9244aedf7d5f"
10606
10606
  },
10607
10607
  {
10608
10608
  "path": "package.json",
10609
10609
  "mode": "0000644",
10610
- "sha256": "9a75c884c3bbd879707469af51cac45699ae093714ed9a2176d6a95bfbb954e0"
10610
+ "sha256": "ac557c163c39ef7b845595788a40b41a256fa82143a06632ac9a0805479a8285"
10611
10611
  },
10612
10612
  {
10613
10613
  "path": "public-docs/index.html",
@@ -10627,7 +10627,7 @@
10627
10627
  {
10628
10628
  "path": "release-notes.json",
10629
10629
  "mode": "0000644",
10630
- "sha256": "bc246f86279741c379c800cd01e417876b45cd7b73adf48d0bb8c7204517376e"
10630
+ "sha256": "a0f68aef51f5fbd8e741303c53f90c1342c0c9cc19f99d8c9de37fce33edb894"
10631
10631
  },
10632
10632
  {
10633
10633
  "path": "scripts/cron/style_guide_audit.sh",
@@ -13017,7 +13017,7 @@
13017
13017
  {
13018
13018
  "path": "src/module-api.js",
13019
13019
  "mode": "0000644",
13020
- "sha256": "1792868060800cda8dd8d8f8426cd070d7083a0d5ae74f1b0964e238ccf79e4d"
13020
+ "sha256": "d91f9c6725d2c369f3af694a4e9b1ecada83994a2208961414c7814f0bf65b48"
13021
13021
  },
13022
13022
  {
13023
13023
  "path": "src/module-loader/catalog.js",
@@ -14917,7 +14917,7 @@
14917
14917
  {
14918
14918
  "path": "tests/goal_map_page.mjs",
14919
14919
  "mode": "0000644",
14920
- "sha256": "b5f49ea9a5adf0cc1909b45ebcf78b6c9806504a4194c90587c5a522b5839460"
14920
+ "sha256": "aa83961201bf3ff0757330b16a799b4de407d0cc2522282f18712dbf624a84ea"
14921
14921
  },
14922
14922
  {
14923
14923
  "path": "tests/goal_member_kinds.mjs",
@@ -15732,7 +15732,7 @@
15732
15732
  {
15733
15733
  "path": "tests/hall_sky.mjs",
15734
15734
  "mode": "0000644",
15735
- "sha256": "9a7eaea562272b7e06e07ac4251ee15aa4aeaa24180bc711c2dae4dd8a56fd8f"
15735
+ "sha256": "f798b71ce6f21026afc89bbca1e35fca06919d38f56eca1f8d8cfc75b3e9ac85"
15736
15736
  },
15737
15737
  {
15738
15738
  "path": "tests/hall_states_regex_escapes.mjs",
@@ -15772,7 +15772,7 @@
15772
15772
  {
15773
15773
  "path": "tests/hall_studio_home.mjs",
15774
15774
  "mode": "0000644",
15775
- "sha256": "6b6e7c34151808c223cc6cd95a01eee05038061dfbd71a7aa01500f90db757e3"
15775
+ "sha256": "2f8bb467008b2a8007627a50e7b9432924364688c24f9a2d8ef38f8966034fef"
15776
15776
  },
15777
15777
  {
15778
15778
  "path": "tests/hall_studio_world.mjs",
@@ -17887,7 +17887,7 @@
17887
17887
  {
17888
17888
  "path": "tests/reward_mode_surfacing.mjs",
17889
17889
  "mode": "0000644",
17890
- "sha256": "ed3538eb724a939eeb0619d3b61e5bf1b8ac4f389030fb4551c3045840018300"
17890
+ "sha256": "9d8e28d50abfb2b0e339f7b934a1ae230812340739bcdc13e4550f7e99d8659b"
17891
17891
  },
17892
17892
  {
17893
17893
  "path": "tests/role_responsibilities.mjs",
@@ -18462,7 +18462,7 @@
18462
18462
  {
18463
18463
  "path": "tests/sky_read.mjs",
18464
18464
  "mode": "0000644",
18465
- "sha256": "b9dced9cf04d54a59de637c91533083ea6f16d0d93304b13f1d40473d23895fe"
18465
+ "sha256": "238538864553803ed21a6330bf5a3570d1da8bd2f691af45b9eec1773a3bd8cd"
18466
18466
  },
18467
18467
  {
18468
18468
  "path": "tests/smoke_dependencies_witness.mjs",
@@ -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
- **2793 strings** across 3 surfaces · 2374 distinct pieces of copy · 2793 certain, 0 heuristic · 243 shown in more than one place.
7
+ **2794 strings** across 3 surfaces · 2375 distinct pieces of copy · 2794 certain, 0 heuristic · 243 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` | 150 | 1973 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 150 | 1974 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 9 | 720 |
16
16
 
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `8226c979db4d` | 3x | Not now | modules/hall-ui/public/collab.js:668, modules/hall-ui/public/paintbrush.js:119, modules/hall-ui/public/paintbrush.js:183 |
79
79
  | … | | _203 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1973 strings
81
+ ## `builders-hall` — 1974 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -590,9 +590,9 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
590
590
  | `320a0516a33e` | certain | Goals seeded — awaiting kickoff | `modules/hall-ui/public/roadmap.js:271` |
591
591
  | `bba4caf837e9` | certain | Goals — {{worldName}} | `modules/hall-ui/public/goals.html:6` |
592
592
  | `bba4caf837e9` | certain | Goals — {{worldName}} | `modules/hall-ui/public/goals.html:6` |
593
- | `d0f76a295d09` | certain | Good afternoon. | `modules/hall-ui/public/studio.js:960` |
593
+ | `d0f76a295d09` | certain | Good afternoon. | `modules/hall-ui/public/studio.js:968` |
594
594
  | `80fd51db38d7` | certain | Good evening. | `modules/hall-ui/public/studio.html:101` |
595
- | `36083f00396d` | certain | Good morning. | `modules/hall-ui/public/studio.js:959` |
595
+ | `36083f00396d` | certain | Good morning. | `modules/hall-ui/public/studio.js:967` |
596
596
  | `cab3de254155` | certain | Government — {{worldName}} | `modules/hall-ui/public/government.html:6` |
597
597
  | `cab3de254155` | certain | Government — {{worldName}} | `modules/hall-ui/public/government.html:6` |
598
598
  | `24d58d284952` | certain | Grades across all recent shipped work — a rolling aggregate. | `modules/hall-ui/public/watch.html:86` |
@@ -778,6 +778,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
778
778
  | `9dacaf382dc8` | certain | More ways to open the page | `modules/hall-ui/public/tweak-editor.html:59` |
779
779
  | `46fea78f33c6` | certain | Most recent first. Each load of this page is itself recorded in the audit log, and so is every transcript {…} | `modules/hall-ui/public/sessions.js:445` |
780
780
  | `f14a91b3f625` | certain | Most recent first. Each row is one recorded work session; every id on it opens its record. | `modules/hall-ui/public/sessions.html:56` |
781
+ | `2d5aea02c40e` | certain | Most words to write | `modules/hall-ui/public/studio.js:737` |
781
782
  | `e52ae74f0052` | certain | Move to goal | `modules/hall-ui/public/task.js:562` |
782
783
  | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:569` |
783
784
  | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:731` |
@@ -861,7 +862,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
861
862
  | `d5cf262e671d` | certain | Nobody is writing this page. Take it, and every line here is yours to type over. | `modules/hall-ui/public/tweak-editor.js:314` |
862
863
  | `91c409a4aea5` | certain | Nobody vouched for this name — anyone can file a request naming anyone. Confirm who is asking before you admit | `modules/hall-ui/public/watch.js:397` |
863
864
  | `4016b9cf8d57` | certain | None of your asks have been settled yet. | `modules/hall-ui/public/collab.html:134` |
864
- | `2dd1beb3b2ab` | certain | None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here | `modules/hall-ui/public/studio.js:738` |
865
+ | `2dd1beb3b2ab` | certain | None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here | `modules/hall-ui/public/studio.js:746` |
865
866
  | `36c97d6094bf` | certain | None yet. Yours will be the first. | `modules/hall-ui/public/lightbulb.js:94` |
866
867
  | `36c97d6094bf` | certain | None yet. Yours will be the first. | `modules/hall-ui/public/page-notes.js:177` |
867
868
  | `7b915c25289b` | certain | None. Governing is held, not picked: you steer your own agents. | `modules/hall-ui/public/my-agents.js:371` |
@@ -1106,7 +1107,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1106
1107
  | `f1b2b158c3b1` | certain | Review quality | `modules/hall-ui/public/watch.html:85` |
1107
1108
  | `4e062d4f8d69` | certain | Reviews by builder | `modules/hall-ui/public/watch.html:78` |
1108
1109
  | `5c233879ce4e` | certain | Reviews from before Tweak Mode | `modules/hall-ui/public/studio.html:165` |
1109
- | `4c112975a739` | certain | Rewrite it in the editor | `modules/hall-ui/public/studio.js:835` |
1110
+ | `4c112975a739` | certain | Rewrite it in the editor | `modules/hall-ui/public/studio.js:843` |
1110
1111
  | `c56d9a6451a7` | certain | Rewrite them in the tweak editor | `modules/hall-ui/public/paintbrush.js:252` |
1111
1112
  | `f7ed4ba07178` | certain | Roadmap — {{worldName}} | `modules/hall-ui/public/roadmap.html:6` |
1112
1113
  | `f7ed4ba07178` | certain | Roadmap — {{worldName}} | `modules/hall-ui/public/roadmap.html:6` |
@@ -1383,7 +1384,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1383
1384
  | `2287c62afcdd` | certain | The street | `modules/hall-ui/public/street.html:103` |
1384
1385
  | `fdc4b3013936` | certain | The street — {{worldName}} | `modules/hall-ui/public/street.html:6` |
1385
1386
  | `fdc4b3013936` | certain | The street — {{worldName}} | `modules/hall-ui/public/street.html:6` |
1386
- | `d00666a91ac4` | certain | The studio page is yours, but its words could not be read just now. Open it in the editor. | `modules/hall-ui/public/studio.js:906` |
1387
+ | `d00666a91ac4` | certain | The studio page is yours, but its words could not be read just now. Open it in the editor. | `modules/hall-ui/public/studio.js:914` |
1387
1388
  | `7d016f4a65bf` | certain | The system is mid-build. These diagrams deliberately separate what stands today from what is yet planned, so t | `modules/hall-ui/public/diagrams.html:40` |
1388
1389
  | `599fd0e82d1c` | certain | The task board — all work | `modules/hall-ui/public/studio.html:178` |
1389
1390
  | `198ba9353bf6` | certain | The task feed did not answer. Nothing on the record has changed, only the read failed. Try again in a moment, | `modules/hall-ui/public/task.js:1015` |
@@ -1836,7 +1837,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1836
1837
  | `1e737a57eb20` | certain | start governing | `modules/hall-ui/public/city.html:99` |
1837
1838
  | `e294c3429c2e` | certain | strong at {…} | `modules/hall-ui/public/modules.js:340` |
1838
1839
  | `0215b2cdca8c` | certain | submit for review | `modules/hall-ui/public/modules.js:286` |
1839
- | `43e6f1592ae3` | certain | submit it in the editor | `modules/hall-ui/public/studio.js:836` |
1840
+ | `43e6f1592ae3` | certain | submit it in the editor | `modules/hall-ui/public/studio.js:844` |
1840
1841
  | `9ec19e32f86f` | certain | submit page | `modules/hall-ui/public/tweak-editor.html:68` |
1841
1842
  | `da30b952a60b` | certain | system / unattributed | `modules/hall-ui/public/goals-page.js:284` |
1842
1843
  | `da30b952a60b` | certain | system / unattributed | `modules/hall-ui/public/work.js:581` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2793,
7
- "unique_ids": 2374,
8
- "certain": 2793,
6
+ "strings": 2794,
7
+ "unique_ids": 2375,
8
+ "certain": 2794,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 243
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 150,
16
- "strings": 1973
16
+ "strings": 1974
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -7223,7 +7223,7 @@
7223
7223
  "surface": "builders-hall",
7224
7224
  "text": "Good afternoon.",
7225
7225
  "file": "modules/hall-ui/public/studio.js",
7226
- "line": 960,
7226
+ "line": 968,
7227
7227
  "origin": "js-text-assign",
7228
7228
  "confidence": "certain"
7229
7229
  },
@@ -7241,7 +7241,7 @@
7241
7241
  "surface": "builders-hall",
7242
7242
  "text": "Good morning.",
7243
7243
  "file": "modules/hall-ui/public/studio.js",
7244
- "line": 959,
7244
+ "line": 967,
7245
7245
  "origin": "js-text-assign",
7246
7246
  "confidence": "certain"
7247
7247
  },
@@ -8910,6 +8910,15 @@
8910
8910
  "origin": "html-text",
8911
8911
  "confidence": "certain"
8912
8912
  },
8913
+ {
8914
+ "id": "2d5aea02c40e",
8915
+ "surface": "builders-hall",
8916
+ "text": "Most words to write",
8917
+ "file": "modules/hall-ui/public/studio.js",
8918
+ "line": 737,
8919
+ "origin": "js-markup",
8920
+ "confidence": "certain"
8921
+ },
8913
8922
  {
8914
8923
  "id": "e52ae74f0052",
8915
8924
  "surface": "builders-hall",
@@ -9662,7 +9671,7 @@
9662
9671
  "surface": "builders-hall",
9663
9672
  "text": "None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here.",
9664
9673
  "file": "modules/hall-ui/public/studio.js",
9665
- "line": 738,
9674
+ "line": 746,
9666
9675
  "origin": "js-markup",
9667
9676
  "confidence": "certain"
9668
9677
  },
@@ -11867,7 +11876,7 @@
11867
11876
  "surface": "builders-hall",
11868
11877
  "text": "Rewrite it in the editor",
11869
11878
  "file": "modules/hall-ui/public/studio.js",
11870
- "line": 835,
11879
+ "line": 843,
11871
11880
  "origin": "js-markup",
11872
11881
  "confidence": "certain"
11873
11882
  },
@@ -14360,7 +14369,7 @@
14360
14369
  "surface": "builders-hall",
14361
14370
  "text": "The studio page is yours, but its words could not be read just now. Open it in the editor.",
14362
14371
  "file": "modules/hall-ui/public/studio.js",
14363
- "line": 906,
14372
+ "line": 914,
14364
14373
  "origin": "js-text-assign",
14365
14374
  "confidence": "certain"
14366
14375
  },
@@ -18437,7 +18446,7 @@
18437
18446
  "surface": "builders-hall",
18438
18447
  "text": "submit it in the editor",
18439
18448
  "file": "modules/hall-ui/public/studio.js",
18440
- "line": 836,
18449
+ "line": 844,
18441
18450
  "origin": "js-markup",
18442
18451
  "confidence": "certain"
18443
18452
  },
@@ -3177,5 +3177,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3177
3177
  landed since 1.21.168 with no explicit bump. run 39992064615. (task 1002620)
3178
3178
  1.21.170 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3179
3179
  landed since 1.21.169 with no explicit bump. run 39994339257. (task 1002620)
3180
+ 1.21.171 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3181
+ landed since 1.21.170 with no explicit bump. run 39996371081. (task 1002620)
3182
+ 1.21.172 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3183
+ landed since 1.21.171 with no explicit bump. run 39998870016. (task 1002620)
3180
3184
  ---------------------------------------------------------------------------
3181
3185
  ```
@@ -50,7 +50,7 @@
50
50
  "surface": "builders",
51
51
  "title": "Blockers",
52
52
  "states_read": ["default"],
53
- "files_hash": "be570fb08f33cde4",
53
+ "files_hash": "f5bf3c2244e3fbdd",
54
54
  "reading_hash": "2b3fd98b7ba1bff6",
55
55
  "counts": {"lines":28,"placed":12,"shared":8,"unplaced":8},
56
56
  "omitted": {"data":9},
@@ -1141,7 +1141,7 @@
1141
1141
  "surface": "builders",
1142
1142
  "title": "The sky",
1143
1143
  "states_read": ["tour","sky","wells","ledger","full","full-back","out"],
1144
- "files_hash": "2dce7dcbe58dcf5d",
1144
+ "files_hash": "5f2c2f22c2fc9dc6",
1145
1145
  "reading_hash": "b83eb8c92a23fd3c",
1146
1146
  "counts": {"lines":47,"placed":23,"shared":7,"unplaced":17},
1147
1147
  "omitted": {"data":8},
@@ -1201,7 +1201,7 @@
1201
1201
  "surface": "builders",
1202
1202
  "title": "Goals",
1203
1203
  "states_read": ["list","filters","cards","detail","dependencies","out","uat","uat-panel","author"],
1204
- "files_hash": "ca38b540196d951b",
1204
+ "files_hash": "4f6f4eb5e9f00412",
1205
1205
  "reading_hash": "7aa1a6e1946dc077",
1206
1206
  "counts": {"lines":156,"placed":52,"shared":8,"unplaced":96},
1207
1207
  "omitted": {"data":268},
@@ -1326,7 +1326,7 @@
1326
1326
  {"key":"L0117","section":"Goals","text":"Version","placement":"unplaced"},
1327
1327
  {"key":"L0118","section":"Goals","text":"— any —","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":173,"string_id":"729d4ce22b99","occurrences":2},
1328
1328
  {"key":"L0119","section":"Goals","text":"Status","placement":"unplaced"},
1329
- {"key":"L0120","section":"Goals","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":164,"string_id":"9a803563c948","occurrences":16},
1329
+ {"key":"L0120","section":"Goals","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":166,"string_id":"9a803563c948","occurrences":16},
1330
1330
  {"key":"L0121","section":"Goals","text":"Sort","placement":"unplaced"},
1331
1331
  {"key":"L0122","section":"Goals","text":"priority (version order)","placement":"unplaced"},
1332
1332
  {"key":"L0123","section":"Goals","text":"newest first","placement":"unplaced"},
@@ -1439,7 +1439,7 @@
1439
1439
  "surface": "builders",
1440
1440
  "title": "Builders' Hall",
1441
1441
  "states_read": ["home","standing","standing-karma","out","founding","genesis","decide","demo"],
1442
- "files_hash": "eb1e8c3a4621add8",
1442
+ "files_hash": "e32eb071e46bec38",
1443
1443
  "reading_hash": "42dbab4f3948fe66",
1444
1444
  "counts": {"lines":117,"placed":63,"shared":10,"unplaced":44},
1445
1445
  "omitted": {"data":101},
@@ -1569,7 +1569,7 @@
1569
1569
  "surface": "builders",
1570
1570
  "title": "Idea",
1571
1571
  "states_read": ["record","missing"],
1572
- "files_hash": "5b7084faad7f4ded",
1572
+ "files_hash": "7456ea53572161d4",
1573
1573
  "reading_hash": "c9f9a84ab844c704",
1574
1574
  "counts": {"lines":52,"placed":23,"shared":9,"unplaced":20},
1575
1575
  "omitted": {"data":18},
@@ -1609,7 +1609,7 @@
1609
1609
  {"key":"L0032","section":"Everything that fell out of it","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","state":"missing"},
1610
1610
  {"key":"L0033","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc","state":"missing","occurrences":2},
1611
1611
  {"key":"L0034","section":"Everything that fell out of it","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":674,"string_id":"fd1903586e8e","state":"missing"},
1612
- {"key":"L0035","section":"12A changelog every builder can read","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":164,"string_id":"9a803563c948"},
1612
+ {"key":"L0035","section":"12A changelog every builder can read","text":"open","placement":"placed","file":"modules/hall-ui/public/approval-queue.js","line":166,"string_id":"9a803563c948"},
1613
1613
  {"key":"L0036","section":"12A changelog every builder can read","text":"had the thought · {…}","placement":"unplaced"},
1614
1614
  {"key":"L0037","section":"12A changelog every builder can read","text":"{…}/100 complete","placement":"placed","file":"modules/hall-ui/public/idea.js","line":87,"string_id":"c43050b62297"},
1615
1615
  {"key":"L0038","section":"12A changelog every builder can read","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":4},
@@ -1719,7 +1719,7 @@
1719
1719
  "surface": "builders",
1720
1720
  "title": "Modules",
1721
1721
  "states_read": ["browse","search-empty","lens","details","out"],
1722
- "files_hash": "ce529d9f575606e5",
1722
+ "files_hash": "6dc5460706966349",
1723
1723
  "reading_hash": "a17f501b72321b7b",
1724
1724
  "counts": {"lines":62,"placed":29,"shared":8,"unplaced":25},
1725
1725
  "omitted": {"data":106},
@@ -2325,7 +2325,7 @@
2325
2325
  "surface": "builders",
2326
2326
  "title": "Profile",
2327
2327
  "states_read": ["own","works","activity","activity-artist","activity-ideator","other","artist","ideator","out"],
2328
- "files_hash": "deca408267a32df9",
2328
+ "files_hash": "82a35b34a5115325",
2329
2329
  "reading_hash": "8088d60a4e93bb91",
2330
2330
  "counts": {"lines":196,"placed":70,"shared":8,"unplaced":118},
2331
2331
  "omitted": {"data":197},
@@ -2721,7 +2721,7 @@
2721
2721
  "surface": "builders",
2722
2722
  "title": "Builders",
2723
2723
  "states_read": ["roster","artists","search","out"],
2724
- "files_hash": "38086c3c20a8cd6d",
2724
+ "files_hash": "88e2f7aca1994346",
2725
2725
  "reading_hash": "5c7e507e9600e59c",
2726
2726
  "counts": {"lines":47,"placed":13,"shared":8,"unplaced":26},
2727
2727
  "omitted": {"data":60},
@@ -2781,7 +2781,7 @@
2781
2781
  "surface": "builders",
2782
2782
  "title": "Sessions",
2783
2783
  "states_read": ["ledger","out"],
2784
- "files_hash": "b5f59d12801a60d9",
2784
+ "files_hash": "cd14ebd4d35e16bf",
2785
2785
  "reading_hash": "5c18aaed7f2e8e0f",
2786
2786
  "counts": {"lines":331,"placed":21,"shared":8,"unplaced":302},
2787
2787
  "omitted": {"data":303},
@@ -3978,7 +3978,7 @@
3978
3978
  "surface": "builders",
3979
3979
  "title": "Studio",
3980
3980
  "states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back"],
3981
- "files_hash": "a072c3fdf156d639",
3981
+ "files_hash": "b9928eadcd3c7524",
3982
3982
  "reading_hash": "55e9090fdd96a979",
3983
3983
  "counts": {"lines":30,"placed":16,"shared":6,"unplaced":8},
3984
3984
  "omitted": {"data":4},
@@ -4021,7 +4021,7 @@
4021
4021
  "surface": "builders",
4022
4022
  "title": "Task",
4023
4023
  "states_read": ["record","from-goal","missing"],
4024
- "files_hash": "8200eeef7a793659",
4024
+ "files_hash": "5535b9790b56985e",
4025
4025
  "reading_hash": "3e80d092af1d7e2c",
4026
4026
  "counts": {"lines":132,"placed":35,"shared":10,"unplaced":87},
4027
4027
  "omitted": {"data":75},
@@ -4166,7 +4166,7 @@
4166
4166
  "surface": "builders",
4167
4167
  "title": "Your thinking",
4168
4168
  "states_read": ["tour","sky","list","simple","full-idea","full","full-list","full-back","out"],
4169
- "files_hash": "f8061c0641b23730",
4169
+ "files_hash": "a7b04635b758d676",
4170
4170
  "reading_hash": "532805c03c3a8a4c",
4171
4171
  "counts": {"lines":48,"placed":23,"shared":7,"unplaced":18},
4172
4172
  "omitted": {"data":8},
@@ -4263,7 +4263,7 @@
4263
4263
  "surface": "builders",
4264
4264
  "title": "Watch",
4265
4265
  "states_read": ["board","out","operations","security"],
4266
- "files_hash": "c3baec6343c9867a",
4266
+ "files_hash": "3980ad82a03bc297",
4267
4267
  "reading_hash": "d698ac62c904aa65",
4268
4268
  "counts": {"lines":88,"placed":38,"shared":8,"unplaced":42},
4269
4269
  "omitted": {"data":166},
@@ -4364,7 +4364,7 @@
4364
4364
  "surface": "builders",
4365
4365
  "title": "Tasks",
4366
4366
  "states_read": ["ready","in-progress","backlog","completed","filters-open","out","founding"],
4367
- "files_hash": "2c4ec39162b32427",
4367
+ "files_hash": "1dcb08711f496752",
4368
4368
  "reading_hash": "a425c7a090d8bf03",
4369
4369
  "counts": {"lines":125,"placed":54,"shared":8,"unplaced":63},
4370
4370
  "omitted": {"data":316},
@@ -242,6 +242,32 @@ function changelogFor(rounds) {
242
242
  // Composition.
243
243
  // ---------------------------------------------------------------------------
244
244
 
245
+ // task 1004670: how much text a page shows, the measure the worst-pages report
246
+ // and the copy queue rank by. Distinct non-shared lines of its reading: shell
247
+ // text every page carries is no page's own work, and a line a page repeats in
248
+ // several states is written once. null when the page has not been read.
249
+ function textLinesOf(readingPage) {
250
+ if (!readingPage || !Array.isArray(readingPage.lines)) return null;
251
+ return new Set(readingPage.lines.filter((l) => l && l.placement !== 'shared').map((l) => String(l.text))).size;
252
+ }
253
+
254
+ // The worst-pages report (task 1004670): the pages with the most text first,
255
+ // each with its status, so the copy queue's priority is visible in the Studio.
256
+ const WORST_PAGES = 5;
257
+ function worstPagesFor(inventory, rIdx, list) {
258
+ const byId = new Map(list.map((p) => [p.id, p]));
259
+ return ((inventory && inventory.pages) || [])
260
+ .filter((p) => p && p.id)
261
+ .map((p, i) => ({ p, i, n: textLinesOf(rIdx.get(p.id)) }))
262
+ .filter((x) => x.n != null && x.n > 0)
263
+ .sort((a, b) => b.n - a.n || a.i - b.i)
264
+ .slice(0, WORST_PAGES)
265
+ .map(({ p, n }) => ({
266
+ page_id: p.id, title: p.title || p.id, surface: p.surface, path: p.path || null,
267
+ text_lines: n, status_label: (byId.get(p.id) || {}).status_label || null,
268
+ }));
269
+ }
270
+
245
271
  function readingIndex(readings) {
246
272
  const m = new Map();
247
273
  for (const p of (readings && readings.pages) || []) if (p && p.id) m.set(p.id, p);
@@ -393,6 +419,7 @@ function composePages({ inventory, readings, tasks, now = Date.now() }) {
393
419
  surfaces,
394
420
  totals: { tweaked, total: list.length, line: `${tweaked} of ${list.length} pages tweaked` },
395
421
  in_flight: inFlightFor(list, byPage, now instanceof Date ? now.getTime() : Number(now)),
422
+ worst_pages: worstPagesFor(inventory, rIdx, list),
396
423
  };
397
424
  }
398
425
 
@@ -575,6 +602,8 @@ function candidateFor({ page, rounds, readingPage, readingsOk, ask, builderId, a
575
602
  askers,
576
603
  oldest_ask_at: ask ? iso(ask.oldest_ask_at) : null,
577
604
  changed_lines: changedLines,
605
+ // task 1004670: more text ranks a page higher within its tier.
606
+ text_lines: textLinesOf(readingPage) || 0,
578
607
  drift_known: drift.known,
579
608
  count: counts.count,
580
609
  status_label: counts.status_label,
@@ -594,6 +623,9 @@ function compareCandidates(a, b) {
594
623
  if (at !== bt) return at - bt;
595
624
  }
596
625
  if (a.tier === 'changed' && a.changed_lines !== b.changed_lines) return b.changed_lines - a.changed_lines;
626
+ // task 1004670: within a tier, the page with the most text leads (the
627
+ // worst-pages report's measure), then the inventory's own order.
628
+ if (a.text_lines !== b.text_lines) return b.text_lines - a.text_lines;
597
629
  return a._base - b._base;
598
630
  }
599
631
 
@@ -922,6 +954,8 @@ module.exports = {
922
954
  roundState,
923
955
  changelogFor,
924
956
  composePages,
957
+ textLinesOf,
958
+ WORST_PAGES,
925
959
  composePage,
926
960
  lastRoundShippedAt,
927
961
  composeTally,
@@ -92,11 +92,12 @@ const order = (out) => [out.recommended, ...out.alternatives].filter(Boolean).ma
92
92
  const EXPECTED = [
93
93
  'builders:work', 'landing:privacy', 'builders:tasks',
94
94
  'builders:goals', 'status:board',
95
- 'landing:index', 'builders:ideas', 'status:explore',
95
+ // task 1004670: within a tier the page with the most text leads, then surface order.
96
+ 'landing:index', 'status:explore', 'builders:ideas',
96
97
  'landing:projects',
97
98
  ];
98
99
 
99
- await test('the order on the fixture: asked, changed, untweaked (surface order), tweaked last', () => {
100
+ await test('the order on the fixture: asked, changed, untweaked (most text first), tweaked last', () => {
100
101
  const out = next(F.ALICE.id);
101
102
  assert.deepEqual(order(out), EXPECTED);
102
103
  assert.deepEqual([...new Set([out.recommended, ...out.alternatives].map((c) => c.tier))], ['asked', 'changed', 'untweaked', 'tweaked']);
@@ -180,5 +181,32 @@ await test('nothing left to take: recommended is null, never a held page', () =>
180
181
  assert.equal(out.skipped.length, 2);
181
182
  });
182
183
 
184
+ // task 1004670: the worst-pages report. Pages with the most text lead the copy
185
+ // queue within their tier, and the Studio's page list names the worst ones.
186
+ await test('more text leads within a tier; shell text and repeats do not count', () => {
187
+ const L = (text, placement = 'placed') => ({ key: 'k', section: 's', text, placement });
188
+ const inventory = { surfaces: [{ id: 'landing' }], pages: [
189
+ { id: 'landing:a', surface: 'landing', title: 'A', path: '/a', files: [] },
190
+ { id: 'landing:b', surface: 'landing', title: 'B', path: '/b', files: [] },
191
+ { id: 'landing:c', surface: 'landing', title: 'C', path: '/c', files: [] },
192
+ ] };
193
+ const readings = { pages: [
194
+ { id: 'landing:a', reading_hash: 'a', lines: [L('one'), L('Skip', 'shared'), L('Nav', 'shared'), L('Footer', 'shared')] },
195
+ { id: 'landing:b', reading_hash: 'b', lines: [L('one'), L('two'), L('three'), L('two')] },
196
+ { id: 'landing:c', reading_hash: 'c', lines: [L('one'), L('two')] },
197
+ ] };
198
+ assert.equal(status.textLinesOf(readings.pages[0]), 1, 'shared shell text is no page\'s own');
199
+ assert.equal(status.textLinesOf(readings.pages[1]), 3, 'a repeated line is written once');
200
+ assert.equal(status.textLinesOf(null), null);
201
+ const out = status.composeNext({ inventory, readings, tasks: [], asks: [], builderId: F.ALICE.id, alternatives: 9 });
202
+ assert.deepEqual(order(out), ['landing:b', 'landing:c', 'landing:a']);
203
+ assert.deepEqual([out.recommended.text_lines, out.recommended.tier], [3, 'untweaked']);
204
+
205
+ const pagesOut = status.composePages({ inventory, readings, tasks: [] });
206
+ assert.deepEqual(pagesOut.worst_pages.map((w) => [w.page_id, w.text_lines, w.status_label]),
207
+ [['landing:b', 3, 'Untweaked'], ['landing:c', 2, 'Untweaked'], ['landing:a', 1, 'Untweaked']]);
208
+ assert.deepEqual(status.composePages({ inventory, readings: null, tasks: [] }).worst_pages, [], 'unread pages are not ranked');
209
+ });
210
+
183
211
  console.log(`\ncopy_page_next: ${passed} passed, ${failed} failed`);
184
212
  if (failed) process.exit(1);
@@ -91,7 +91,7 @@
91
91
  // estimate with a "<Currency> Est." label; settled → the actual ledger payout
92
92
  // with a plain "<Currency>" label.
93
93
  //
94
- // task 1002469 — on a cost-plus-only instance that second case is a LIE. The
94
+ // task 1002469 (superseded by task 1004781) — on a cost-plus-only instance that second case was a LIE. The
95
95
  // per-task estimate is never paid, so credits_awarded is 0 on every shipped
96
96
  // task, taskCreditValue's zero-fallback hands back the estimate, and the plain
97
97
  // label then presents that estimate as the actual payout. So the "Est."
@@ -105,18 +105,25 @@
105
105
  function taskCreditValue(task) {
106
106
  if (!task) return null;
107
107
  if (task.status === 'abandoned' || task.merged_into_task_id != null) return 0;
108
+ // task 1004781 — a settled task is worth what the ledger PAID for it and
109
+ // nothing else. It used to fall back to the estimate when the ledger held
110
+ // 0, and on a cost-plus-only instance that is every shipped task, so goal
111
+ // totals summed guesses as money (a goal read 3,373 for a builder paid
112
+ // 776). A settled 0 is the truth: the task itself paid nothing. Only
113
+ // unsettled work carries the estimate, and taskCreditLabel marks it "Est.".
114
+ // The decision the task asked for: estimates are KEPT, for live work only —
115
+ // they still rank claimable work (task-lib's best-value sort) and tell a
116
+ // builder roughly how big a task is — but they never reach a total.
108
117
  if (taskSettled(task)) {
109
118
  var awarded = Number(task.credits_awarded);
110
- // Settled with a real ledger payout → show it. If a settled task has no
111
- // ledger record (0 — legacy pre-ledger tasks, or reward-exempt), fall back
112
- // to the estimate rather than showing a misleading "0".
113
- if (isFinite(awarded) && awarded > 0) return awarded;
119
+ return isFinite(awarded) && awarded > 0 ? awarded : 0;
114
120
  }
115
121
  return task.credits_reward;
116
122
  }
117
123
  function taskCreditLabel(task, opts) {
124
+ // A settled figure is now always the actual payout (see taskCreditValue),
125
+ // so it takes the plain label on every reward mode; only an estimate is "Est.".
118
126
  var base = currencyLabel(opts);
119
- if (!perTaskCredit()) return base + ' Est.';
120
127
  return taskSettled(task) ? base : base + ' Est.';
121
128
  }
122
129
 
@@ -142,15 +142,16 @@
142
142
  // The prototype read a snapshot (docs/design/mocks/sky/snapshot.js). Each
143
143
  // adaptor maps one real read onto the same field names, so the world below
144
144
  // is the prototype's world line for line.
145
+ const SETTLED = new Set(['confirmed', 'shipped']);
145
146
  function goalFromSky(g) {
146
147
  return {
147
148
  id: String(g.goal_id), title: g.title || 'A private goal', subtitle: g.subtitle || null, version: g.version_id, status: g.status,
148
149
  taskTotal: Number(g.task_total) || 0, taskDone: Number(g.task_done) || 0, createdAt: g.created_at || null,
149
150
  crit: (g.criteria || []).map((c) => ({ id: String(c.id), text: c.criterion_md || '', satisfied: c.satisfied === true, tasks: (c.task_ids || []).map(String), pendingReview: c.pending_review === true, uat: c.uat_state || null, uatLabel: uatLabel(c.uat_state), backendOnly: c.backend_only === true })),
150
- // task 1004782 — abandoned (or merged-away) work carries no credits: it is listed, at zero, and never adds to a
151
- // well's mass or a ring. A merged-away task is always abandoned (tasks.merged_into_task_id is CHECK-constrained to
152
- // abandoned rows), so the status test covers both; /sky does not send the pointer.
153
- tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: t.status === 'abandoned' || t.credits_reward == null ? 0 : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
151
+ // `credits` is what the ledger PAID — settled work only (task 1004781) — so a well's mass and a ring's weight are
152
+ // money paid. Live work carries its guess as `estimate`, shown "Est." and never added to a total. Abandoned (or
153
+ // merged-away, which is always abandoned — CHECK-constrained) work has neither (task 1004782).
154
+ tasks: (g.tasks || []).map((t) => ({ id: String(t.id), title: t.title || 'a task', status: t.status, credits: SETTLED.has(t.status) ? Number(t.credits_awarded) || 0 : 0, estimate: SETTLED.has(t.status) || t.status === 'abandoned' || t.credits_reward == null ? null : Number(t.credits_reward), createdAt: t.created_on || null, sourceRef: t.idea_id != null ? String(t.idea_id) : null })),
154
155
  };
155
156
  }
156
157
  const taskRow = (t) => ({ id: String(t.task_id ?? t.id), title: t.title || 'a task', status: t.status, goalId: t.goal_id != null ? String(t.goal_id) : null, origin: t.origin || null });
@@ -98,7 +98,7 @@
98
98
  <div class="facts">
99
99
  ${g ? fact('well', esc(g.title)) : ''}
100
100
  ${crit ? fact('gates the ring', esc(plain(crit.text).slice(0, 90))) : g ? fact('gates the ring', 'none — outer orbit') : ''}
101
- ${t.credits != null ? fact('credits', String(t.credits), true) : ''}
101
+ ${t.estimate != null ? fact('credits', `${t.estimate} <span class="lbl">Est.</span>`, true) : t.credits != null ? fact('credits paid', String(t.credits), true) : ''}
102
102
  ${t.createdAt ? fact('born', esc(t.createdAt)) : ''}
103
103
  ${t.sourceRef ? fact('born of a thought', `<a href="${HALL}/idea/${esc(t.sourceRef)}" target="_blank" rel="noopener">${esc(ideaTitle(t.sourceRef))}</a>`) : ''}
104
104
  </div>
@@ -171,6 +171,18 @@
171
171
  });
172
172
  }
173
173
 
174
+ // The worst-pages report (task 1004670): the pages with the most text, from
175
+ // the server's own list, most first. The panel only words the count.
176
+ function worstRows(pages) {
177
+ return ((pages && pages.worst_pages) || []).map((p) => ({
178
+ page_id: p.page_id,
179
+ title: titleOf(p),
180
+ surface: surfaceWord(p.surface),
181
+ size: `${num(p.text_lines)} ${num(p.text_lines) === 1 ? 'line' : 'lines'} of text`,
182
+ status: p.status_label || '',
183
+ }));
184
+ }
185
+
174
186
  function askedLine(n) {
175
187
  const v = num(n);
176
188
  if (v <= 0) return '';
@@ -241,7 +253,7 @@
241
253
 
242
254
  const api = {
243
255
  STUDIO_PAGE, PAGE_ID_RE, SURFACE_WORDS, surfaceWord, titleOf, numberWords, tallyParts, resumeDetail,
244
- subParts, flowDetail, statusDetail, apexHost, landingHost, surfaceRows, inFlightRows, askedLine, pickLabel, matchLine, claimWords,
256
+ subParts, flowDetail, statusDetail, apexHost, landingHost, surfaceRows, inFlightRows, worstRows, askedLine, pickLabel, matchLine, claimWords,
245
257
  skyAt, dayPart, STOCK_GREETING, clockGreets,
246
258
  };
247
259
  if (typeof window !== 'undefined') window.OTBStudioLib = api;
@@ -727,7 +727,15 @@
727
727
  <span class="st-flight__state">${escapeHtml(f.state)}${f.age ? ` · ${escapeHtml(f.age)}` : ''}${f.stuck ? ' · stuck' : ''}</span>
728
728
  </li>`).join('');
729
729
  const inFlight = flight ? `<h3 class="st-flight__head">Where your pages are</h3><ul class="st-flights">${flight}</ul>` : '';
730
- return `<ul class="st-surfaces">${rows}</ul>${asked ? `<p class="st-asked">${escapeHtml(asked)}</p>` : ''}${inFlight}`;
730
+ // task 1004670: the worst-pages report, the pages with the most text first.
731
+ const worst = lib.worstRows(home.pages).map((w) => `
732
+ <li class="st-flight">
733
+ <a class="st-flight__page" href="${escapeHtml(editorHref(w.page_id))}">${escapeHtml(w.title)}</a>
734
+ <span class="st-flight__where">${escapeHtml(w.surface)}</span>
735
+ <span class="st-flight__state">${escapeHtml(w.size)}${w.status ? ` · ${escapeHtml(w.status.toLowerCase())}` : ''}</span>
736
+ </li>`).join('');
737
+ const worstList = worst ? `<h3 class="st-flight__head">Most words to write</h3><ul class="st-flights st-worst">${worst}</ul>` : '';
738
+ return `<ul class="st-surfaces">${rows}</ul>${asked ? `<p class="st-asked">${escapeHtml(asked)}</p>` : ''}${inFlight}${worstList}`;
731
739
  }
732
740
 
733
741
  function siteHtml() {
@@ -44,6 +44,7 @@ const goalGraph = require('./goal-graph');
44
44
  const goalCategory = require('./goal-category');
45
45
  const { isPendingReview, uatState, uatReadColumnsSql } = require('./done-when');
46
46
  const { TERMINAL_STATUSES } = require('./task-status');
47
+ const { CREDITS_AWARDED_SUBQUERY } = require('./db-tasks');
47
48
 
48
49
  const SKY_CACHE_TTL_MS = 60 * 1000;
49
50
 
@@ -92,9 +93,12 @@ async function skyRows({ versionId = null, viewerId = null, viewerIsArchon = fal
92
93
  // Compact by construction: no description, no value summary, no notes.
93
94
  // created_at is truncated to the DAY in SQL so the payload carries ten
94
95
  // characters per task, not twenty-seven, and the clock draws by day anyway.
96
+ // credits_awarded (task 1004781) is the shared settled-only payout subquery; its
97
+ // per-row lookup rides idx_credit_log_task (core_189), never a scan of the log.
95
98
  `SELECT t.id::text AS id, t.goal_id::text AS goal_id, t.title, t.status,
96
99
  t.credits_reward, t.created_at::date::text AS created_on,
97
- CASE WHEN t.source_ref ~ '^idea:[0-9]+$' THEN substring(t.source_ref from 6) END AS idea_id
100
+ CASE WHEN t.source_ref ~ '^idea:[0-9]+$' THEN substring(t.source_ref from 6) END AS idea_id,
101
+ ${CREDITS_AWARDED_SUBQUERY} AS credits_awarded
98
102
  FROM tasks t
99
103
  JOIN goals g ON g.id = t.goal_id
100
104
  WHERE ($1::text IS NULL OR g.version_id = $1)
@@ -189,6 +193,9 @@ function shapeSky({ goals = [], edges = [], extras = [], criteria = [], tasks =
189
193
  title: visible ? t.title : null,
190
194
  status: t.status,
191
195
  credits_reward: t.credits_reward == null ? null : Number(t.credits_reward),
196
+ // task 1004781 — what the ledger actually paid (0 until settled), so the
197
+ // sky sizes a well by money paid, never by the estimate.
198
+ credits_awarded: t.credits_awarded == null ? 0 : Number(t.credits_awarded),
192
199
  created_on: t.created_on || null,
193
200
  idea_id: t.idea_id != null ? String(t.idea_id) : null,
194
201
  });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.170",
3
+ "version": "1.21.172",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.170",
9
+ "version": "1.21.172",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.170",
3
+ "version": "1.21.172",
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",
@@ -10094,5 +10094,17 @@
10094
10094
  "id": "1003806",
10095
10095
  "text": "When a new module version is being tested, how it went for the testing projects now counts toward its quality score — and a handful of friendly testers can no longer outscore hundreds of real ones."
10096
10096
  }
10097
+ ],
10098
+ "1.21.171": [
10099
+ {
10100
+ "id": "1004781",
10101
+ "text": "Credit figures in the hall now show money that was actually paid. A finished task shows its real payout instead of the old guess, so goal totals, member stats and the goal map stop adding up estimates as if they were earnings."
10102
+ }
10103
+ ],
10104
+ "1.21.172": [
10105
+ {
10106
+ "id": "1004670",
10107
+ "text": "The Studio now shows which pages have the most words to write: a short list of the five pages with the most text, each with how many lines it has and whether it has been tweaked, linked straight to the editor. The suggested ne"
10108
+ }
10097
10109
  ]
10098
10110
  }
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.21.170'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.172'; // 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');
@@ -60,14 +60,14 @@ function well(over = {}) {
60
60
  { id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
61
61
  ],
62
62
  tasks: [
63
- { id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
63
+ { id: '500', title: 'The task', status: 'shipped', credits_reward: 60, credits_awarded: 60, created_on: '2026-07-01', idea_id: '10' },
64
64
  { id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
65
65
  { id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
66
66
  ],
67
67
  ...over,
68
68
  };
69
69
  }
70
- const tasksOf = (prefix, n, status, created = '2026-05-01') => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, title: `task ${prefix}${i}`, status, credits_reward: 20, created_on: created, idea_id: null }));
70
+ const tasksOf = (prefix, n, status, created = '2026-05-01') => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, title: `task ${prefix}${i}`, status, credits_reward: 20, credits_awarded: status === 'shipped' ? 20 : 0, created_on: created, idea_id: null }));
71
71
  // A quiet well: nothing held, nothing landed this week.
72
72
  const quiet = (over = {}) => well({ goal_id: '9', title: 'A quiet well — nothing moves', task_total: 4, task_done: 4, criteria: [], tasks: tasksOf('9-', 4, 'shipped'), ...over });
73
73
  const settled = (over = {}) => well({ goal_id: '8', title: 'A settled system — it finished', status: 'achieved', task_total: 3, task_done: 3, tasks: tasksOf('8-', 3, 'shipped'), created_at: '2026-03-01T00:00:00.000Z', ...over });
@@ -142,7 +142,7 @@ await test('a goal is a well sized by its credits; settled and collapsed goals k
142
142
  assert.equal(o.mass, 60);
143
143
  assert.ok(Math.abs(a.r - o.r * 0.8) < 1e-9, 'a settled system is 0.8× the well it would have been');
144
144
  assert.ok(Math.abs(c.r - o.r * 0.5) < 1e-9, 'a collapsed goal is half');
145
- const big = await boot({ sky: sky([well(same({ goal_id: '1' })), well({ ...same({ goal_id: '4' }), tasks: tasksOf('4-', 3, 'shipped').map((t) => ({ ...t, credits_reward: 400 })) })]) });
145
+ const big = await boot({ sky: sky([well(same({ goal_id: '1' })), well({ ...same({ goal_id: '4' }), tasks: tasksOf('4-', 3, 'shipped').map((t) => ({ ...t, credits_reward: 400, credits_awarded: 400 })) })]) });
146
146
  assert.ok(goal(big, '4').r > goal(big, '1').r, 'more credits, a bigger well');
147
147
  });
148
148
 
@@ -227,7 +227,7 @@ await test('EXPLORE IS ON BY DEFAULT: the sky asks where you begin, and the ledg
227
227
  assert.equal(p.el('start').hidden, false, '"where do you begin" is up');
228
228
  const opts = p.el('startOpts').querySelectorAll('button');
229
229
  assert.equal(opts[0].textContent, 'where my thoughts are');
230
- assert.deepEqual(opts.slice(1, -1).map((b) => b.dataset.start), ['7', '9'], 'one door per open well, biggest first');
230
+ assert.deepEqual(opts.slice(1, -1).map((b) => b.dataset.start), ['9', '7'], 'one door per open well, biggest first (task 1004781: 7 weighs only the 60 it paid now)');
231
231
  assert.equal(opts.at(-1).textContent, 'show me everything');
232
232
  assert.equal(SKY.seen(goal(p, '7')), false, 'nothing is reached yet');
233
233
  p.key('l');
@@ -283,7 +283,7 @@ await test('THE LEDGER IS THE ACCESSIBLE PATH: every goal is a row, both directi
283
283
  assert.ok(rows.includes(SKY.esc(g.title)), `${g.title} is named whole`);
284
284
  }
285
285
  const rowOf = (id) => (new RegExp(`data-id="${id}"[\\s\\S]*?</button>`).exec(rows) || [''])[0];
286
- assert.match(rowOf('7'), /alive · 1\/3 tasks · 130 cr · 1\/2 rings · waits on 1/, 'the waiting goal (alive: a task is held) counts what it waits on');
286
+ assert.match(rowOf('7'), /alive · 1\/3 tasks · 60 cr · 1\/2 rings · waits on 1/, 'the waiting goal (alive: a task is held) counts what it waits on');
287
287
  assert.match(rowOf('9'), /waited on by 1/, 'the goal waited on counts it the other way');
288
288
  assert.match(rowOf('8'), /settled/); assert.match(rowOf('6'), /collapsed/);
289
289
  const filters = p.el('filters').querySelectorAll('button').map((b) => [b.dataset.f, b.textContent.replace(/\s*\d+$/, '').trim()]);
@@ -92,7 +92,7 @@ function well(over = {}) {
92
92
  { id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
93
93
  ],
94
94
  tasks: [
95
- { id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
95
+ { id: '500', title: 'The task', status: 'shipped', credits_reward: 60, credits_awarded: 6, created_on: '2026-07-01', idea_id: '10' },
96
96
  { id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
97
97
  { id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
98
98
  ],
@@ -211,7 +211,18 @@ await test('the rings are the criteria, closing as each is satisfied and thick w
211
211
  const g = p.S().goalById.get('7');
212
212
  assert.equal(g.crit.length, 2);
213
213
  same(g.crit.map((c) => c.satisfied), [true, false]);
214
- same(g.crit.map((c) => c.credits), [60, 70], 'a ring\'s credits are the credits of the tasks on it');
214
+ // task 1004781 — a ring weighs what its tasks were PAID: the shipped task paid 6 (its 60 was a guess), and
215
+ // the held and claimable tasks have paid nothing yet, so their estimates stay off the ring.
216
+ same(g.crit.map((c) => c.credits), [6, 0], 'a ring\'s credits are what the tasks on it were paid');
217
+ assert.equal(g.tasks.find((t) => t.id === '500').estimate, null, 'a settled task carries no estimate');
218
+ assert.equal(g.tasks.find((t) => t.id === '501').estimate, 40, 'live work keeps its estimate, apart from the total');
219
+ const SKY = p.SKY();
220
+ SKY.pick({ kind: 'task', ref: g.tasks.find((t) => t.id === '501') }, false);
221
+ assert.match(p.panel(), /40 <span class="lbl">Est\.<\/span>/, 'the panel labels a live task\'s figure as an estimate');
222
+ assert.doesNotMatch(p.panel(), /credits paid/);
223
+ SKY.pick({ kind: 'task', ref: g.tasks.find((t) => t.id === '500') }, false);
224
+ assert.match(p.panel(), /credits paid[\s\S]*?6/, 'and a settled task\'s as what it was paid');
225
+ assert.doesNotMatch(p.panel(), /Est\./);
215
226
  assert.equal(g.tasks.find((t) => t.id === '500').ci, 0, 'a task knows which ring it gates');
216
227
  assert.equal(g.tasks.find((t) => t.id === '500').sourceRef, '10', 'and which thought it was born of');
217
228
  });
@@ -230,8 +241,9 @@ await test('abandoned work is listed at zero and adds nothing to a well or its r
230
241
  const g = p.S().goalById.get('7');
231
242
  assert.equal(g.tasks.find((t) => t.id === '503').credits, 0, 'still listed, at zero');
232
243
  assert.equal(g.tasks.find((t) => t.id === '504').credits, 0, 'a merged-away task is listed at zero too');
233
- assert.equal(g.credits, 130, 'the well\'s mass is the live work only (60 + 40 + 30)');
234
- same(g.crit.map((c) => c.credits), [60, 70], 'and the ring it sat on is not thickened by it');
244
+ assert.equal(g.credits, 6, 'the well\'s mass is what was paid only');
245
+ same(g.crit.map((c) => c.credits), [6, 0], 'and the ring it sat on is not thickened by it');
246
+ assert.equal(g.tasks.find((t) => t.id === '503').estimate, null, 'abandoned work carries no estimate either');
235
247
  });
236
248
 
237
249
  await test('a private goal keeps its structure and none of its prose', async () => {
@@ -360,6 +360,30 @@ try {
360
360
  assert.ok(!s.html.classList.contains('st-open'));
361
361
  });
362
362
 
363
+ // task 1004670: the worst-pages report, through the real composer into the
364
+ // real Studio panel: the pages with the most text, most first.
365
+ await test('Most words to write: the worst-pages report, most text first, linked to the editor', async () => {
366
+ const T = (key, text) => ({ key, section: 's', text, placement: 'placed' });
367
+ const readings = { pages: INV.pages.map((p, i) => ({ id: p.id, reading_hash: `r${i}`, lines: Array.from({ length: i + 2 }, (_, k) => T(`L${k}`, `${p.id} line ${k}`)) })) };
368
+ const pagesWithReadings = ps.composePages({ inventory: INV, readings, tasks: TASKS });
369
+ assert.ok(pagesWithReadings.worst_pages.length >= 1);
370
+ const top = pagesWithReadings.worst_pages[0];
371
+ assert.equal(top.page_id, INV.pages[INV.pages.length - 1].id, 'the page with the most text leads');
372
+ const rows = lib.worstRows(pagesWithReadings);
373
+ assert.equal(rows[0].size, `${top.text_lines} lines of text`);
374
+ const s = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages': ok(pagesWithReadings) } });
375
+ await s.settle();
376
+ await s.click('st-status');
377
+ const body = s.el('st-panel-body').innerHTML;
378
+ assert.match(body, /Most words to write/);
379
+ assert.match(body, new RegExp(`${top.text_lines} lines of text`));
380
+ assert.match(body, new RegExp(`href="/builders/tweak-editor\\?page=${top.page_id}"`));
381
+ const plain = bootStudio({ answers: HOME });
382
+ await plain.settle();
383
+ await plain.click('st-status');
384
+ assert.doesNotMatch(plain.el('st-panel-body').innerHTML, /Most words to write/, 'no readings, no report');
385
+ });
386
+
363
387
  await test('the studio\'s words: a first click offers the studio page, and nothing is taken until the artist says so', async () => {
364
388
  const s = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages/builders:studio/draft': ok(studioRead({ held: false })) } });
365
389
  await s.settle();
@@ -136,14 +136,27 @@ function loadBrandApply(rewardBlock) {
136
136
  const SHIPPED_TASK = { status: 'shipped', credits_awarded: 0, credits_reward: 60 };
137
137
  const READY_TASK = { status: 'ready', credits_reward: 60 };
138
138
 
139
- test('hall: cost-plus-only keeps "Est." on a SHIPPED task (the 60 is not a payout)', () => {
139
+ // task 1004781 — a settled task shows what it was PAID, never its estimate:
140
+ // the 60 is not a payout, so a shipped task with nothing in the ledger is 0,
141
+ // and that 0 is an actual figure (plain label). Only live work is "Est.".
142
+ test('hall: cost-plus-only shows a SHIPPED task as what it paid (0), not the 60 estimate', () => {
140
143
  const B = loadBrandApply(CPO);
141
- assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits Est.',
142
- 'dropping "Est." presents the unpaid estimate as the real payout');
144
+ assert.equal(B.taskCreditValue(SHIPPED_TASK), 0, 'the estimate must never stand in for a payout');
145
+ assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits');
146
+ assert.equal(B.taskCreditValue({ ...SHIPPED_TASK, credits_awarded: 7 }), 7, 'a real payout shows as itself');
147
+ assert.equal(B.taskCreditValue(READY_TASK), 60, 'live work keeps its estimate');
143
148
  assert.equal(B.taskCreditLabel(READY_TASK), 'credits Est.');
144
149
  assert.equal(B.perTaskCredit(), false);
145
150
  });
146
151
 
152
+ test('hall: a settled task never falls back to its estimate, on any reward mode', () => {
153
+ for (const mode of [CPO, PAYS, undefined]) {
154
+ const B = loadBrandApply(mode);
155
+ assert.equal(B.taskCreditValue(SHIPPED_TASK), 0);
156
+ assert.equal(B.taskCreditValue({ status: 'confirmed', credits_reward: 30 }), 0, 'no ledger figure ⇒ 0');
157
+ }
158
+ });
159
+
147
160
  test('hall: a PAYING instance keeps the original settled/unsettled split', () => {
148
161
  const B = loadBrandApply(PAYS);
149
162
  assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits', 'settled ⇒ plain label, unchanged');
@@ -56,14 +56,14 @@ await test('the shape: wells carry their criteria and their compact tasks; edges
56
56
  assert.equal(w1.created_at, '2026-09-01T00:00:00.000Z');
57
57
  // task 1004402: each ring also carries its UAT state + backend-only flag (ADR 0351).
58
58
  assert.deepEqual(w1.criteria, [{ id: '10', criterion_id: 'slug-10', criterion_md: 'Criterion 10', satisfied: false, backend_only: false, task_ids: ['100', '101'], pending_review: false, uat_state: 'open' }]);
59
- assert.deepEqual(w1.tasks[0], { id: '100', title: 'Task 100', status: 'shipped', credits_reward: 30, created_on: '2026-09-02', idea_id: '555' });
59
+ assert.deepEqual(w1.tasks[0], { id: '100', title: 'Task 100', status: 'shipped', credits_reward: 30, credits_awarded: 0, created_on: '2026-09-02', idea_id: '555' });
60
60
  assert.equal(w1.tasks[1].idea_id, null, 'a task not born of an idea carries null, not an empty string');
61
61
  assert.deepEqual(out.goals[1].criteria[0].task_ids, [], 'a criterion with no linked task is an empty ring, not a missing one');
62
62
  });
63
63
 
64
64
  await test('nothing but the compact fields rides on a task — no description, no value summary, no notes', () => {
65
65
  const out = shapeSky({ goals: [g(1)], extras: [x(1)], tasks: [t(1, 1, { description: 'long prose', value_summary: 'more prose', notes: 'and more' })] });
66
- assert.deepEqual(Object.keys(out.goals[0].tasks[0]).sort(), ['created_on', 'credits_reward', 'id', 'idea_id', 'status', 'title']);
66
+ assert.deepEqual(Object.keys(out.goals[0].tasks[0]).sort(), ['created_on', 'credits_awarded', 'credits_reward', 'id', 'idea_id', 'status', 'title']);
67
67
  });
68
68
 
69
69
  await test('a ring or a task under a goal that is not a well is dropped, not drawn to nowhere', () => {
@@ -177,6 +177,17 @@ await test('the criterion→task join is read ONCE for every goal-bearing criter
177
177
  assert.match(crit[0].sql, /LEFT JOIN tasks t\s+ON t\.id = tc\.task_id/, 'a criterion with no task still arrives (the rollup\'s LEFT JOIN)');
178
178
  });
179
179
 
180
+ // task 1004781 — the sky sizes wells by what the ledger PAID, so the read
181
+ // carries the payout (the shared subquery, settled-only) beside the estimate.
182
+ await test('a task carries what the ledger paid for it, from the shared payout subquery', async () => {
183
+ const out = shapeSky({ goals: [g(1)], extras: [x(1)], tasks: [t(100, 1, { status: 'shipped', credits_awarded: '7' })] });
184
+ assert.equal(out.goals[0].tasks[0].credits_awarded, 7);
185
+ const pool = fakePool();
186
+ await skyRows({}, { pool });
187
+ const tq = pool.calls.find((c) => /FROM tasks t\s+JOIN goals g/.test(c.sql));
188
+ assert.match(tq.sql, /FROM credit_log cl[\s\S]*AS credits_awarded/);
189
+ });
190
+
180
191
  await test('the task query is compact by construction: no description, no value_summary, and the date is truncated in SQL', async () => {
181
192
  const pool = fakePool();
182
193
  await skyRows({}, { pool });