@bongos/core 1.21.74 → 1.21.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.74",
6
- "core_contract": "1.21.74",
7
- "source_commit": "e1a614cc89edf2e1f13bf098fe70e5d6909887f9",
5
+ "core_version": "1.21.75",
6
+ "core_contract": "1.21.75",
7
+ "source_commit": "fdb1b2948dda051d6bd0b5b5e79a1382a7e93f63",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-08T17:20:42.399Z",
9
+ "built_at": "2026-10-08T17:33:55.022Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 589,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 2980,
14
+ "functional_verbatim": 2982,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3598,
20
- "tree_sha256": "a36fc59cee5bf88f0f473a9967f3a2a30e9ab3d59e59ef3d5b91df49789fc855",
19
+ "file_count": 3600,
20
+ "tree_sha256": "9485028ef2ecfaf25ef937e1315cca15d5526ff1d11ed8a280e78a28177fd26e",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -312,12 +312,12 @@
312
312
  {
313
313
  "path": "clients/bongos-client/README.md",
314
314
  "mode": "0000644",
315
- "sha256": "92ec02892192cb3ec336b8feb68516981334c6c5e9951d6983855296b6d79dfc"
315
+ "sha256": "cdea685186e07a106fcd43e603d006648d5d681f68895989915f3a38a54f3073"
316
316
  },
317
317
  {
318
318
  "path": "clients/bongos-client/bongos-client.global.js",
319
319
  "mode": "0000644",
320
- "sha256": "16b666c57928d55b8c5b8b96f90e77e3d1702ea2b601d43a118853ae7d6f5f10"
320
+ "sha256": "d244bccc1ec1ced97ebdc3219dbe130d9a615d12be060e8479e46368a95862bc"
321
321
  },
