@bongos/core 1.21.153 → 1.21.154

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.153",
6
- "core_contract": "1.21.153",
7
- "source_commit": "fc0ed4122e62acbeb463e589bd686bc56e242daf",
5
+ "core_version": "1.21.154",
6
+ "core_contract": "1.21.154",
7
+ "source_commit": "046cb72ed357309e34e58fb712b7efa7b56a4888",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-21T21:04:39.395Z",
9
+ "built_at": "2026-10-22T14:58:30.668Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 608,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3808,
20
- "tree_sha256": "8bafc2ef799ecc86b401ba0af5adbd70549de74a2c687d44b722f607b58fc268",
20
+ "tree_sha256": "9b227a8bf5b2795c452c985814ac1f22809900835d5148efee15c24a2203b4af",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2387,7 +2387,7 @@
2387
2387
  {
2388
2388
  "path": "docs/api/openapi.json",
2389
2389
  "mode": "0000644",
2390
- "sha256": "36e20b38d432f4f698b2138ebc76e97aec0307fed427337825ee4c7080ea8332"
2390
+ "sha256": "9402cb974357bea192c8fd919a1f49cc6e8f514b9948705f37ef5dafebca1278"
2391
2391
  },
2392
2392
  {
2393
2393
  "path": "docs/architecture.md",
@@ -2412,12 +2412,12 @@
2412
2412
  {
2413
2413
  "path": "docs/copy-inventory.md",
2414
2414
  "mode": "0000644",
2415
- "sha256": "b38c249c28740733f21a3629881b56e0a12b67418094c442da3ed5573370f09d"
2415
+ "sha256": "36192920b84dfe1a36853f586c7de9259b6ce62adba750ef73537a050604b6d7"
2416
2416
  },
2417
2417
  {
2418
2418
  "path": "docs/copy-registry.json",
2419
2419
  "mode": "0000644",
2420
- "sha256": "309b4557fa126b25d30658607eb37fc950bd36d4e8a5737e40ba1678d7735b0d"
2420
+ "sha256": "0ce492be99e91cdf249acc1f49c2acfde87e652b5a73a95c1d12ff41865273ff"
2421
2421
  },
2422
2422
  {
2423
2423
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2927,12 +2927,12 @@
2927
2927
  {
2928
2928
  "path": "docs/module-api-changelog.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "6255a313246c3289458f9d8b9e002216e7226ba57318b7814a9d621fc9dc6d7f"
2930
+ "sha256": "933a5d803257d84f6df3fa2fd66229f74e0463ba41f23256ab7db81c7a689ca8"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/modules-contract.md",
2934
2934
  "mode": "0000644",
2935
- "sha256": "bf2879ae52353790c2f8b9ef22f06ae3358a1357513d2a967f5ce3768eafb221"
2935
+ "sha256": "4d0036450b55adc4539a9838f8478848e1cce16fa237b9207c10d08980a3d906"
2936
2936
  },
2937
2937
  {
2938
2938
  "path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
@@ -3017,7 +3017,7 @@
3017
3017
  {
3018
3018
  "path": "docs/page-readings.json",
3019
3019
  "mode": "0000644",
3020
- "sha256": "a6a2bdce1ad9bdb2463892c9e4618090c4bf6c1dbabe7ab026507268810d940a"
3020
+ "sha256": "5bfc13802503364ffb6812d85c052ec273404d5994affc1be4949dd819161b1f"
3021
3021
  },
3022
3022
  {
3023
3023
  "path": "docs/project-context.template.md",
@@ -6162,12 +6162,12 @@
6162
6162
  {
6163
6163
  "path": "modules/hall-ui/public/modules.html",
6164
6164
  "mode": "0000644",
6165
- "sha256": "ac61c093dd938f658898762259a8f9431b4f625797943a42d6048602317576bd"
6165
+ "sha256": "75a38104b51ea30550a616c8e709a3ec278bf153dedbd487acf89d247a61b19f"
6166
6166
  },
6167
6167
  {
6168
6168
  "path": "modules/hall-ui/public/modules.js",
6169
6169
  "mode": "0000644",
6170
- "sha256": "6174a55c79244cf80508024903f89803c908f731b09dc74190f447af832204b6"
6170
+ "sha256": "6b5a48f2bb96e0478a7150265ded185e04d707b51f5ad7d3b813446115eb6142"
6171
6171
  },
6172
6172
  {
6173
6173
  "path": "modules/hall-ui/public/modules.states.json",
@@ -6762,7 +6762,7 @@
6762
6762
  {
6763
6763
  "path": "modules/hall-ui/records/catalog.md",
6764
6764
  "mode": "0000644",
6765
- "sha256": "f2ef04fd2c76d94bcdfd70354a243a77370502849b50365352ae633c9566b258"
6765
+ "sha256": "e760daf386c9e723843c573afcce713a1b18514684c57f58ad156f3a79d9b557"
6766
6766
  },
6767
6767
  {
6768
6768
  "path": "modules/hall-ui/records/collaboration.md",
@@ -10557,12 +10557,12 @@
10557
10557
  {
10558
10558
  "path": "package-lock.json",
10559
10559
  "mode": "0000644",
10560
- "sha256": "7107af68ad1c2b1d87998b3e40cc5f51ca1607c681ab363b95196e7a93a5c9c5"
10560
+ "sha256": "8896ac22750733e9413d6ac302b78e85999a7878eb8ca8c1ec1af5e493abea42"
10561
10561
  },
10562
10562
  {
10563
10563
  "path": "package.json",
10564
10564
  "mode": "0000644",
10565
- "sha256": "67ef9a37cc7fd004e95cec39e419b6a132e331867d6dabd29cfa4fdc2d8c23d1"
10565
+ "sha256": "1a220fca30df53b3e1cc4ae053b91ec424e142b0e2aaf38bf53bf22cf444b5ba"
10566
10566
  },
10567
10567
  {
10568
10568
  "path": "public-docs/index.html",
@@ -10582,7 +10582,7 @@
10582
10582
  {
10583
10583
  "path": "release-notes.json",
10584
10584
  "mode": "0000644",
10585
- "sha256": "d7f56b04bf200720e04752f98b564cc8f245344bbf93bd1e64fa81a7a41a5a46"
10585
+ "sha256": "3dbca36d0a1743a39859a3d3d26cf209db264cd6bd6c8588e7ebf1177be55b75"
10586
10586
  },
10587
10587
  {
10588
10588
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12692,7 +12692,7 @@
12692
12692
  {
12693
12693
  "path": "src/bongos/module-listing.js",
12694
12694
  "mode": "0000644",
12695
- "sha256": "3f35454a231ef86e08e2c49f641ae912977bf1c351ed6838c348a0880d2d2850"
12695
+ "sha256": "2f2ac4e04af85579c9c2bfd6e6b8e880cff46cadf49ad3791948c9bca03c8d72"
12696
12696
  },
12697
12697
  {
12698
12698
  "path": "src/bongos/module-overrides.js",
@@ -12872,7 +12872,7 @@
12872
12872
  {
12873
12873
  "path": "src/bongos/routes/modules.js",
12874
12874
  "mode": "0000644",
12875
- "sha256": "e6b1b25371695b4aa959f5fdcece31869e827221f0205d1797c8332628d7b6a2"
12875
+ "sha256": "051144a0c2235022e7e423b6a48c2203dadb8142dedfb57edbbf6c5ba69206bb"
12876
12876
  },
12877
12877
  {
12878
12878
  "path": "src/bongos/routes/my-passkeys.js",
@@ -12962,7 +12962,7 @@
12962
12962
  {
12963
12963
  "path": "src/module-api.js",
12964
12964
  "mode": "0000644",
12965
- "sha256": "43818864e42838c6ba32937c9fee880154e5164fe64363ba230bb576a8ff02d7"
12965
+ "sha256": "39421d70df1f7628486cf17294e001fc06d49fa502847ca7786fff43fb2738c4"
12966
12966
  },
12967
12967
  {
12968
12968
  "path": "src/module-loader/catalog.js",
@@ -15467,7 +15467,7 @@
15467
15467
  {
15468
15468
  "path": "tests/hall_catalog_world.mjs",
15469
15469
  "mode": "0000644",
15470
- "sha256": "8cbc6512e9ab4dace612378482aab215d39de9727622daef998ebe84ab68f9f0"
15470
+ "sha256": "d209d00843af7bab570d02be627761913e03129c80f29f0c036235633b8085a9"
15471
15471
  },
15472
15472
  {
15473
15473
  "path": "tests/hall_city.mjs",
@@ -16642,7 +16642,7 @@
16642
16642
  {
16643
16643
  "path": "tests/module_store_listing.mjs",
16644
16644
  "mode": "0000644",
16645
- "sha256": "3402dfb7575e482b409712381ee5cfe6b39e015ae3c1186edcba0b049690d532"
16645
+ "sha256": "be639096f82a9750e25b57c7612e94754b6109ef13507144be9ceb384628eb91"
16646
16646
  },
16647
16647
  {
16648
16648
  "path": "tests/module_store_publish.mjs",
@@ -23004,7 +23004,7 @@
23004
23004
  "store"
23005
23005
  ],
23006
23006
  "summary": "GET /store/modules",
23007
- "description": "The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each listed module's newest version, if it passed the Security gate and scores at or above the floor, or under \"new\" if it has no real-world data yet. Below the bar is not listed at all. The floor is fixed in module-listing.js and returned here. rank: any-builder — a read of public store metadata, like /store/modules/latest: no entitlement taken, nothing changed.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
23007
+ "description": "The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each listed module's newest version, if it passed the Security gate and scores at or above the floor, or under \"new\" if it has no real-world data yet. Below the bar is not listed at all. The floor is fixed in module-listing.js and returned here. `?sort=<part>` orders the listed modules by the part a buyer cares about (task 1003800, sortByPart); the parts it accepts come back as sort_parts. rank: any-builder — a read of public store metadata, like /store/modules/latest: no entitlement taken, nothing changed.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
23008
23008
  "x-rank": "any-builder",
23009
23009
  "x-source": "src/bongos/routes/modules.js",
23010
23010
  "responses": {
@@ -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
- **2769 strings** across 3 surfaces · 2348 distinct pieces of copy · 2769 certain, 0 heuristic · 241 shown in more than one place.
7
+ **2771 strings** across 3 surfaces · 2350 distinct pieces of copy · 2771 certain, 0 heuristic · 241 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` | 148 | 1961 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 148 | 1963 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 9 | 708 |
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
  | … | | _201 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1961 strings
81
+ ## `builders-hall` — 1963 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -129,7 +129,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
129
129
  | `0150152ea9ec` | certain | A known snag stopped the move to core {…}: | `modules/hall-ui/public/deploy.js:214` |
130
130
  | `2e17d7e236d9` | certain | A module built here can be submitted upstream for review, so it can join the shared catalog with your name and | `modules/hall-ui/public/modules.html:74` |
131
131
  | `0c611f5a76af` | certain | A move to core {…} failed{…} — this project has moved on since, and is serving {…} now. Whatever stopped that | `modules/hall-ui/public/deploy.js:374` |
132
- | `d316ba4f7e9a` | certain | A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not co | `modules/hall-ui/public/modules.html:87` |
132
+ | `e395cd11edab` | certain | A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not co | `modules/hall-ui/public/modules.html:90` |
133
133
  | `a07c7b335a66` | certain | A protected-scope goal requires a Metic+ member. | `modules/hall-ui/public/goals-render.js:1306` |
134
134
  | `a07c7b335a66` | certain | A protected-scope goal requires a Metic+ member. | `modules/hall-ui/public/goals-render.js:1328` |
135
135
  | `5723baf26b2d` | certain | A quick idea is a thought filed fast. It is not an unfinished Full Idea — only its author can offer it up for | `modules/hall-ui/public/idea.js:475` |
@@ -176,7 +176,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
176
176
  | `375b0e67bc30` | certain | Allow new releases and move to {…} | `modules/hall-ui/public/deploy.js:257` |
177
177
  | `375b0e67bc30` | certain | Allow new releases and move to {…} | `modules/hall-ui/public/deploy.js:685` |
178
178
  | `e99cc5d37404` | certain | Already a member? | `modules/hall-ui/public/hall-render.js:373` |
179
- | `70ffbebe42fd` | certain | Also available as a Claude page | `modules/hall-ui/public/modules.html:100` |
179
+ | `70ffbebe42fd` | certain | Also available as a Claude page | `modules/hall-ui/public/modules.html:103` |
180
180
  | `da5368d10f85` | certain | Amendment {…} passed{…}, and the constitution below is not the one it decided: | `modules/hall-ui/public/government.js:671` |
181
181
  | `2ca420e19dbb` | certain | An earlier attempt did not land. | `modules/hall-ui/public/deploy.js:374` |
182
182
  | `69f0ca4a4d84` | certain | Analytics scope | `modules/hall-ui/public/hall-render.js:1184` |
@@ -368,7 +368,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
368
368
  | `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/project-settings.js:284` |
369
369
  | `90691bceaf68` | certain | Could not load | `modules/hall-ui/public/task.js:1022` |
370
370
  | `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:1018` |
371
- | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:458` |
371
+ | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:489` |
372
372
  | `9912c3835136` | certain | Could not load requests — reload to try again. | `modules/hall-ui/public/collab.js:845` |
373
373
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:504` |
374
374
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:542` |
@@ -389,7 +389,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
389
389
  | `64f9d51f0fa2` | certain | Could not load your settings. | `modules/hall-ui/public/settings.js:1204` |
390
390
  | `de244c45d916` | certain | Could not load {…} | `modules/hall-ui/public/task.js:1015` |
391
391
  | `2366356926b4` | certain | Could not mark the primer just now. Try again. | `modules/hall-ui/public/primer.js:259` |
392
- | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:436` |
392
+ | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:467` |
393
393
  | `6f500726f0ac` | certain | Could not read the report: {…} | `modules/hall-ui/public/watch.js:768` |
394
394
  | `21c2f33168a3` | certain | Could not read this project's deploy state: {…} | `modules/hall-ui/public/deploy.js:624` |
395
395
  | `21a782f1af26` | certain | Could not read what this would bring in: {…}. The check below still applies. | `modules/hall-ui/public/deploy.js:542` |
@@ -831,7 +831,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
831
831
  | `90623fec4cfa` | certain | No page is open. | `modules/hall-ui/public/tweak-editor.js:563` |
832
832
  | `1a46c31343cb` | certain | No projects are hosted here yet. | `modules/hall-ui/public/fleet-costs.js:68` |
833
833
  | `9f73b3f3dad0` | certain | No rank floor — any builder can claim this task | `modules/hall-ui/public/task-lib.js:82` |
834
- | `4ffd939755e0` | certain | No real-world data yet, so it is listed but not ranked. | `modules/hall-ui/public/modules.js:221` |
834
+ | `4ffd939755e0` | certain | No real-world data yet, so it is listed but not ranked. | `modules/hall-ui/public/modules.js:235` |
835
835
  | `722d107d7a90` | certain | No render is attached to this page yet. /tweak attaches them when it applies a page; until then there is nothi | `modules/hall-ui/public/approval-queue.js:135` |
836
836
  | `3fd0c6fcb92e` | certain | No request and no invite needed. Sign in with GitHub, and a step-by-step guide takes you the rest of the way. | `modules/hall-ui/public/hall-render.js:283` |
837
837
  | `2947c1ddea9a` | certain | No running box to deploy. | `modules/hall-ui/public/deploy.js:708` |
@@ -902,7 +902,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
902
902
  | `43626cff7071` | certain | Nothing of that kind in your sky. | `modules/hall-ui/public/sky-panel.js:123` |
903
903
  | `b05a5f8cb074` | certain | Nothing personal is sent. | `modules/hall-ui/public/my-agents.js:450` |
904
904
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/profile.js:297` |
905
- | `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:397` |
905
+ | `f326632f2013` | certain | Nothing to submit — every module here ships with the core. | `modules/hall-ui/public/modules.js:428` |
906
906
  | `27c0d872db2d` | certain | Nothing you have typed above is lost by opening this. | `modules/hall-ui/public/ideas.html:155` |
907
907
  | `26a07b64f49d` | certain | Now running core {…} | `modules/hall-ui/public/deploy.js:400` |
908
908
  | `21cb60a8a96a` | certain | Now running core {…} — but GitHub was not updated. | `modules/hall-ui/public/deploy.js:390` |
@@ -1111,7 +1111,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1111
1111
  | `48e5f1d19e90` | certain | Runner allowed to work | `modules/hall-ui/public/gate.js:425` |
1112
1112
  | `41730c113dd5` | certain | Runners work only the goals above, and only while the switch is on. Each builder picks which of them their own | `modules/hall-ui/public/gate.js:425` |
1113
1113
  | `b6596905f845` | certain | Said more than once | `modules/hall-ui/public/copy-desk.html:56` |
1114
- | `ce046dcbd8e1` | certain | Save as PDF | `modules/hall-ui/public/modules.html:101` |
1114
+ | `ce046dcbd8e1` | certain | Save as PDF | `modules/hall-ui/public/modules.html:104` |
1115
1115
  | `819cdcaac499` | certain | Save current filters as | `modules/hall-ui/public/hall-kit.js:411` |
1116
1116
  | `0c79b504aebe` | certain | Save key | `modules/hall-ui/public/settings.js:876` |
1117
1117
  | `b3d55c8c765a` | certain | Save my choices | `modules/hall-ui/public/settings-specialities.js:163` |
@@ -1216,6 +1216,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1216
1216
  | `867df58c9aaa` | certain | Something else | `modules/hall-ui/public/copy-desk.html:95` |
1217
1217
  | `e11c18877155` | certain | Something is here. | `modules/hall-ui/public/sky-interaction.js:35` |
1218
1218
  | `c377d8ed744b` | certain | Something went wrong. Try again. | `modules/hall-ui/public/profile-connections.js:331` |
1219
+ | `70383b194ec0` | certain | Sort by | `modules/hall-ui/public/modules.html:87` |
1219
1220
  | `e0767e80206c` | certain | Sound for {…} | `modules/hall-ui/public/settings.js:309` |
1220
1221
  | `7f4f7b25c61f` | certain | Sound preference saved. | `modules/hall-ui/public/settings.js:326` |
1221
1222
  | `490f4bbbc75b` | certain | Sounds are switched off for now. Nothing plays when a task ships, when {{currency}} arrive, or when Claude fin | `modules/hall-ui/public/settings.html:396` |
@@ -1757,7 +1758,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1757
1758
  | `7b5ad068dc26` | certain | not graded | `modules/hall-ui/public/hall-render.js:349` |
1758
1759
  | `01a3d68d80c1` | certain | not graded (grader outage) | `modules/hall-ui/public/builders.js:557` |
1759
1760
  | `d43b9bbb8d4d` | certain | not graded (quality lens unavailable) | `modules/hall-ui/public/builders.js:559` |
1760
- | `380177c47d89` | certain | not measured yet | `modules/hall-ui/public/modules.js:236` |
1761
+ | `380177c47d89` | certain | not measured yet | `modules/hall-ui/public/modules.js:252` |
1761
1762
  | `c04e4491af67` | certain | not now | `modules/hall-ui/public/studio.html:110` |
1762
1763
  | `3916a3dc8e98` | certain | not started | `modules/hall-ui/public/roadmap.js:120` |
1763
1764
  | `3916a3dc8e98` | certain | not started | `modules/hall-ui/public/roadmap.js:341` |
@@ -1824,7 +1825,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1824
1825
  | `c8c2551006dc` | certain | skip the tour | `modules/hall-ui/public/goal-map.html:121` |
1825
1826
  | `c8c2551006dc` | certain | skip the tour | `modules/hall-ui/public/thinking.html:122` |
1826
1827
  | `1e737a57eb20` | certain | start governing | `modules/hall-ui/public/city.html:99` |
1827
- | `e294c3429c2e` | certain | strong at {…} | `modules/hall-ui/public/modules.js:225` |
1828
+ | `e294c3429c2e` | certain | strong at {…} | `modules/hall-ui/public/modules.js:241` |
1828
1829
  | `0215b2cdca8c` | certain | submit for review | `modules/hall-ui/public/modules.js:187` |
1829
1830
  | `43e6f1592ae3` | certain | submit it in the editor | `modules/hall-ui/public/studio.js:836` |
1830
1831
  | `9ec19e32f86f` | certain | submit page | `modules/hall-ui/public/tweak-editor.html:68` |
@@ -1895,7 +1896,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1895
1896
  | `18cf722bdd7e` | certain | waiting · | `modules/hall-ui/public/sky-interaction.js:128` |
1896
1897
  | `354861d3c2ce` | certain | waits for a restart | `modules/hall-ui/public/modules.js:139` |
1897
1898
  | `35be9e0923da` | certain | walks the whole path in plain language. | `modules/hall-ui/public/hall-render.js:837` |
1898
- | `20318b6da94e` | certain | weak at {…} | `modules/hall-ui/public/modules.js:227` |
1899
+ | `20318b6da94e` | certain | weak at {…} | `modules/hall-ui/public/modules.js:243` |
1899
1900
  | `db42054582ed` | certain | wells · | `modules/hall-ui/public/sky-interaction.js:129` |
1900
1901
  | `17705daf7b85` | certain | what counts as passing | `modules/hall-ui/public/government.html:55` |
1901
1902
  | `96a8971a5c44` | certain | what got un-stuck | `modules/hall-ui/public/blockers.js:132` |
@@ -1945,6 +1946,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1945
1946
  | `80b4aa799a4b` | certain | {…} is writing this page now. You can read it, download it, or take another page from the studio. | `modules/hall-ui/public/tweak-editor.js:319` |
1946
1947
  | `830d2be6f859` | certain | {…} line | `modules/hall-ui/public/tweak-editor.js:235` |
1947
1948
  | `511a4b6073cf` | certain | {…} more PR{…} still being checked. Refresh shortly. | `modules/hall-ui/public/gate.js:225` |
1949
+ | `92469c4757f1` | certain | {…} not measured yet | `modules/hall-ui/public/modules.js:226` |
1948
1950
  | `73c1372b43d9` | certain | {…} of {…} | `modules/hall-ui/public/sky-panel.js:120` |
1949
1951
  | `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-lib.js:32` |
1950
1952
  | `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-render.js:701` |
@@ -1968,7 +1970,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1968
1970
  | `1a8a6267e490` | certain | {…} unfinished task{…} — say what happens to each | `modules/hall-ui/public/goals-render.js:1032` |
1969
1971
  | `2f75f69cdb2f` | certain | {…} {…} changed | `modules/hall-ui/public/profile-crafts.js:218` |
1970
1972
  | `8186b562553a` | certain | {…} {…} waiting | `modules/hall-ui/public/work.js:420` |
1971
- | `a9216af9b05e` | certain | {…} {…} — how-to | `modules/hall-ui/public/modules.js:513` |
1973
+ | `a9216af9b05e` | certain | {…} {…} — how-to | `modules/hall-ui/public/modules.js:544` |
1972
1974
  | `0d4016a5531c` | certain | {…} {…}{…} | `modules/hall-ui/public/watch.js:514` |
1973
1975
  | `0d4016a5531c` | certain | {…} {…}{…} | `modules/hall-ui/public/work.js:265` |
1974
1976
  | `b3757ba3bb46` | certain | {…} · Stored encrypted; never shown again after saving. | `modules/hall-ui/public/settings.js:878` |
@@ -1976,7 +1978,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1976
1978
  | `d73256fbc395` | certain | {…} · who votes, and how it {…} | `modules/hall-ui/public/board-room.js:460` |
1977
1979
  | `cab2a4296aec` | certain | {…} · {…} · {…}{…} | `modules/hall-ui/public/goal-inbox.js:458` |
1978
1980
  | `1ac723849449` | certain | {…} · {…}{…} | `modules/hall-ui/public/sky-interaction.js:33` |
1979
- | `ddd2bfc1fb60` | certain | {…} — how to use it | `modules/hall-ui/public/modules.js:515` |
1981
+ | `ddd2bfc1fb60` | certain | {…} — how to use it | `modules/hall-ui/public/modules.js:546` |
1980
1982
  | `085f93f3f775` | certain | {…} — see all standings | `modules/hall-ui/public/hall-render.js:80` |
1981
1983
  | `2a2728a4ec4f` | certain | {…}% of tasks complete | `modules/hall-ui/public/roadmap.js:63` |
1982
1984
  | `15e545eb3ce2` | certain | {…}, no such task | `modules/hall-ui/public/task.js:1002` |
@@ -1991,7 +1993,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1991
1993
  | `97e27ab169ae` | certain | {…}{…} session{…} across {…} builder{…}. | `modules/hall-ui/public/sessions.js:443` |
1992
1994
  | `3d74668f69e5` | certain | {…}{…} · {…} | `modules/hall-ui/public/task.js:639` |
1993
1995
  | `7f79bedecf60` | certain | {…}{…} — open it to see what went | `modules/hall-ui/public/goal-inbox.js:374` |
1994
- | `1026fd5f68b7` | certain | · version | `modules/hall-ui/public/modules.js:518` |
1996
+ | `1026fd5f68b7` | certain | · version | `modules/hall-ui/public/modules.js:549` |
1995
1997
  | `4a5ff4f662df` | certain | — ${…} of ${…} this month ({…}%). {…} | `modules/hall-ui/public/hall-render.js:523` |
1996
1998
  | `a6ae9cbdee30` | certain | — all builders — | `modules/hall-ui/public/builders.js:711` |
1997
1999
  | `729d4ce22b99` | certain | — any — | `modules/hall-ui/public/hall-widgets.js:163` |
@@ -2026,7 +2028,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2026
2028
  | `5f1d914ad45e` | certain | …{…} more — | `modules/hall-ui/public/goal-inbox.js:561` |
2027
2029
  | `de8fa5223aee` | certain | ‹ Prev | `modules/hall-ui/public/hall-kit.js:74` |
2028
2030
  | `d4b84b7fbdeb` | certain | ← All goals | `modules/hall-ui/public/goals.html:77` |
2029
- | `9159687c73ff` | certain | ← All modules | `modules/hall-ui/public/modules.html:98` |
2031
+ | `9159687c73ff` | certain | ← All modules | `modules/hall-ui/public/modules.html:101` |
2030
2032
  | `d063a7b9c83e` | certain | ← Back | `modules/hall-ui/public/page-notes.js:283` |
2031
2033
  | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:189` |
2032
2034
  | `7926e30955d2` | certain | ← Back to goal | `modules/hall-ui/public/goals.html:205` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2769,
7
- "unique_ids": 2348,
8
- "certain": 2769,
6
+ "strings": 2771,
7
+ "unique_ids": 2350,
8
+ "certain": 2771,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 241
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 148,
16
- "strings": 1961
16
+ "strings": 1963
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -3054,11 +3054,11 @@
3054
3054
  "confidence": "certain"
3055
3055
  },
3056
3056
  {
3057
- "id": "d316ba4f7e9a",
3057
+ "id": "e395cd11edab",
3058
3058
  "surface": "builders-hall",
3059
- "text": "A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module.",
3059
+ "text": "A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module. Sort by a part to see the modules strongest at it first; a module with no score for that part goes last, and a new module is never ranked.",
3060
3060
  "file": "modules/hall-ui/public/modules.html",
3061
- "line": 87,
3061
+ "line": 90,
3062
3062
  "origin": "html-text",
3063
3063
  "confidence": "certain"
3064
3064
  },
@@ -3481,7 +3481,7 @@
3481
3481
  "surface": "builders-hall",
3482
3482
  "text": "Also available as a Claude page",
3483
3483
  "file": "modules/hall-ui/public/modules.html",
3484
- "line": 100,
3484
+ "line": 103,
3485
3485
  "origin": "html-text",
3486
3486
  "confidence": "certain"
3487
3487
  },
@@ -5209,7 +5209,7 @@
5209
5209
  "surface": "builders-hall",
5210
5210
  "text": "Could not load modules.",
5211
5211
  "file": "modules/hall-ui/public/modules.js",
5212
- "line": 458,
5212
+ "line": 489,
5213
5213
  "origin": "js-notice",
5214
5214
  "confidence": "certain"
5215
5215
  },
@@ -5398,7 +5398,7 @@
5398
5398
  "surface": "builders-hall",
5399
5399
  "text": "Could not reach the submit endpoint.",
5400
5400
  "file": "modules/hall-ui/public/modules.js",
5401
- "line": 436,
5401
+ "line": 467,
5402
5402
  "origin": "js-notice",
5403
5403
  "confidence": "certain"
5404
5404
  },
@@ -9376,7 +9376,7 @@
9376
9376
  "surface": "builders-hall",
9377
9377
  "text": "No real-world data yet, so it is listed but not ranked.",
9378
9378
  "file": "modules/hall-ui/public/modules.js",
9379
- "line": 221,
9379
+ "line": 235,
9380
9380
  "origin": "js-markup",
9381
9381
  "confidence": "certain"
9382
9382
  },
@@ -10015,7 +10015,7 @@
10015
10015
  "surface": "builders-hall",
10016
10016
  "text": "Nothing to submit — every module here ships with the core.",
10017
10017
  "file": "modules/hall-ui/public/modules.js",
10018
- "line": 397,
10018
+ "line": 428,
10019
10019
  "origin": "js-text-assign",
10020
10020
  "confidence": "certain"
10021
10021
  },
@@ -11896,7 +11896,7 @@
11896
11896
  "surface": "builders-hall",
11897
11897
  "text": "Save as PDF",
11898
11898
  "file": "modules/hall-ui/public/modules.html",
11899
- "line": 101,
11899
+ "line": 104,
11900
11900
  "origin": "html-text",
11901
11901
  "confidence": "certain"
11902
11902
  },
@@ -12836,6 +12836,15 @@
12836
12836
  "origin": "js-notice",
12837
12837
  "confidence": "certain"
12838
12838
  },
12839
+ {
12840
+ "id": "70383b194ec0",
12841
+ "surface": "builders-hall",
12842
+ "text": "Sort by",
12843
+ "file": "modules/hall-ui/public/modules.html",
12844
+ "line": 87,
12845
+ "origin": "html-text",
12846
+ "confidence": "certain"
12847
+ },
12839
12848
  {
12840
12849
  "id": "e0767e80206c",
12841
12850
  "surface": "builders-hall",
@@ -17710,7 +17719,7 @@
17710
17719
  "surface": "builders-hall",
17711
17720
  "text": "not measured yet",
17712
17721
  "file": "modules/hall-ui/public/modules.js",
17713
- "line": 236,
17722
+ "line": 252,
17714
17723
  "origin": "js-markup",
17715
17724
  "confidence": "certain"
17716
17725
  },
@@ -18313,7 +18322,7 @@
18313
18322
  "surface": "builders-hall",
18314
18323
  "text": "strong at {…}",
18315
18324
  "file": "modules/hall-ui/public/modules.js",
18316
- "line": 225,
18325
+ "line": 241,
18317
18326
  "origin": "js-markup",
18318
18327
  "confidence": "certain"
18319
18328
  },
@@ -18952,7 +18961,7 @@
18952
18961
  "surface": "builders-hall",
18953
18962
  "text": "weak at {…}",
18954
18963
  "file": "modules/hall-ui/public/modules.js",
18955
- "line": 227,
18964
+ "line": 243,
18956
18965
  "origin": "js-markup",
18957
18966
  "confidence": "certain"
18958
18967
  },
@@ -19397,6 +19406,15 @@
19397
19406
  "origin": "js-text-assign",
19398
19407
  "confidence": "certain"
19399
19408
  },
19409
+ {
19410
+ "id": "92469c4757f1",
19411
+ "surface": "builders-hall",
19412
+ "text": "{…} not measured yet",
19413
+ "file": "modules/hall-ui/public/modules.js",
19414
+ "line": 226,
19415
+ "origin": "js-markup",
19416
+ "confidence": "certain"
19417
+ },
19400
19418
  {
19401
19419
  "id": "73c1372b43d9",
19402
19420
  "surface": "builders-hall",
@@ -19609,7 +19627,7 @@
19609
19627
  "surface": "builders-hall",
19610
19628
  "text": "{…} {…} — how-to",
19611
19629
  "file": "modules/hall-ui/public/modules.js",
19612
- "line": 513,
19630
+ "line": 544,
19613
19631
  "origin": "js-text-assign",
19614
19632
  "confidence": "certain"
19615
19633
  },
@@ -19681,7 +19699,7 @@
19681
19699
  "surface": "builders-hall",
19682
19700
  "text": "{…} — how to use it",
19683
19701
  "file": "modules/hall-ui/public/modules.js",
19684
- "line": 515,
19702
+ "line": 546,
19685
19703
  "origin": "js-text-assign",
19686
19704
  "confidence": "certain"
19687
19705
  },
@@ -19816,7 +19834,7 @@
19816
19834
  "surface": "builders-hall",
19817
19835
  "text": "· version",
19818
19836
  "file": "modules/hall-ui/public/modules.js",
19819
- "line": 518,
19837
+ "line": 549,
19820
19838
  "origin": "js-markup",
19821
19839
  "confidence": "certain"
19822
19840
  },
@@ -20131,7 +20149,7 @@
20131
20149
  "surface": "builders-hall",
20132
20150
  "text": "← All modules",
20133
20151
  "file": "modules/hall-ui/public/modules.html",
20134
- "line": 98,
20152
+ "line": 101,
20135
20153
  "origin": "html-text",
20136
20154
  "confidence": "certain"
20137
20155
  },
@@ -3143,5 +3143,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3143
3143
  landed since 1.21.151 with no explicit bump. run 39671625798. (task 1002620)
3144
3144
  1.21.153 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3145
3145
  landed since 1.21.152 with no explicit bump. run 39675693737. (task 1002620)
3146
+ 1.21.154 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3147
+ landed since 1.21.153 with no explicit bump. run 39800316919. (task 1002620)
3146
3148
  ---------------------------------------------------------------------------
3147
3149
  ```
@@ -393,6 +393,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
393
393
  - **A how-to is required** ([ADR 0347](adr/0347-every-store-module-ships-a-how-to.md)). `modules/<key>/HOWTO.md` is what a buyer reads — the store shows it as the module's page — so it is written for the person who installs the module, not for an AI working inside it (that is `CLAUDE.md`). It is plain Markdown: any AI or person can write it. Publish refuses the version unless it has these five level-2 headings, in any order and any case, each with some text under it: `## What it does`, `## Install and enable`, `## How to use it` (with one worked example), `## Configuration` ("None." is fine) and `## Limits and known issues` ("None known." is fine). HTML comments don't count as text, so the scaffold's prompts must be replaced. The refusal names each failing section. The check is one function, `checkHowto` in `scripts/gds/module-artifact.js`; the CLI runs it before uploading and the store runs it again on upload, so a hand-built upload can't skip it. `bongos module check` reports it as advice only — an upstream submit is not gated by it. Install and update don't re-check it, so a version published before the gate still installs. The file travels in the tarball, so its hash pins it to the version. An optional `howto.artifactUrl` in `module.json` (an `https://` link on `claude.ai`) is shown as an extra, never graded.
394
394
  - **Getting listed** (task 1003807, [ADR 0343](adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md)). Publishing a version does not by itself put it in the store's catalog, `GET /store/modules`. The catalog shows each module's newest version, judged by its current score. It must pass the Security gate first; a version that failed it, or has not been checked yet, is not listed. A new version with no install or reliability data yet is listed with a **New** label and is not ranked; the floor does not apply to it until it has a real score. Every other version is listed only if its overall score is **at least 50 out of 100**. Below that it is not listed at all. The floor is fixed in Bongos itself (`QUALITY_FLOOR` in `src/bongos/module-listing.js`) and returned in the catalog response; no author or instance setting moves it, and changing it is an owner-approved, reviewed task.
395
395
  - **What a module is strong and weak at** (task 1003799, ADR 0343 D4). Each catalog entry also carries `parts` (the current result of Tests, Clean installs, Reliability, Tester feedback and How-to) and a reading of them: `strengths` (a part scored **80 or more**), `weaknesses` (a part scored **below the floor**) and `unmeasured` (a part with no score yet, which is never counted as weak). Each strength and weakness has a one-sentence `detail` saying what the number rests on. Security is not among them: every listed version has passed it. The reading is `readParts` in `src/bongos/module-listing.js`; the hall's Modules tab shows it in its "In the store" group, so two modules with the same overall score can be told apart.
396
+ - **Sorting by a part** (task 1003800). `GET /store/modules?sort=<part>` orders the listed modules by one part a buyer cares about: `tests`, `install`, `reliability`, `tester_feedback` or `docs` (the response lists them as `sort_parts`, and says which one it applied as `sort`). Modules with a score for that part come first, highest first; a tie goes to the higher overall score, then the module key. A module with no score for that part goes last, so missing data never wins or loses a place on its own. New modules are not sorted: they are never ranked. Any other `sort` value is refused with a 400. Without `sort`, both lists are in key order. The rule is `sortByPart` in `src/bongos/module-listing.js`.
396
397
 
397
398
  ### 7. Install from the store
398
399
 
@@ -5,7 +5,7 @@
5
5
  "adr": "0341 D3",
6
6
  "reread": "node scripts/gds/page-reader.js --stale",
7
7
  "how": ["Generated by node scripts/gds/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
8
- "counts": {"pages":46,"lines":5561,"placed":1706,"shared":398,"unplaced":3457,"omitted_data":3600},
8
+ "counts": {"pages":46,"lines":5563,"placed":1707,"shared":398,"unplaced":3458,"omitted_data":3605},
9
9
  "pages": [
10
10
  {
11
11
  "id": "builders:atlas",
@@ -1885,10 +1885,10 @@
1885
1885
  "surface": "builders",
1886
1886
  "title": "Modules",
1887
1887
  "states_read": ["browse","search-empty","lens","details","out"],
1888
- "files_hash": "e607fd3b8af5c0ba",
1889
- "reading_hash": "9983cfd08a0605e5",
1890
- "counts": {"lines":55,"placed":24,"shared":8,"unplaced":23},
1891
- "omitted": {"data":100},
1888
+ "files_hash": "78b02310ae1e8e04",
1889
+ "reading_hash": "a3861c8a1816328f",
1890
+ "counts": {"lines":57,"placed":25,"shared":8,"unplaced":24},
1891
+ "omitted": {"data":105},
1892
1892
  "notes": ["details: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood"],
1893
1893
  "lines": [
1894
1894
  {"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":872,"string_id":"87cee78ca134"},
@@ -1931,21 +1931,23 @@
1931
1931
  {"key":"L0038","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":187,"string_id":"0215b2cdca8c"},
1932
1932
  {"key":"L0039","section":"Module catalog","text":"A module built here can be submitted upstream for review, so it can join the shared catalog with your name and this project credited.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":74,"string_id":"2e17d7e236d9"},
1933
1933
  {"key":"L0040","section":"Module catalog","text":"In the store","placement":"placed","file":"modules/hall-ui/public/modules.html","line":83,"string_id":"ecfebcd52894"},
1934
- {"key":"L0041","section":"Module catalog","text":"No store module matches that {…}.","placement":"unplaced","state":"search-empty"},
1935
- {"key":"L0042","section":"Module catalog","text":"scores","placement":"unplaced","occurrences":2},
1936
- {"key":"L0043","section":"Module catalog","text":"strong at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":225,"string_id":"e294c3429c2e","occurrences":3},
1937
- {"key":"L0044","section":"Module catalog","text":"weak at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":227,"string_id":"20318b6da94e"},
1938
- {"key":"L0045","section":"Module catalog","text":"A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":87,"string_id":"d316ba4f7e9a"},
1939
- {"key":"L0046","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc"},
1940
- {"key":"L0047","section":"Module catalog","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":810,"string_id":"fd1903586e8e"},
1941
- {"key":"L0048","section":"What are you doing now?","text":"What are you doing now?","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":811,"string_id":"60853319f290"},
1942
- {"key":"L0049","section":"What are you doing now?","text":"Pick one. You can switch any time from the top bar.","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":812,"string_id":"4f87d5b17b54"},
1943
- {"key":"L0050","section":"What are you doing now?","text":"Building","placement":"unplaced"},
1944
- {"key":"L0051","section":"What are you doing now?","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":3},
1945
- {"key":"L0052","section":"What are you doing now?","text":"Artistry","placement":"unplaced"},
1946
- {"key":"L0053","section":"What are you doing now?","text":"Ideating","placement":"unplaced"},
1947
- {"key":"L0054","section":"What are you doing now?","text":"Turn thoughts into {…}.","placement":"unplaced"},
1948
- {"key":"L0055","section":"What are you doing now?","text":"Ask me again next session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":815,"string_id":"fa86bf403775"}
1934
+ {"key":"L0041","section":"Module catalog","text":"Sort by","placement":"placed","file":"modules/hall-ui/public/modules.html","line":87,"string_id":"70383b194ec0"},
1935
+ {"key":"L0042","section":"Module catalog","text":"Name","placement":"unplaced"},
1936
+ {"key":"L0043","section":"Module catalog","text":"No store module matches that {…}.","placement":"unplaced","state":"search-empty"},
1937
+ {"key":"L0044","section":"Module catalog","text":"scores","placement":"unplaced","occurrences":2},
1938
+ {"key":"L0045","section":"Module catalog","text":"strong at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":241,"string_id":"e294c3429c2e","occurrences":3},
1939
+ {"key":"L0046","section":"Module catalog","text":"weak at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":243,"string_id":"20318b6da94e"},
1940
+ {"key":"L0047","section":"Module catalog","text":"A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module. Sort by a part to see the modules strongest at it first; a module with no score for that part goes last, and a new module is never ranked.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":90,"string_id":"e395cd11edab"},
1941
+ {"key":"L0048","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":520,"string_id":"1883ad7f07fc"},
1942
+ {"key":"L0049","section":"Module catalog","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":810,"string_id":"fd1903586e8e"},
1943
+ {"key":"L0050","section":"What are you doing now?","text":"What are you doing now?","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":811,"string_id":"60853319f290"},
1944
+ {"key":"L0051","section":"What are you doing now?","text":"Pick one. You can switch any time from the top bar.","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":812,"string_id":"4f87d5b17b54"},
1945
+ {"key":"L0052","section":"What are you doing now?","text":"Building","placement":"unplaced"},
1946
+ {"key":"L0053","section":"What are you doing now?","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":3},
1947
+ {"key":"L0054","section":"What are you doing now?","text":"Artistry","placement":"unplaced"},
1948
+ {"key":"L0055","section":"What are you doing now?","text":"Ideating","placement":"unplaced"},
1949
+ {"key":"L0056","section":"What are you doing now?","text":"Turn thoughts into {…}.","placement":"unplaced"},
1950
+ {"key":"L0057","section":"What are you doing now?","text":"Ask me again next session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":815,"string_id":"fa86bf403775"}
1949
1951
  ]
1950
1952
  },
1951
1953
  {
@@ -82,9 +82,12 @@
82
82
  <div class="ov-group__head">
83
83
  <span class="ov-group__name">In the store</span>
84
84
  <span class="ov-group__n" id="store-n"></span>
85
+ <!-- task 1003800: the buyer picks the part that matters to them, and
86
+ the server orders by it (sortByPart). Filled from sort_parts. -->
87
+ <label class="ov-group__note">Sort by <select id="store-sort"><option value="">Name</option></select></label>
85
88
  </div>
86
89
  <ul class="ov-rows" id="store-grid"></ul>
87
- <p class="ov-foot" id="store-lede">A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module.</p>
90
+ <p class="ov-foot" id="store-lede">A part is strong at 80 out of 100 or more, and weak below the listing floor. A part with no data yet is not counted against a module. Sort by a part to see the modules strongest at it first; a module with no score for that part goes last, and a new module is never ranked.</p>
88
91
  </div>
89
92
  </section>
90
93
 
@@ -211,6 +211,20 @@
211
211
  // short labels; the sentence behind each is one click away in the details, the
212
212
  // splitCriterion idea: scannable in a list, thorough on demand.
213
213
  let storeModules = [];
214
+ // The part the buyer sorts by (task 1003800): '' is name order. The server owns
215
+ // the order and the list of parts; this page only asks for one and shows it.
216
+ let storeSort = '';
217
+ let storeSortTitle = '';
218
+ let storeSortWired = false;
219
+
220
+ // The sorted-by part's own score, so the order explains itself on every row.
221
+ function sortedPartFact(m) {
222
+ if (!storeSort || m.is_new) return '';
223
+ const p = m.parts && m.parts[storeSort];
224
+ return p && p.score != null
225
+ ? `<span class="ov-fact">${escapeHtml(storeSortTitle)} <b>${escapeHtml(String(p.score))}</b></span>`
226
+ : `<span class="ov-fact">${escapeHtml(storeSortTitle)} not measured yet</span>`;
227
+ }
214
228
 
215
229
  function partsList(items) {
216
230
  return items.map((p) => `${escapeHtml(p.title)} <b>${escapeHtml(String(p.score))}</b>`).join(', ');
@@ -220,6 +234,8 @@
220
234
  const facts = [m.is_new
221
235
  ? '<span class="fact-pill fact-pill--info" title="No real-world data yet, so it is listed but not ranked.">new</span>'
222
236
  : `<span class="ov-fact">scores <b>${escapeHtml(String(m.overall))}</b></span>`];
237
+ const sorted = sortedPartFact(m);
238
+ if (sorted) facts.push(sorted);
223
239
  const strong = Array.isArray(m.strengths) ? m.strengths : [];
224
240
  const weak = Array.isArray(m.weaknesses) ? m.weaknesses : [];
225
241
  if (strong.length) facts.push(`<span class="ov-fact">strong at ${partsList(strong)}</span>`);
@@ -275,11 +291,26 @@
275
291
 
276
292
  // Best-effort and last: the store is a second read, and the modules on disk ARE
277
293
  // this page, so a store that cannot be read costs this group and nothing else.
294
+ // Filled and wired once, on the first store read; a re-read only re-renders.
295
+ function fillStoreSort(parts) {
296
+ const select = $('#store-sort');
297
+ if (storeSortWired || !select || !Array.isArray(parts)) return;
298
+ storeSortWired = true;
299
+ for (const p of parts) select.add(new Option(p.title, p.part));
300
+ select.addEventListener('change', () => {
301
+ storeSort = select.value;
302
+ storeSortTitle = storeSort ? select.options[select.selectedIndex].text : '';
303
+ loadStore();
304
+ });
305
+ }
306
+
278
307
  async function loadStore() {
279
308
  try {
280
- const res = await api.request('GET', `${API}/store/modules`);
309
+ const q = storeSort ? `?sort=${encodeURIComponent(storeSort)}` : '';
310
+ const res = await api.request('GET', `${API}/store/modules${q}`);
281
311
  if (!res.ok) return;
282
312
  const body = res.data || {};
313
+ fillStoreSort(body.sort_parts);
283
314
  storeModules = [...(body.listed || []), ...(body.new || [])];
284
315
  renderStore();
285
316
  } catch (err) {
@@ -12,3 +12,5 @@ Also gone, and the one rule in that list: `.ab-caption` takes the lede's `--meas
12
12
  **A store version's how-to is a view of the Modules page, not a new page (task 1004366).** `/builders/modules?howto=<key>&version=<X.Y.Z>` hides the catalog and shows that version's `HOWTO.md` in the reading room's article card: the page adds `body.room` so `room.css`'s sizes apply, and the markdown goes through `md-reader.js` (author-written, so every byte is escaped; only absolute http(s), mailto and #anchor links become links). A query, not a `#/` route — the hash namespace is frozen to the idea and blocker overlays. "Save as PDF" is `window.print()` with an `@media print` block in `modules.css` that keeps only the document and prints in the system ink (`CanvasText`), because a literal colour is the token tier's. Harness fixture: `store__modules__howto.json`.
13
13
 
14
14
  **The store's modules are a third group on the same panel, read as strengths and weaknesses (task 1003799, ADR 0343 D4).** `#store-group` ("In the store") lists what `GET /store/modules` lists, hidden until that read answers with something; it loads after the modules on disk and best-effort, so a store that cannot be read costs this group and nothing else. A row is the same `.ov-row`: the overall score as a plain fact (or the `new` info pill), `strong at Tests 96, How-to 88` as a plain fact, and `weak at …` as the warn pill, because a weakness is the exception a buyer must not miss. The sentence behind each part ("Tests scored 96 out of 100, from 40 test cases.") and the parts with no data yet sit in the row's `<details class="mod-more">`, the `splitCriterion` idea: short label in the list, the detail one click away. The reading is the server's (`readParts` in `src/bongos/module-listing.js`: strong at 80 or more, weak below the listing floor, unmeasured never weak); the page re-derives nothing. Harness fixture: `store__modules.json`.
15
+
16
+ **The buyer sorts the store by the part that matters to them (task 1003800, owner decision 2026-10-22).** A "Sort by" menu sits in the store group's head (`#store-sort`, `.ov-group__note`): Name, then the parts the server lists in `sort_parts`. Picking one re-reads `GET /store/modules?sort=<part>`, and the server orders the listed modules (`sortByPart` in `src/bongos/module-listing.js`: highest for that part first, a tie to the overall score then the key, a module with no score for the part last). The page never sorts the rows itself, and New modules keep name order because they are never ranked. While a part is picked, each listed row carries that part's score as a plain fact (`Reliability 91`, or `Reliability not measured yet`), so the order explains itself. The owner chose the buyer over a per-category "key part" because nothing records what a category is for, and a hidden weighting would break the score an author can check (ADR 0343 D3).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.153",
3
+ "version": "1.21.154",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.153",
9
+ "version": "1.21.154",
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.153",
3
+ "version": "1.21.154",
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",
@@ -9940,5 +9940,11 @@
9940
9940
  "id": "1004766",
9941
9941
  "text": "When a project's server can't talk to GitHub, the land-status check now says which step failed and what GitHub answered, instead of a bare error."
9942
9942
  }
9943
+ ],
9944
+ "1.21.154": [
9945
+ {
9946
+ "id": "1003800",
9947
+ "text": "In the module store, a buyer can now sort by the score that matters to them, like reliability or tests, so the module strongest at that comes first."
9948
+ }
9943
9949
  ]
9944
9950
  }
@@ -92,6 +92,40 @@ function readParts(signals = [], { floor = QUALITY_FLOOR } = {}) {
92
92
  return { parts, strengths, weaknesses, unmeasured };
93
93
  }
94
94
 
95
+ // ---------------------------------------------------------------------------
96
+ // Sort by the part a buyer cares about (task 1003800, owner decision 2026-10-22)
97
+ // ---------------------------------------------------------------------------
98
+ //
99
+ // A module strong at what a buyer needs should come first for THAT buyer. Nothing
100
+ // records what a category is "for", and a hidden per-category weighting would break
101
+ // the score an author can check (ADR 0343 D3), so the owner chose: the buyer picks
102
+ // the part. The rule, in a sentence an author can check:
103
+ //
104
+ // modules with a score for the part come first, highest first; a tie goes to the
105
+ // higher overall score, then the key. A module with no score for the part goes
106
+ // last, in key order: a missing part is never a zero, and never a first place.
107
+ //
108
+ // Only `listed` is sorted. A "New" version is never ranked (owner decision,
109
+ // 2026-10-01), so `new` keeps key order whatever the buyer picks. Today this runs
110
+ // over the whole store; once approved sub-categories exist (task 1004355) the same
111
+ // rule orders each one (ADR 0342 D2).
112
+ const SORT_PARTS = PARTS.map(({ part, title }) => ({ part, title }));
113
+
114
+ // `entries` carry `parts` as readParts builds them. Returns a new array. Pure.
115
+ function sortByPart(entries, part) {
116
+ const scoreOf = (e) => (e.parts && e.parts[part] ? e.parts[part].score : null);
117
+ return [...entries].sort((a, b) => {
118
+ const sa = scoreOf(a);
119
+ const sb = scoreOf(b);
120
+ if (sa == null || sb == null) {
121
+ if (sa != null) return -1;
122
+ if (sb != null) return 1;
123
+ return a.key.localeCompare(b.key);
124
+ }
125
+ return (sb - sa) || ((b.overall ?? -1) - (a.overall ?? -1)) || a.key.localeCompare(b.key);
126
+ });
127
+ }
128
+
95
129
  // The newest version row per module key, in semver order (a version that does not
96
130
  // parse is skipped, as latestVersions does in module-store.js).
97
131
  function newestPerModule(rows) {
@@ -106,12 +140,13 @@ function newestPerModule(rows) {
106
140
  return [...out.values()];
107
141
  }
108
142
 
109
- // The catalog: { floor, listed: [...], new: [...] }, each entry
143
+ // The catalog: { floor, sort, sort_parts, listed: [...], new: [...] }, each entry
110
144
  // { key, title, version, published_at, overall, is_new, parts, strengths, weaknesses,
111
145
  // unmeasured } (the last four from readParts). Excluded versions are left
112
146
  // out entirely: a module below the bar is not listed at all. Both lists are in key
113
- // order; ranking is a separate task (1003808) and does not happen here.
114
- async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
147
+ // order unless `sort` names a part, which orders `listed` by sortByPart; ranking
148
+ // is a separate task (1003808) and does not happen here.
149
+ async function listCatalog({ db, floor = QUALITY_FLOOR, sort = null } = {}) {
115
150
  const pool = db || require('./pool').pool;
116
151
  const { rows: versions } = await pool.query(
117
152
  `SELECT m.module_key, m.title, v.id AS version_id, v.version, v.published_at
@@ -142,7 +177,7 @@ async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
142
177
  signals.get(k).push(s);
143
178
  }
144
179
  }
145
- const out = { floor, listed: [], new: [] };
180
+ const out = { floor, sort: sort || null, sort_parts: SORT_PARTS, listed: [], new: [] };
146
181
  for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
147
182
  const score = scores.get(String(r.version_id)) || null;
148
183
  const { placement } = placeVersion(score, floor);
@@ -153,7 +188,8 @@ async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
153
188
  ...readParts(signals.get(String(r.version_id)) || [], { floor }),
154
189
  });
155
190
  }
191
+ if (sort) out.listed = sortByPart(out.listed, sort);
156
192
  return out;
157
193
  }
158
194
 
159
- module.exports = { QUALITY_FLOOR, STRONG_AT, placeVersion, readParts, listCatalog };
195
+ module.exports = { QUALITY_FLOOR, STRONG_AT, SORT_PARTS, placeVersion, readParts, sortByPart, listCatalog };
@@ -110,7 +110,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
110
110
  // property access on a CommonJS namespace, so reaching these through one made them read
111
111
  // as unused exports and tripped the dead-code ratchet that held this branch out of main.
112
112
  const { listOverrides, setOverride } = require('../module-overrides');
113
- const { listCatalog } = require('../module-listing');
113
+ const { listCatalog, SORT_PARTS } = require('../module-listing');
114
114
 
115
115
  // The sentence for a toggle that waits on a restart (task 1004463). 'routes' are the
116
116
  // module's pages, 'pollers' its background jobs. PURE.
@@ -733,12 +733,20 @@ module.exports = function buildModulesRouter() {
733
733
  // listed module's newest version, if it passed the Security gate and scores at or
734
734
  // above the floor, or under "new" if it has no real-world data yet. Below the bar
735
735
  // is not listed at all. The floor is fixed in module-listing.js and returned here.
736
+ // `?sort=<part>` orders the listed modules by the part a buyer cares about
737
+ // (task 1003800, sortByPart); the parts it accepts come back as sort_parts.
736
738
  //
737
739
  // rank: any-builder — a read of public store metadata, like /store/modules/latest:
738
740
  // no entitlement taken, nothing changed.
739
741
  router.get('/store/modules', auth.requireBuilder,
740
- asyncHandler('GET /store/modules', async (_req, res) => {
741
- const listing = await listCatalog();
742
+ asyncHandler('GET /store/modules', async (req, res) => {
743
+ const raw = req.query && req.query.sort;
744
+ const sort = typeof raw === 'string' && raw ? raw : null;
745
+ if (sort && !SORT_PARTS.some((p) => p.part === sort)) {
746
+ const names = SORT_PARTS.map((p) => `${p.title} (${p.part})`).join(', ');
747
+ return res.fail('bad_request', { status: 400, message: `The store can be sorted by one part of the score: ${names}.` });
748
+ }
749
+ const listing = await listCatalog({ sort });
742
750
  res.set('Cache-Control', 'no-store');
743
751
  res.json(listing);
744
752
  }));
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.153'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.154'; // 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');
@@ -94,7 +94,7 @@ test('modules: the store group reads strengths and weaknesses off the server row
94
94
  const html = read('modules.html');
95
95
  assert.match(html, /<div class="ov-group" id="store-group" hidden>/, 'the store group is hidden until the store answers');
96
96
  assert.match(html, /<ul class="ov-rows" id="store-grid"><\/ul>/);
97
- assert.match(js, /api\.request\('GET', `\$\{API\}\/store\/modules`\)/, 'it reads GET /store/modules');
97
+ assert.match(js, /api\.request\('GET', `\$\{API\}\/store\/modules\$\{q\}`\)/, 'it reads GET /store/modules');
98
98
  assert.match(js, /<span class="ov-fact">strong at \$\{partsList\(strong\)\}<\/span>/, 'a strength is a plain fact');
99
99
  assert.match(js, /<span class="fact-pill fact-pill--warn">weak at \$\{partsList\(weak\)\}<\/span>/, 'a weakness is the warn pill');
100
100
  assert.match(js, /not measured yet/, 'a part with no data is named, never counted as weak');
@@ -106,6 +106,18 @@ test('modules: the store group reads strengths and weaknesses off the server row
106
106
  assert.notDeepEqual(two[0].strengths.map((s) => s.part), two[1].strengths.map((s) => s.part), '…that are strong at different things');
107
107
  });
108
108
 
109
+ test('modules: the buyer sorts the store by the part that matters to them (task 1003800)', () => {
110
+ const js = read('modules.js');
111
+ const html = read('modules.html');
112
+ assert.match(html, /<select id="store-sort"><option value="">Name<\/option><\/select>/, 'the menu starts at name order');
113
+ assert.match(js, /\?sort=\$\{encodeURIComponent\(storeSort\)\}/, 'the server orders the list; the page asks for a part');
114
+ assert.match(js, /fillStoreSort\(body\.sort_parts\)/, 'the parts on the menu are the server\'s list');
115
+ assert.doesNotMatch(decomment(js), /\.sort\(\(a, b\)/, 'the page does not re-sort the store rows itself');
116
+ assert.match(js, /not measured yet<\/span>/, 'a row with no score for the sorted part says so, not a zero');
117
+ const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'store__modules.json'), 'utf8'));
118
+ assert.deepEqual(fixture.sort_parts.map((p) => p.part), ['tests', 'install', 'reliability', 'tester_feedback', 'docs'], 'the harness carries the menu');
119
+ });
120
+
109
121
  test('modules: the kit filter bar carries the search and a state lens, and the empty state is the kit\'s', () => {
110
122
  const js = read('modules.js');
111
123
  assert.match(js, /kit\.makeFilterBar\(\{/, 'the bar is the kit\'s');
@@ -53,7 +53,7 @@ const poolPath = require.resolve('../src/bongos/pool.js');
53
53
  require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
54
54
 
55
55
  const buildModulesRouter = require('../src/bongos/routes/modules.js');
56
- const { QUALITY_FLOOR, placeVersion } = require('../src/bongos/module-listing.js');
56
+ const { QUALITY_FLOOR, SORT_PARTS, placeVersion, sortByPart } = require('../src/bongos/module-listing.js');
57
57
 
58
58
  function handlerFor(router, method, p) {
59
59
  const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
@@ -163,7 +163,83 @@ test('GET /store/modules: an empty store answers empty lists and the floor', asy
163
163
  state.modules = {}; state.versions = []; state.scores = [];
164
164
  const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
165
165
  const res = await invoke(handle, {});
166
- assert.deepEqual(res.body, { floor: 50, listed: [], new: [] });
166
+ assert.deepEqual(res.body, { floor: 50, sort: null, sort_parts: SORT_PARTS, listed: [], new: [] });
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // sort by the part a buyer cares about (task 1003800, owner decision 2026-10-22)
171
+ // ---------------------------------------------------------------------------
172
+
173
+ const entry = (key, overall, partScores) => ({
174
+ key, overall,
175
+ parts: Object.fromEntries(Object.entries(partScores).map(([p, s]) => [p, { outcome: s == null ? 'no_data' : 'scored', score: s, sample_size: null }])),
176
+ });
177
+
178
+ test('sortByPart: highest first for the chosen part; a tie goes to the overall score, then the key', () => {
179
+ const out = sortByPart([
180
+ entry('b-mod', 70, { reliability: 90 }),
181
+ entry('a-mod', 70, { reliability: 90 }),
182
+ entry('steady', 60, { reliability: 95 }),
183
+ entry('higher', 80, { reliability: 90 }),
184
+ ], 'reliability');
185
+ assert.deepEqual(out.map((e) => e.key), ['steady', 'higher', 'a-mod', 'b-mod']);
186
+ });
187
+
188
+ test('sortByPart: a module with no score for the part goes last, never first, and the input is untouched', () => {
189
+ const input = [entry('z-unmeasured', 99, { reliability: null }), entry('low', 55, { reliability: 10 }), entry('a-missing', 99, {})];
190
+ const out = sortByPart(input, 'reliability');
191
+ assert.deepEqual(out.map((e) => e.key), ['low', 'a-missing', 'z-unmeasured']);
192
+ assert.equal(input[0].key, 'z-unmeasured', 'a new array; the listing it came from keeps its order');
193
+ });
194
+
195
+ test('SORT_PARTS: every part a buyer compares, Security not among them', () => {
196
+ assert.deepEqual(SORT_PARTS.map((p) => p.part), ['tests', 'install', 'reliability', 'tester_feedback', 'docs']);
197
+ });
198
+
199
+ test('GET /store/modules?sort=: orders the listed modules by the part, leaves New in key order', async () => {
200
+ state.modules = {
201
+ tested: { status: 'listed', title: 'Tested' }, steady: { status: 'listed', title: 'Steady' },
202
+ 'b-new': { status: 'listed', title: 'B new' }, 'a-new': { status: 'listed', title: 'A new' },
203
+ };
204
+ state.versions = [
205
+ { id: 21, module_key: 'tested', version: '1.0.0', published_at: 't' },
206
+ { id: 22, module_key: 'steady', version: '1.0.0', published_at: 't' },
207
+ { id: 23, module_key: 'b-new', version: '1.0.0', published_at: 't' },
208
+ { id: 24, module_key: 'a-new', version: '1.0.0', published_at: 't' },
209
+ ];
210
+ state.scores = [
211
+ { id: 21, version_id: 21, computed_at: 1, ...score({ overall: 74 }) },
212
+ { id: 22, version_id: 22, computed_at: 1, ...score({ overall: 74 }) },
213
+ { id: 23, version_id: 23, computed_at: 1, ...score({ is_new: true }) },
214
+ { id: 24, version_id: 24, computed_at: 1, ...score({ is_new: true }) },
215
+ ];
216
+ state.signals = [
217
+ { id: 21, version_id: 21, measured_at: 1, ...sig('tests', 96) },
218
+ { id: 22, version_id: 21, measured_at: 1, ...sig('reliability', 52) },
219
+ { id: 23, version_id: 22, measured_at: 1, ...sig('tests', 58) },
220
+ { id: 24, version_id: 22, measured_at: 1, ...sig('reliability', 91) },
221
+ { id: 25, version_id: 23, measured_at: 1, ...sig('reliability', 99) },
222
+ ];
223
+ const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
224
+ const byRel = await invoke(handle, { query: { sort: 'reliability' } });
225
+ assert.equal(byRel.body.sort, 'reliability');
226
+ assert.deepEqual(byRel.body.listed.map((m) => m.key), ['steady', 'tested'], 'the buyer who cares about reliability sees Steady first');
227
+ assert.deepEqual(byRel.body.new.map((m) => m.key), ['a-new', 'b-new'], 'New is never ranked, whatever the buyer picks');
228
+ const byTests = await invoke(handle, { query: { sort: 'tests' } });
229
+ assert.deepEqual(byTests.body.listed.map((m) => m.key), ['tested', 'steady']);
230
+ const plain = await invoke(handle, { query: {} });
231
+ assert.equal(plain.body.sort, null);
232
+ assert.deepEqual(plain.body.listed.map((m) => m.key), ['steady', 'tested'], 'no sort is key order, as before');
233
+ state.signals = [];
234
+ });
235
+
236
+ test('GET /store/modules?sort=: a part it does not know is refused, Security included', async () => {
237
+ const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
238
+ for (const sort of ['security', 'price', 'overall']) {
239
+ const res = await invoke(handle, { query: { sort } });
240
+ assert.equal(res.statusCode, 400, `${sort} is not a part to sort by`);
241
+ assert.match(res.body.message, /Tests \(tests\), Clean installs \(install\), Reliability \(reliability\), Tester feedback \(tester_feedback\), How-to \(docs\)/, 'the refusal names each part as a person says it, with the value to send');
242
+ }
167
243
  });
168
244
 
169
245
  // ---------------------------------------------------------------------------