322
322
  {
323
323
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -327,17 +327,17 @@
327
327
  {
328
328
  "path": "clients/bongos-client/index.cjs",
329
329
  "mode": "0000644",
330
- "sha256": "cac495a3acbe96e8036f79dd772cce00372bac4c3a162c7a050d572cabccb464"
330
+ "sha256": "17bbcc0e24a99a7e8e6cba5f04db7fc210baf2fc119863c51698f69fd04771c4"
331
331
  },
332
332
  {
333
333
  "path": "clients/bongos-client/index.d.ts",
334
334
  "mode": "0000644",
335
- "sha256": "4ecbd807e9ddbd35f4c076b6daa559fa4a6eeaab80e3f03065d61e4ca139f6dd"
335
+ "sha256": "88bcc5e74a08fb574b1f1a81d40355a69ffd8271a75e9fdd25038c60a67b6ff3"
336
336
  },
337
337
  {
338
338
  "path": "clients/bongos-client/index.mjs",
339
339
  "mode": "0000644",
340
- "sha256": "6217699f613e7839e12aee3465717863397243daf741c022d43a704993c9023d"
340
+ "sha256": "042c25ee1600192bdaa6c7c817ff94558a884a428a7ab5b06dbf2b665fc89101"
341
341
  },
342
342
  {
343
343
  "path": "clients/bongos-client/package.json",
@@ -2172,7 +2172,7 @@
2172
2172
  {
2173
2173
  "path": "docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md",
2174
2174
  "mode": "0000644",
2175
- "sha256": "668d76cffbfaeb9662fc84b4d2c185bc7cd0109a403e6b9bf3032ebe3b569457"
2175
+ "sha256": "db41e62093dd09e5ed3f9bb4628c98ea0255b487bb5b94191bbb4adc0651636b"
2176
2176
  },
2177
2177
  {
2178
2178
  "path": "docs/adr/0344-a-tester-is-a-project-that-opts-in-once.md",
@@ -2312,12 +2312,12 @@
2312
2312
  {
2313
2313
  "path": "docs/api-reference.md",
2314
2314
  "mode": "0000644",
2315
- "sha256": "7ee0c370728c049634024bb204f60667a3a975f7d42dc0a714d14a1feb24f7f5"
2315
+ "sha256": "9d29a5b16d79cbf4da889fd3d58c7164a8f70f511fd9de75b94f321ce7e01f1c"
2316
2316
  },
2317
2317
  {
2318
2318
  "path": "docs/api/openapi.json",
2319
2319
  "mode": "0000644",
2320
- "sha256": "3e3bcaac1dbdd052ad5e7c44ec77bf057faf71c76202bb6d43bd7852b0b1999a"
2320
+ "sha256": "2f5f417f06c5b9e99e02b830fe9309c55a901cff059be909a18078a4b3ad0dee"
2321
2321
  },
2322
2322
  {
2323
2323
  "path": "docs/architecture.md",
@@ -2857,12 +2857,12 @@
2857
2857
  {
2858
2858
  "path": "docs/module-api-changelog.md",
2859
2859
  "mode": "0000644",
2860
- "sha256": "6028080759e4c92a71ed09f4bbe67d8bfae0152377c4f3a635d02841b29fa2c7"
2860
+ "sha256": "244e5150f6cb37861f017cf08c10d4396d641be31e7c27836ff278045b8c7365"
2861
2861
  },
2862
2862
  {
2863
2863
  "path": "docs/modules-contract.md",
2864
2864
  "mode": "0000644",
2865
- "sha256": "33eb2665aea5cc00feb1de80b673f6063f98b2a8bd2caeb6fa4b09e41965c1df"
2865
+ "sha256": "55a964846d438df4265777244e046b5b86deaebda3a6081c6f10a4a5a7a0dbf3"
2866
2866
  },
2867
2867
  {
2868
2868
  "path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
@@ -10057,12 +10057,12 @@
10057
10057
  {
10058
10058
  "path": "package-lock.json",
10059
10059
  "mode": "0000644",
10060
- "sha256": "ebda74c3eebe46d4e8a441c4b63a195fc9d6aa911edfffdca7d9a5189ce29014"
10060
+ "sha256": "b55138ff35c924085a5d9b12fa4651c403dea3a2f1c4d3ee9e2d1a5fcb931314"
10061
10061
  },
10062
10062
  {
10063
10063
  "path": "package.json",
10064
10064
  "mode": "0000644",
10065
- "sha256": "5c6a09991c3531cf64543c466ed124dc5f19825906c42e176c00e5cdae44c0d5"
10065
+ "sha256": "e99024f875cb0765bdf60139021c5bf984d616c954eec42444666a0b79395c94"
10066
10066
  },
10067
10067
  {
10068
10068
  "path": "public-docs/index.html",
@@ -10082,7 +10082,7 @@
10082
10082
  {
10083
10083
  "path": "release-notes.json",
10084
10084
  "mode": "0000644",
10085
- "sha256": "b64964b1e7abbaa7a3f508f4ae02c06928e4e59d61adf1c6402584a6e681e710"
10085
+ "sha256": "c545e162df4719ea3f8712009774705b406674c314973d49487b2d31b71185dd"
10086
10086
  },
10087
10087
  {
10088
10088
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12069,6 +12069,11 @@
12069
12069
  "mode": "0000644",
12070
12070
  "sha256": "917f364b804f25437dea7fe507abe50e9b64cac79b6cc29ac786de0e24350d33"
12071
12071
  },
12072
+ {
12073
+ "path": "src/bongos/module-listing.js",
12074
+ "mode": "0000644",
12075
+ "sha256": "15047b229ce7c3633d74597fb8f971db00fac2ddae5fb0038666fa0896b25574"
12076
+ },
12072
12077
  {
12073
12078
  "path": "src/bongos/module-overrides.js",
12074
12079
  "mode": "0000644",
@@ -12237,7 +12242,7 @@
12237
12242
  {
12238
12243
  "path": "src/bongos/routes/modules.js",
12239
12244
  "mode": "0000644",
12240
- "sha256": "fb935713e6d817c5b3ea16edd620ad50c7c0ff5992c4b70de5ccf14eac73192d"
12245
+ "sha256": "6c20741df0b1395355abf58146ad6753197dcfde928e532b43f69ecf1ce7a325"
12241
12246
  },
12242
12247
  {
12243
12248
  "path": "src/bongos/routes/my-sessions.js",
@@ -12317,7 +12322,7 @@
12317
12322
  {
12318
12323
  "path": "src/module-api.js",
12319
12324
  "mode": "0000644",
12320
- "sha256": "f0769084872b98cacd871ea7e0b062dd1fcaad8c759e38acad6393f94084b418"
12325
+ "sha256": "e2dc4fbb86019edff6c023c9b78afdd70fa04010249e28eee821da8383b2252e"
12321
12326
  },
12322
12327
  {
12323
12328
  "path": "src/module-loader/catalog.js",
@@ -15674,6 +15679,11 @@
15674
15679
  "mode": "0000644",
15675
15680
  "sha256": "43828c1a95652180d558e7676cc2053d0599a61c0edf4cd1ab100820fc7de493"
15676
15681
  },
15682
+ {
15683
+ "path": "tests/module_store_listing.mjs",
15684
+ "mode": "0000644",
15685
+ "sha256": "289478dfc8666527e919a28db3127807aefe87c19849001f133780bb8a8ac816"
15686
+ },
15677
15687
  {
15678
15688
  "path": "tests/module_store_publish.mjs",
15679
15689
  "mode": "0000644",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 525 operations across 75 resource groups
8
+ - 526 operations across 75 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -1149,6 +1149,8 @@ function createClient(opts = {}) {
1149
1149
  "store": {
1150
1150
  // GET /store/entitlements — rank: any-builder — GET /store/entitlements
1151
1151
  getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
1152
+ // GET /store/modules — rank: any-builder — GET /store/modules
1153
+ getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
1152
1154
  // POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
1153
1155
  postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
1154
1156
  // POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
@@ -1148,6 +1148,8 @@ function createClient(opts = {}) {
1148
1148
  "store": {
1149
1149
  // GET /store/entitlements — rank: any-builder — GET /store/entitlements
1150
1150
  getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
1151
+ // GET /store/modules — rank: any-builder — GET /store/modules
1152
+ getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
1151
1153
  // POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
1152
1154
  postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
1153
1155
  // POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
@@ -1649,6 +1649,8 @@ export interface BongosClient {
1649
1649
  "store": {
1650
1650
  /** GET /store/entitlements — rank: any-builder */
1651
1651
  getStoreEntitlements(args?: RequestArgs): Promise<GetStoreEntitlementsResponse>;
1652
+ /** GET /store/modules — rank: any-builder */
1653
+ getStoreModules(args?: RequestArgs): Promise<ApiResponse>;
1652
1654
  /** POST /store/modules/{key}/acquire — rank: any-builder */
1653
1655
  postStoreModulesKeyAcquire(args?: RequestArgs & { body?: PostStoreModulesKeyAcquireRequest }): Promise<PostStoreModulesKeyAcquireResponse>;
1654
1656
  /** POST /store/modules/{key}/acquired — rank: any-builder */
@@ -1145,6 +1145,8 @@ export function createClient(opts = {}) {
1145
1145
  "store": {
1146
1146
  // GET /store/entitlements — rank: any-builder — GET /store/entitlements
1147
1147
  getStoreEntitlements: (args) => request("GET", "/store/entitlements", { hasBody: false }, args),
1148
+ // GET /store/modules — rank: any-builder — GET /store/modules
1149
+ getStoreModules: (args) => request("GET", "/store/modules", { hasBody: false }, args),
1148
1150
  // POST /store/modules/{key}/acquire — rank: any-builder — POST /store/modules/:key/acquire
1149
1151
  postStoreModulesKeyAcquire: (args) => request("POST", "/store/modules/{key}/acquire", { hasBody: true }, args),
1150
1152
  // POST /store/modules/{key}/acquired — rank: any-builder — POST /store/modules/:key/acquired
@@ -66,6 +66,18 @@ couldn't have yet. The floor and the ranking decide how New modules are placed (
66
66
  Price is never an input. The Metic+ override (task 1003796) and delist (task 1003797) sit on
67
67
  top of the computed score and never replace how it is computed. Every override is recorded.
68
68
 
69
+ ## Implementation note — the floor (task 1003807, 2026-10-03)
70
+
71
+ The owner set the floor on 2026-10-01: **50 out of 100**, fixed in Bongos itself
72
+ (`QUALITY_FLOOR` in `src/bongos/module-listing.js`). It is not a live setting and no new
73
+ permission moves it; changing it is an owner-approved, reviewed task, so no author can
74
+ move it. `GET /store/modules` lists each listed module's newest version by its current
75
+ score: the Security gate must have passed (failed or not yet run means not listed); a
76
+ version still labelled New (D5) is listed under "new", never ranked, and the floor does
77
+ not apply to it until it has a real score; any other version is listed only at or above
78
+ the floor. Below the bar is not listed at all. Authors read the rule in
79
+ `docs/modules-contract.md` ("Getting listed").
80
+
69
81
  ## Consequences
70
82
 
71
83
  - **Day one, only Tests counts toward the overall score**, behind the Security gate. The number
@@ -21492,6 +21492,37 @@
21492
21492
  ]
21493
21493
  }
21494
21494
  },
21495
+ "/store/modules": {
21496
+ "get": {
21497
+ "operationId": "get_store_modules",
21498
+ "tags": [
21499
+ "store"
21500
+ ],
21501
+ "summary": "GET /store/modules",
21502
+ "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).",
21503
+ "x-rank": "any-builder",
21504
+ "x-source": "src/bongos/routes/modules.js",
21505
+ "responses": {
21506
+ "200": {
21507
+ "description": "Success."
21508
+ },
21509
+ "400": {
21510
+ "$ref": "#/components/responses/BadRequest"
21511
+ },
21512
+ "401": {
21513
+ "$ref": "#/components/responses/Unauthorized"
21514
+ },
21515
+ "403": {
21516
+ "$ref": "#/components/responses/Forbidden"
21517
+ }
21518
+ },
21519
+ "security": [
21520
+ {
21521
+ "builderSession": []
21522
+ }
21523
+ ]
21524
+ }
21525
+ },
21495
21526
  "/store/modules/latest": {
21496
21527
  "get": {
21497
21528
  "operationId": "get_store_modules_latest",
@@ -33747,7 +33778,7 @@
33747
33778
  "description": "A required dependency/feature is not configured or is temporarily down."
33748
33779
  }
33749
33780
  },
33750
- "x-endpoint-count": 525,
33781
+ "x-endpoint-count": 526,
33751
33782
  "x-schema-count": 567,
33752
33783
  "x-undocumented-bodies": 12,
33753
33784
  "x-response-schemas": 376,
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 525 endpoints across 93 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 526 endpoints across 93 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -814,11 +814,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
814
814
  | GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
815
815
  | POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
816
816
 
817
- ## `store` (10)
817
+ ## `store` (11)
818
818
 
819
819
  | Method | Path | Rank | Body | Description |
820
820
  |---|---|---|---|---|
821
821
  | GET | `/api/bongos/store/entitlements` | `any-builder` | — | GET /api/bongos/store/entitlements and /store/modules/:key/entitlement — the read side of the entitlement record (task 1003813, ADR 0338 … |
822
+ | GET | `/api/bongos/store/modules` | `any-builder` | — | The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each listed module's newest version, if it passed the Security … |
822
823
  | POST | `/api/bongos/store/modules/:key/acquire` | `any-builder` | `version` | ---- install: acquire → download → record (task 1003785, ADR 0338 D1) ---- The server end of `bongos module install`. |
823
824
  | POST | `/api/bongos/store/modules/:key/acquired` | `any-builder` | `version` | |
824
825
  | POST | `/api/bongos/store/modules/:key/delist` | `metic+archon` | `reason` | POST /api/bongos/store/modules/:key/delist — a Metic+ person takes a module out of the store (task 1003797, ADR 0338 D2). |
@@ -2980,5 +2980,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2980
2980
  landed since 1.21.72 with no explicit bump. run 37803458577. (task 1002620)
2981
2981
  1.21.74 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2982
2982
  landed since 1.21.73 with no explicit bump. run 37815719494. (task 1002620)
2983
+ 1.21.75 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2984
+ landed since 1.21.74 with no explicit bump. run 37817454172. (task 1002620)
2983
2985
  ---------------------------------------------------------------------------
2984
2986
  ```
@@ -362,6 +362,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
362
362
  - The publish denylist applies (ADR 0098), plus a refusal of credential-named files (`.env*`, `*.pem`, `*.key`, `id_rsa`, …) anywhere in the module. The store re-checks every hash and the denylist itself.
363
363
  - Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
364
364
  - **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.
365
+ - **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.
365
366
 
366
367
  ### 7. Install from the store
367
368
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.74",
3
+ "version": "1.21.75",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.74",
9
+ "version": "1.21.75",
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.74",
3
+ "version": "1.21.75",
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",
@@ -9254,5 +9254,11 @@
9254
9254
  "id": "1004628",
9255
9255
  "text": "Projects now have a Ship mode setting. Auto, the default, means the AI ships finished work on its own. Manual means it asks first. Owners can switch it on the project settings page."
9256
9256
  }
9257
+ ],
9258
+ "1.21.75": [
9259
+ {
9260
+ "id": "1003807",
9261
+ "text": "The module store now only shows modules that are safe and good enough: each must pass the security check, and once it has real-world data it needs a score of at least 50 out of 100. Brand-new modules show with a 'New' label un"
9262
+ }
9257
9263
  ]
9258
9264
  }
@@ -0,0 +1,85 @@
1
+ // src/bongos/module-listing.js — which store modules the catalog lists, under the
2
+ // quality floor (task 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5).
3
+ //
4
+ // One module appears once, as its NEWEST published version (semver order), judged by
5
+ // that version's CURRENT score: the newest module_assessment_scores row for it
6
+ // (computed_at DESC, id DESC), whether computed or a Metic+ override (core_265).
7
+ // placeVersion is the whole rule and is pure, so tests drive it directly:
8
+ //
9
+ // excluded the Security gate has not passed (false, or not run yet), or the
10
+ // version has no score row at all. The gate is a hard exclusion (D2).
11
+ // new passed the gate but has no real-world data yet (is_new, D5). Listed
12
+ // with a "New" label and never ranked: the floor does not apply until
13
+ // it has a real score (owner decision, 2026-10-01).
14
+ // listed passed the gate and its overall score is at least the floor.
15
+ // excluded otherwise: scored below the floor, or no overall to compare.
16
+ //
17
+ // A delisted module (ADR 0338 D2) is never in the catalog, whatever its score.
18
+
19
+ const { parseVersion, cmp } = require('../module-loader/semver');
20
+
21
+ // The quality floor, 0-100. It lives here and nowhere else: it is part of Bongos
22
+ // itself, not a live setting, so no author, builder or instance can move it.
23
+ // Changing it is an owner-approved, reviewed task (owner decision, 2026-10-01).
24
+ // Authors read the rule in docs/modules-contract.md ("Getting listed").
25
+ const QUALITY_FLOOR = 50;
26
+
27
+ // Where one version stands. `score` is its current score row, or null.
28
+ // Returns { placement: 'listed' | 'new' | 'excluded', reason }.
29
+ function placeVersion(score, floor = QUALITY_FLOOR) {
30
+ if (!score) return { placement: 'excluded', reason: 'not_assessed' };
31
+ if (score.security_passed !== true) return { placement: 'excluded', reason: 'security_not_passed' };
32
+ if (score.is_new) return { placement: 'new', reason: 'new' };
33
+ if (score.overall == null) return { placement: 'excluded', reason: 'no_score' };
34
+ if (score.overall < floor) return { placement: 'excluded', reason: 'below_floor' };
35
+ return { placement: 'listed', reason: 'at_or_above_floor' };
36
+ }
37
+
38
+ // The newest version row per module key, in semver order (a version that does not
39
+ // parse is skipped, as latestVersions does in module-store.js).
40
+ function newestPerModule(rows) {
41
+ const out = new Map();
42
+ for (const r of rows) {
43
+ const next = parseVersion(r.version);
44
+ if (!next) continue;
45
+ const have = out.get(r.module_key);
46
+ if (have && cmp(parseVersion(have.version), next) >= 0) continue;
47
+ out.set(r.module_key, r);
48
+ }
49
+ return [...out.values()];
50
+ }
51
+
52
+ // The catalog: { floor, listed: [...], new: [...] }, each entry
53
+ // { key, title, version, published_at, overall, is_new }. Excluded versions are left
54
+ // out entirely: a module below the bar is not listed at all. Both lists are in key
55
+ // order; ranking is a separate task (1003808) and does not happen here.
56
+ async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
57
+ const pool = db || require('./pool').pool;
58
+ const { rows: versions } = await pool.query(
59
+ `SELECT m.module_key, m.title, v.id AS version_id, v.version, v.published_at
60
+ FROM store_modules m JOIN store_module_versions v ON v.module_key = m.module_key
61
+ WHERE m.status = 'listed'`);
62
+ const newest = newestPerModule(versions);
63
+ const scores = new Map();
64
+ if (newest.length) {
65
+ const { rows } = await pool.query(
66
+ `SELECT DISTINCT ON (version_id) version_id, overall, security_passed, is_new
67
+ FROM module_assessment_scores
68
+ WHERE version_id = ANY($1)
69
+ ORDER BY version_id, computed_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
70
+ for (const s of rows) scores.set(String(s.version_id), s);
71
+ }
72
+ const out = { floor, listed: [], new: [] };
73
+ for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
74
+ const score = scores.get(String(r.version_id)) || null;
75
+ const { placement } = placeVersion(score, floor);
76
+ if (placement === 'excluded') continue;
77
+ out[placement].push({
78
+ key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
79
+ overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+
85
+ module.exports = { QUALITY_FLOOR, placeVersion, listCatalog };
@@ -97,6 +97,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
97
97
  // property access on a CommonJS namespace, so reaching these through one made them read
98
98
  // as unused exports and tripped the dead-code ratchet that held this branch out of main.
99
99
  const { listOverrides, setOverride } = require('../module-overrides');
100
+ const { listCatalog } = require('../module-listing');
100
101
 
101
102
  // A packaged submission embeds every file's content (base64) — cap the total
102
103
  // so a runaway/misbuilt module can't bloat the review-queue table. Generous
@@ -692,6 +693,20 @@ module.exports = function buildModulesRouter() {
692
693
  res.json({ latest });
693
694
  }));
694
695
 
696
+ // The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each
697
+ // listed module's newest version, if it passed the Security gate and scores at or
698
+ // above the floor, or under "new" if it has no real-world data yet. Below the bar
699
+ // is not listed at all. The floor is fixed in module-listing.js and returned here.
700
+ //
701
+ // rank: any-builder — a read of public store metadata, like /store/modules/latest:
702
+ // no entitlement taken, nothing changed.
703
+ router.get('/store/modules', auth.requireBuilder,
704
+ asyncHandler('GET /store/modules', async (_req, res) => {
705
+ const listing = await listCatalog();
706
+ res.set('Cache-Control', 'no-store');
707
+ res.json(listing);
708
+ }));
709
+
695
710
  router.post('/store/modules/:key/acquired', auth.requireBuilder,
696
711
  asyncHandler('POST /store/modules/:key/acquired', async (req, res) => {
697
712
  const { key } = req.params;
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.74'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.75'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,156 @@
1
+ // tests/module_store_listing.mjs — the store catalog under the quality floor (task
2
+ // 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5). Two halves, no port and
3
+ // no real DB:
4
+ // - the rule: placeVersion, pure — the Security gate excludes, "New" is listed
5
+ // unranked, a real score must reach the floor;
6
+ // - the route: the real GET /store/modules handler off the real router, with
7
+ // pool.js replaced in the require cache (the module_upgrade_updates.mjs harness).
8
+ //
9
+ // Run: node tests/module_store_listing.mjs
10
+
11
+ import { strict as assert } from 'node:assert';
12
+ import { test } from 'node:test';
13
+ import { createRequire } from 'node:module';
14
+
15
+ const require = createRequire(import.meta.url);
16
+
17
+ // ---- the fake registry the route reads ----
18
+ const state = { modules: {}, versions: [], scores: [] };
19
+ const pool = {
20
+ async query(sql, params = []) {
21
+ if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
22
+ return {
23
+ rows: state.versions
24
+ .filter((v) => state.modules[v.module_key] && state.modules[v.module_key].status === 'listed')
25
+ .map((v) => ({ ...v, version_id: v.id, title: state.modules[v.module_key].title })),
26
+ };
27
+ }
28
+ if (/FROM module_assessment_scores/.test(sql)) {
29
+ const ids = params[0].map(String);
30
+ const newest = new Map();
31
+ for (const s of state.scores) {
32
+ if (!ids.includes(String(s.version_id))) continue;
33
+ const have = newest.get(s.version_id);
34
+ if (!have || s.computed_at > have.computed_at || (s.computed_at === have.computed_at && s.id > have.id)) newest.set(s.version_id, s);
35
+ }
36
+ return { rows: [...newest.values()] };
37
+ }
38
+ return { rows: [] };
39
+ },
40
+ };
41
+ const poolPath = require.resolve('../src/bongos/pool.js');
42
+ require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
43
+
44
+ const buildModulesRouter = require('../src/bongos/routes/modules.js');
45
+ const { QUALITY_FLOOR, placeVersion } = require('../src/bongos/module-listing.js');
46
+
47
+ function handlerFor(router, method, p) {
48
+ const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
49
+ assert.ok(layer, `no ${method.toUpperCase()} ${p} on the router`);
50
+ const stack = layer.route.stack;
51
+ return { handle: stack[stack.length - 1].handle, names: stack.map((s) => s.name) };
52
+ }
53
+
54
+ async function invoke(handle, req) {
55
+ const res = {
56
+ statusCode: 200, body: undefined, headers: {},
57
+ json(b) { this.body = b; return this; },
58
+ status(c) { this.statusCode = c; return this; },
59
+ set(k, v) { this.headers[k] = v; return this; },
60
+ fail(code, opts) { this.statusCode = opts.status; this.body = { error: code, ...opts }; return this; },
61
+ };
62
+ let nextErr = null;
63
+ await handle(req, res, (err) => { nextErr = err; });
64
+ if (nextErr) throw nextErr;
65
+ return res;
66
+ }
67
+
68
+ const score = (extra = {}) => ({ overall: 70, security_passed: true, is_new: false, ...extra });
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // the rule (pure)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ test('the floor is 50 out of 100 (owner decision, 2026-10-01)', () => {
75
+ assert.equal(QUALITY_FLOOR, 50);
76
+ });
77
+
78
+ test('placeVersion: the Security gate must have PASSED — false or not run yet excludes', () => {
79
+ assert.equal(placeVersion(score({ security_passed: false })).placement, 'excluded');
80
+ assert.equal(placeVersion(score({ security_passed: null })).placement, 'excluded');
81
+ assert.equal(placeVersion(score({ security_passed: false, is_new: true })).placement, 'excluded');
82
+ assert.equal(placeVersion(score({ security_passed: false })).reason, 'security_not_passed');
83
+ });
84
+
85
+ test('placeVersion: no score row at all is excluded, not failed silently', () => {
86
+ assert.deepEqual(placeVersion(null), { placement: 'excluded', reason: 'not_assessed' });
87
+ });
88
+
89
+ test('placeVersion: a New version that passed the gate is listed as new, whatever its number', () => {
90
+ assert.equal(placeVersion(score({ is_new: true, overall: 10 })).placement, 'new');
91
+ assert.equal(placeVersion(score({ is_new: true, overall: null })).placement, 'new');
92
+ });
93
+
94
+ test('placeVersion: a scored version must reach the floor — at the floor is in, one below is out', () => {
95
+ assert.equal(placeVersion(score({ overall: 50 })).placement, 'listed');
96
+ assert.equal(placeVersion(score({ overall: 100 })).placement, 'listed');
97
+ assert.deepEqual(placeVersion(score({ overall: 49 })), { placement: 'excluded', reason: 'below_floor' });
98
+ assert.equal(placeVersion(score({ overall: null })).placement, 'excluded');
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // the route
103
+ // ---------------------------------------------------------------------------
104
+
105
+ test('GET /store/modules: any signed-in builder, no extra permission', () => {
106
+ const { names } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
107
+ assert.equal(names.length, 2, `expected requireBuilder + handler, got ${names.join(', ')}`);
108
+ });
109
+
110
+ test('GET /store/modules: lists by the newest version\'s current score, drops what falls short', async () => {
111
+ state.modules = {
112
+ good: { status: 'listed', title: 'Good' },
113
+ fresh: { status: 'listed', title: 'Fresh' },
114
+ low: { status: 'listed', title: 'Low' },
115
+ unsafe: { status: 'listed', title: 'Unsafe' },
116
+ unscored: { status: 'listed', title: 'Unscored' },
117
+ gone: { status: 'delisted', title: 'Gone' },
118
+ fixed: { status: 'listed', title: 'Fixed' },
119
+ };
120
+ state.versions = [
121
+ { id: 1, module_key: 'good', version: '1.9.0', published_at: 't1' },
122
+ { id: 2, module_key: 'good', version: '1.10.0', published_at: 't2' },
123
+ { id: 3, module_key: 'fresh', version: '0.1.0', published_at: 't3' },
124
+ { id: 4, module_key: 'low', version: '1.0.0', published_at: 't4' },
125
+ { id: 5, module_key: 'unsafe', version: '1.0.0', published_at: 't5' },
126
+ { id: 6, module_key: 'unscored', version: '1.0.0', published_at: 't6' },
127
+ { id: 7, module_key: 'gone', version: '1.0.0', published_at: 't7' },
128
+ { id: 8, module_key: 'fixed', version: '2.0.0', published_at: 't8' },
129
+ ];
130
+ state.scores = [
131
+ { id: 1, version_id: 1, computed_at: 1, ...score({ overall: 20 }) }, // an OLDER version scored low
132
+ { id: 2, version_id: 2, computed_at: 1, ...score({ overall: 80 }) },
133
+ { id: 3, version_id: 3, computed_at: 1, ...score({ is_new: true, overall: 30 }) },
134
+ { id: 4, version_id: 4, computed_at: 1, ...score({ overall: 49 }) },
135
+ { id: 5, version_id: 5, computed_at: 1, ...score({ overall: 95, security_passed: false }) },
136
+ { id: 6, version_id: 7, computed_at: 1, ...score({ overall: 99 }) },
137
+ { id: 7, version_id: 8, computed_at: 1, ...score({ overall: 40 }) }, // superseded by the newer row
138
+ { id: 8, version_id: 8, computed_at: 2, ...score({ overall: 60 }) },
139
+ ];
140
+ const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
141
+ const res = await invoke(handle, {});
142
+ assert.equal(res.statusCode, 200);
143
+ assert.equal(res.headers['Cache-Control'], 'no-store');
144
+ assert.equal(res.body.floor, 50);
145
+ assert.deepEqual(res.body.listed.map((m) => [m.key, m.version, m.overall]), [['fixed', '2.0.0', 60], ['good', '1.10.0', 80]]);
146
+ assert.deepEqual(res.body.new.map((m) => [m.key, m.is_new, m.overall]), [['fresh', true, null]]);
147
+ const all = [...res.body.listed, ...res.body.new].map((m) => m.key);
148
+ for (const k of ['low', 'unsafe', 'unscored', 'gone']) assert.ok(!all.includes(k), `${k} must not be listed`);
149
+ });
150
+
151
+ test('GET /store/modules: an empty store answers empty lists and the floor', async () => {
152
+ state.modules = {}; state.versions = []; state.scores = [];
153
+ const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
154
+ const res = await invoke(handle, {});
155
+ assert.deepEqual(res.body, { floor: 50, listed: [], new: [] });
156
+ });