@bongos/core 1.21.32 → 1.21.33

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.
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2422,
7
- "unique_ids": 2105,
8
- "certain": 2422,
6
+ "strings": 2424,
7
+ "unique_ids": 2107,
8
+ "certain": 2424,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 220
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 130,
16
- "strings": 1773
16
+ "strings": 1775
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -4682,7 +4682,7 @@
4682
4682
  "surface": "builders-hall",
4683
4683
  "text": "Could not load modules.",
4684
4684
  "file": "modules/hall-ui/public/modules.js",
4685
- "line": 328,
4685
+ "line": 335,
4686
4686
  "origin": "js-notice",
4687
4687
  "confidence": "certain"
4688
4688
  },
@@ -4862,7 +4862,7 @@
4862
4862
  "surface": "builders-hall",
4863
4863
  "text": "Could not reach the submit endpoint.",
4864
4864
  "file": "modules/hall-ui/public/modules.js",
4865
- "line": 307,
4865
+ "line": 314,
4866
4866
  "origin": "js-notice",
4867
4867
  "confidence": "certain"
4868
4868
  },
@@ -8948,7 +8948,7 @@
8948
8948
  "surface": "builders-hall",
8949
8949
  "text": "Nothing to submit — every module here ships with the core.",
8950
8950
  "file": "modules/hall-ui/public/modules.js",
8951
- "line": 268,
8951
+ "line": 275,
8952
8952
  "origin": "js-text-assign",
8953
8953
  "confidence": "certain"
8954
8954
  },
@@ -11742,6 +11742,15 @@
11742
11742
  "origin": "js-text-assign",
11743
11743
  "confidence": "certain"
11744
11744
  },
11745
+ {
11746
+ "id": "8b3cdb88b0f4",
11747
+ "surface": "builders-hall",
11748
+ "text": "Taken out of the store. Your copy keeps working, but it will get no more updates.",
11749
+ "file": "modules/hall-ui/public/modules.js",
11750
+ "line": 113,
11751
+ "origin": "js-markup",
11752
+ "confidence": "certain"
11753
+ },
11745
11754
  {
11746
11755
  "id": "e604fc3ace79",
11747
11756
  "surface": "builders-hall",
@@ -15216,6 +15225,15 @@
15216
15225
  "origin": "js-markup",
15217
15226
  "confidence": "certain"
15218
15227
  },
15228
+ {
15229
+ "id": "953f99598871",
15230
+ "surface": "builders-hall",
15231
+ "text": "delisted · no more updates",
15232
+ "file": "modules/hall-ui/public/modules.js",
15233
+ "line": 113,
15234
+ "origin": "js-markup",
15235
+ "confidence": "certain"
15236
+ },
15219
15237
  {
15220
15238
  "id": "563e03d200c6",
15221
15239
  "surface": "builders-hall",
@@ -16490,7 +16508,7 @@
16490
16508
  "surface": "builders-hall",
16491
16509
  "text": "submit for review",
16492
16510
  "file": "modules/hall-ui/public/modules.js",
16493
- "line": 142,
16511
+ "line": 149,
16494
16512
  "origin": "js-markup",
16495
16513
  "confidence": "certain"
16496
16514
  },
@@ -17354,7 +17372,7 @@
17354
17372
  "surface": "builders-hall",
17355
17373
  "text": "{…} and above can enable or disable a module",
17356
17374
  "file": "modules/hall-ui/public/modules.js",
17357
- "line": 140,
17375
+ "line": 147,
17358
17376
  "origin": "js-markup",
17359
17377
  "confidence": "certain"
17360
17378
  },
@@ -17705,7 +17723,7 @@
17705
17723
  "surface": "builders-hall",
17706
17724
  "text": "{…} {…} — how-to",
17707
17725
  "file": "modules/hall-ui/public/modules.js",
17708
- "line": 383,
17726
+ "line": 390,
17709
17727
  "origin": "js-text-assign",
17710
17728
  "confidence": "certain"
17711
17729
  },
@@ -17777,7 +17795,7 @@
17777
17795
  "surface": "builders-hall",
17778
17796
  "text": "{…} — how to use it",
17779
17797
  "file": "modules/hall-ui/public/modules.js",
17780
- "line": 385,
17798
+ "line": 392,
17781
17799
  "origin": "js-text-assign",
17782
17800
  "confidence": "certain"
17783
17801
  },
@@ -17912,7 +17930,7 @@
17912
17930
  "surface": "builders-hall",
17913
17931
  "text": "· version",
17914
17932
  "file": "modules/hall-ui/public/modules.js",
17915
- "line": 388,
17933
+ "line": 395,
17916
17934
  "origin": "js-markup",
17917
17935
  "confidence": "certain"
17918
17936
  },
@@ -0,0 +1,69 @@
1
+ # Craft mode looks — the design direction (task 1004494, goal 1000095 WA6)
2
+
3
+ > **Input:** the owner's four-mode prototype, <https://claude.ai/artifact/<redacted>> (boards Building, Artistry, Ideating, Governing). On 2026-10-03 the owner named that prototype **the standardized design** this direction takes from. Built by task 1004495; the mode tools are tasks 1004496 to 1004498.
4
+
5
+ Each craft mode should feel like the place that craft works, not like a recolour. Each mode gets one colour, one header mark and one signature element, and all of them sit inside the live hall.
6
+
7
+ ## The blend rule (what does not change)
8
+
9
+ This direction follows the blend rule in [`project-startup-direction.md`](project-startup-direction.md#<redacted>). A mode is something the existing hall wears, never a second hall:
10
+
11
+ - **The shell stays.** That means the v3 glass hall, the 64px rail that opens on hover, the topbar, the nav groups, the panels, both light and dark themes, and every page's content.
12
+ - **The accent stays the project's action colour.** Claim buttons, links and the lit windows keep `--accent` (salmon) in every mode, as the prototype does. The mode colour marks *where you are*, never *what to press*.
13
+ - **Building and Legacy wear no look.** Building is the hall as today (the prototype's Building board reads "no mode tools"); Legacy is the hall as it was. Only Artistry, Ideating and Governing change anything.
14
+ - **Where the prototype's chrome differs from the live hall, the live hall wins.** The prototype's 232px open rail, its own header layout, and rooms that do not exist yet (Store, Treasury, Charter, Page reviews) are illustration. The tabs each mode shows are settled by task 1004493 (`MODE_TABS` in `craft-mode.js`).
15
+ - **Collab, Builders and Profile show in every mode** (owner, 2026-10-03). The always-present list widens from Builders alone to Builders, Collab and Profile, so every mode's sidebar keeps the You and Community groups. Built in task 1004495.
16
+
17
+ ## The three looks
18
+
19
+ Every value is a token on `html[data-craft="<mode>"]`, with a dark twin on `html[data-theme="dark"][data-craft="<mode>"]`. The dark values are the prototype's. The prototype is dark only, so the light values are derived from it and checked for contrast: each mode colour reads at least 5.4:1 on the hall's light ground, and its pill text at least 5.8:1.
20
+
21
+ | | Artistry — *the studio · night* | Ideating — *the sky* | Governing — *the city* |
22
+ |---|---|---|---|
23
+ | `--craft` dark | `#f1e6d2` moonlight | `#f2d27a` star gold | `#aabeff` city blue |
24
+ | `--craft` light | `#2e3c6e` night ink | `#7a5a0c` deep gold | `#33479e` council blue |
25
+ | `--craft-on` (pill text) dark / light | `#0c1122` / `#f1e6d2` | `#1d1604` / `#fdf6dd` | `#0a1030` / `#eef1ff` |
26
+ | ground, dark only | `--bg #07080d`, `--bg-frame #0a0b12`, glass tinted night blue | `--bg #05060d`, glass tinted deep space | unchanged black: the city is drawn on the hall's own black |
27
+ | glass edge, dark only | moonlight at 16% | gold at 16% | city blue at 16% |
28
+
29
+ Light mode keeps the hall's paper ground in every mode. A night-blue page in light mode would be a second theme, and the hall has two already.
30
+
31
+ ### Shared treatment, all three modes
32
+
33
+ 1. **The mode switch.** The current mode's pill fills with `--craft`, with `--craft-on` text, as on every prototype board. Today it is grey glass in every mode.
34
+ 2. **The current tab.** Its tint, icon, label and the 2px marker take `--craft` instead of salmon and grey glass.
35
+ 3. **The header mark.** The mode's name sits beside the page title as a small uppercase eyebrow in `--craft`, using the prototype's own words: "the studio · night", "the sky", "the city". At phone width it is hidden, because the switch's single pill already says the mode and the topbar has no room.
36
+ 4. **The topbar's hairline** takes `--craft` at 30%, so the whole top edge reads as the mode.
37
+
38
+ ### The signature element: the door's scene, framed in the rail
39
+
40
+ The mode picker (task 1004492) already draws each mode's scene on its door: the night room, the sky of ideas and the governor city. **Each mode puts its own door's scene at the top of the rail**, as a small framed picture under the brand. It is 40px tall while the rail is closed and 64px when it opens. This is the owner's "carry the lo-fi elements into the nav bar so the theme is obvious". It reuses art the hall already ships, so there is one scene per mode and never two versions that drift apart.
41
+
42
+ - **Artistry:** the night room, with stars, the moon and houses whose windows are lit in the accent colour. Its source is the prototype's Artistry rail plate.
43
+ - **Ideating:** the sky of ideas, with gold stars and protostar rings. Ideating also scatters faint star dust across the rail itself, after the sky in the prototype's Ideating board and the ideator surface ([ADR 0271](../adr/0271-the-ideator-surface-is-two-halves.md), [`ideator-surface-direction.md`](ideator-surface-direction.md)).
44
+ - **Governing:** the isometric governor city, after the prototype's Governing board and the Board Room pages.
45
+
46
+ **The plate follows the theme** (owner, 2026-10-03). Dark mode shows each scene as the picker draws it; light mode shows its day version:
47
+
48
+ | Scene | Light-mode version |
49
+ |---|---|
50
+ | Artistry | dawn sky `#dfe3ef`, stars in night ink `#2e3c6e`, a warm low sun `#f3c98b` in the moon's place, ground `#c3c9dc`, houses `#8d97b8`, windows still lit in the accent |
51
+ | Ideating | cream sky `#f6f0dc`, stars in deep gold `#c9a23a` / `#b8891c`, protostar and well rings in `#7a5a0c` |
52
+ | Governing | pale ground `#e9ecf6`, the city's faces flipped to light greys with the glass kept blue |
53
+
54
+ It is decorative (`aria-hidden`) and drawn from fixed positions, so every visit shows the same picture. It does not animate, and there is no second reduced-motion block.
55
+
56
+ ### Left for the build, and the tools tasks
57
+
58
+ - **Governing's header tallies** ("needs you 3 · moving 12" on the prototype) need a live count. "Needs you" is the open-blocker count the nav badge mechanism already carries; "moving" has no source yet. The build may ship the plate and colour first and file the tallies as their own task rather than inventing a number.
59
+ - The palette, paintbrush and lightbulb buttons the Artistry and Ideating boards show in the topbar are tasks 1004496 to 1004498, not this look.
60
+
61
+ ## Renders
62
+
63
+ The renders show the live hall before (today, with each mode's nav from task 1004493) and after this direction is applied through a scratch stylesheet. Each mode is shown on its own room: Studio, Your thinking and Blockers. Every mode was rendered in light and dark, at desktop (1440) and phone (390) widths, and no render has horizontal overflow. The screenshots are reviewed by the owner and not committed: the hall's evidence rule keeps PNGs out of the repo (`modules/hall-ui/CLAUDE.md`, Evidence matrix).
64
+
65
+ ## Provenance
66
+
67
+ - Owner ask, task 1004494: unique per craft, not a recolour; Artistry carries the lo-fi elements into the nav; Ideating draws on the ideator surface; Governing on the Board Room and government pages; Building stays as today.
68
+ - Owner decision, 2026-10-03: the four-mode prototype is the standardized design.
69
+ - Owner review of the first renders, 2026-10-03: the scene plate follows light and dark; Collab, Builders and Profile stay in every mode's sidebar.
@@ -2891,5 +2891,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2891
2891
  landed since 1.21.30 with no explicit bump. run 37124963833. (task 1002620)
2892
2892
  1.21.32 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2893
2893
  landed since 1.21.31 with no explicit bump. run 37150508709. (task 1002620)
2894
+ 1.21.33 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2895
+ landed since 1.21.32 with no explicit bump. run 37152823234. (task 1002620)
2894
2896
  ---------------------------------------------------------------------------
2895
2897
  ```
@@ -1909,7 +1909,7 @@
1909
1909
  "surface": "builders",
1910
1910
  "title": "Modules",
1911
1911
  "states_read": ["browse","search-empty","lens","details","out"],
1912
- "files_hash": "87dc9c1fbfb1be20",
1912
+ "files_hash": "735baf73dba8b0e1",
1913
1913
  "reading_hash": "80813af9d392105c",
1914
1914
  "counts": {"lines":52,"placed":20,"shared":10,"unplaced":22},
1915
1915
  "omitted": {"data":74},
@@ -1937,7 +1937,7 @@
1937
1937
  {"key":"L0020","section":"Module catalog","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"lens","occurrences":3},
1938
1938
  {"key":"L0021","section":"Module catalog","text":"modules","placement":"unplaced"},
1939
1939
  {"key":"L0022","section":"Module catalog","text":"16 from the {…}","placement":"unplaced"},
1940
- {"key":"L0023","section":"Module catalog","text":"of {…}","placement":"placed","file":"modules/hall-ui/public/roadmap.js","line":288,"string_id":"5e7a2220c6a9"},
1940
+ {"key":"L0023","section":"Module catalog","text":"of {…}","placement":"placed","file":"modules/hall-ui/public/roadmap.js","line":341,"string_id":"5e7a2220c6a9"},
1941
1941
  {"key":"L0024","section":"Module catalog","text":"built here","placement":"placed","file":"modules/hall-ui/public/modules.js","line":53,"string_id":"a87a3c30b1b5","occurrences":17},
1942
1942
  {"key":"L0025","section":"Module catalog","text":"can go upstream","placement":"unplaced"},
1943
1943
  {"key":"L0026","section":"Module catalog","text":"no upkeep","placement":"unplaced"},
@@ -1953,7 +1953,7 @@
1953
1953
  {"key":"L0036","section":"Module catalog","text":"{…} file{…}","placement":"placed","file":"modules/hall-ui/public/gate.js","line":151,"string_id":"3efa67dc6e07"},
1954
1954
  {"key":"L0037","section":"Module catalog","text":"sessions","placement":"placed","file":"modules/hall-ui/public/task.js","line":467,"string_id":"732b4c935e36"},
1955
1955
  {"key":"L0038","section":"Module catalog","text":"Built by this project","placement":"placed","file":"modules/hall-ui/public/modules.html","line":70,"string_id":"b51edc826cbe"},
1956
- {"key":"L0039","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":142,"string_id":"0215b2cdca8c"},
1956
+ {"key":"L0039","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":149,"string_id":"0215b2cdca8c"},
1957
1957
  {"key":"L0040","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"},
1958
1958
  {"key":"L0041","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":471,"string_id":"1883ad7f07fc"},
1959
1959
  {"key":"L0042","section":"Module catalog","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":249,"string_id":"fd1903586e8e"},
@@ -228,6 +228,9 @@ const PERMISSIONS = [
228
228
  // Re-run a store version's automatic assessment (task 1003796, ADR 0359). Never a
229
229
  // hand-set score: the owner ruled a person may ask for a re-check, not change the number.
230
230
  { key: 'module.assessment.rerun', system: false, floor: 'metic', guards: 'POST /store/modules/:key/versions/:version/reassess' },
231
+ // Take a module out of the store (task 1003797, ADR 0338 D2). Stops new installs,
232
+ // versions and downloads; never reaches an instance that already holds it.
233
+ { key: 'module.delist', system: false, floor: 'metic', guards: 'POST /store/modules/:key/delist' },
231
234
  { key: 'security.report.file', system: false, floor: 'metic', guards: 'POST /security/reports, GET /security/adr' },
232
235
  { key: 'llm_cache.use', system: false, floor: 'metic', guards: 'POST /llm-cache/{lookup,store}' },
233
236
  { key: 'backup.manage', system: false, floor: 'metic', guards: 'GET /backup/status, POST /backup/trigger' },
@@ -0,0 +1,36 @@
1
+ -- government_025_module_delist.sql — grant the new `module.delist` atom to Metic
2
+ -- and Archon (task 1003797, ADR 0338 D2).
3
+ --
4
+ -- WHAT IT IS. A Metic+ person may take a module out of the store. It leaves the
5
+ -- catalog and can no longer be newly installed, updated or downloaded. Instances
6
+ -- that already hold it keep it, and it keeps working: a delist never revokes an
7
+ -- entitlement or reaches into an installed copy (ADR 0338 D2), and it never
8
+ -- changes a score (ADR 0359). A reason is required and the audit log records it.
9
+ --
10
+ -- WHY THIS FILE EXISTS. RANK_SEED is derived from the catalog's floors, and the
11
+ -- drift guard (tests/government_seed.mjs) compares it against the union of the
12
+ -- run-once seed plus every later grant migration, per rank. A metic-floor key is
13
+ -- held by metic AND archon, so both are granted here.
14
+ --
15
+ -- Additive only: one new permission key granted to two ranks; nothing is revoked.
16
+ -- Approved by the owner (Will, Archon) on 2026-10-01.
17
+ -- Idempotent (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
18
+ --
19
+ -- The `SELECT '<rank>', unnest(ARRAY[…])` shape is load-bearing: the drift guard
20
+ -- scans for it literally.
21
+
22
+ BEGIN;
23
+
24
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
25
+ SELECT 'metic', unnest(ARRAY[
26
+ 'module.delist'
27
+ ])
28
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
29
+
30
+ INSERT INTO government_rank_permissions (rank_key, permission_key)
31
+ SELECT 'archon', unnest(ARRAY[
32
+ 'module.delist'
33
+ ])
34
+ ON CONFLICT (rank_key, permission_key) DO NOTHING;
35
+
36
+ COMMIT;
@@ -106,6 +106,13 @@
106
106
  return `<a class="ov-fact mod-howto" href="${escapeHtml(href)}">how-to</a>`;
107
107
  }
108
108
 
109
+ // A store module this instance holds that the store has since delisted (task
110
+ // 1003797, ADR 0338 D2): it keeps working here, but no more versions will come.
111
+ function delistedFact(m) {
112
+ if (m.store_delisted !== true) return '';
113
+ return '<span class="fact-pill fact-pill--warn" title="Taken out of the store. Your copy keeps working, but it will get no more updates.">delisted · no more updates</span>';
114
+ }
115
+
109
116
  // The lines the row's details disclosure opens onto: the facts the payload
110
117
  // carries that the card never rendered — what the module contributes, the
111
118
  // core it needs, who maintains it and what it hands off to.
@@ -152,7 +159,7 @@
152
159
  <div class="ov-row__main">
153
160
  <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
154
161
  ${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
155
- <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}</div>
162
+ <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}</div>
156
163
  ${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
157
164
  </div>
158
165
  <div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.32",
3
+ "version": "1.21.33",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.32",
9
+ "version": "1.21.33",
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.32",
3
+ "version": "1.21.33",
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",
@@ -8948,5 +8948,15 @@
8948
8948
  "id": "1004551",
8949
8949
  "text": "An Archon can star a version on the Roadmap as the project's focus; it persists across reloads, and everyone else sees the star read-only (the server refuses non-Archons)."
8950
8950
  }
8951
+ ],
8952
+ "1.21.33": [
8953
+ {
8954
+ "id": "1003797",
8955
+ "text": "Senior builders can now take a module out of the store, with a reason. Nobody can newly install or update it, but anyone who already has it keeps it working and is told no more updates will come."
8956
+ },
8957
+ {
8958
+ "id": "1004494",
8959
+ "text": "The look for each craft mode is decided. Artistry, Ideating and Governing each get their own colour (moonlight, star gold, city blue) on the mode switch, the tab you are on and the top bar, the mode's name beside the page titl"
8960
+ }
8951
8961
  ]
8952
8962
  }
@@ -433,7 +433,7 @@ function clip(s, n) {
433
433
 
434
434
  // Pure renderer — every input arrives as a parameter (the tests pass catalog
435
435
  // rows straight in), so it touches no disk and no config.
436
- function formatCatalog(rows, { catalogOnly = false, query = '', core = null } = {}) {
436
+ function formatCatalog(rows, { catalogOnly = false, query = '', core = null, delisted = new Set() } = {}) {
437
437
  const scoped = catalogOnly ? catalog.catalogRows(rows) : rows;
438
438
  const shown = catalog.searchCatalog(scoped, query);
439
439
  const counts = catalog.summarize(rows);
@@ -458,11 +458,14 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
458
458
  // An inferred credit is marked so it never reads as something the author
459
459
  // typed — the catalog's whole job is honest attribution (ADR 0107 §2).
460
460
  const suffix = r.credit.inferred ? '*' : '';
461
- const upkeep = (r.maintenance && r.maintenance.status) || 'undeclared';
461
+ const upkeep = ((r.maintenance && r.maintenance.status) || 'undeclared') + (delisted.has(r.key) ? ' · DELISTED' : '');
462
462
  out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
463
463
  }
464
464
  out.push('');
465
465
  out.push(' [x] enabled here · [ ] available, not enabled');
466
+ if (shown.some((r) => delisted.has(r.key))) {
467
+ out.push(' DELISTED = taken out of the store: your copy keeps working, but no more versions will come.');
468
+ }
466
469
  if (shown.some((r) => r.credit.inferred)) {
467
470
  out.push(` * credit not declared in module.json — shown as the platform (${catalog.CORE_PROJECT_NAME}), which ships it.`);
468
471
  }
@@ -470,7 +473,18 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
470
473
  return out.join('\n');
471
474
  }
472
475
 
473
- function cmdList(args, { log = console.log, errlog = console.error, gather = gatherCatalog } = {}) {
476
+ // Which installed store modules the store has delisted (task 1003797). Only modules
477
+ // built into this instance's tree can be store installs, so a core checkout asks
478
+ // nothing. Fail-soft like the upgrade check: offline, or no session, lists as before.
479
+ async function gatherDelisted(rows, { checkStore = gatherUpdates } = {}) {
480
+ const held = rows.filter((r) => r.source === 'instance');
481
+ if (!held.length) return new Set();
482
+ const found = await checkStore(held);
483
+ if (!found.checked) return new Set();
484
+ return new Set(Object.keys(found.latest).filter((k) => found.latest[k] && found.latest[k].status === 'delisted'));
485
+ }
486
+
487
+ async function cmdList(args, { log = console.log, errlog = console.error, gather = gatherCatalog, checkStore = gatherUpdates } = {}) {
474
488
  const catalogOnly = args.includes('--catalog');
475
489
  const json = args.includes('--json');
476
490
  const sIdx = args.indexOf('--search');
@@ -484,14 +498,16 @@ function cmdList(args, { log = console.log, errlog = console.error, gather = gat
484
498
  try { data = gather(); }
485
499
  catch (e) { errlog(`bongos module list: ${e.message}`); return 1; }
486
500
 
501
+ const delisted = await gatherDelisted(data.rows, { checkStore });
502
+
487
503
  if (json) {
488
504
  const scoped = catalogOnly ? catalog.catalogRows(data.rows) : data.rows;
489
- const modules = catalog.searchCatalog(scoped, query);
505
+ const modules = catalog.searchCatalog(scoped, query).map((m) => ({ ...m, store_delisted: delisted.has(m.key) }));
490
506
  log(JSON.stringify({ core: data.core, summary: catalog.summarize(data.rows), modules }, null, 2));
491
507
  return 0;
492
508
  }
493
509
 
494
- log(formatCatalog(data.rows, { catalogOnly, query, core: data.core }));
510
+ log(formatCatalog(data.rows, { catalogOnly, query, core: data.core, delisted }));
495
511
  for (const s of data.skipped) {
496
512
  errlog(` ! ${s.key}: not listed — ${s.reason}`);
497
513
  }
@@ -232,6 +232,36 @@ async function latestVersions(keys, { db } = {}) {
232
232
  return out;
233
233
  }
234
234
 
235
+ // Delist a module (task 1003797, ADR 0338 D2): it leaves the catalog, takes no new
236
+ // acquisitions or versions, and serves no further downloads — every read above
237
+ // already refuses on status 'delisted', so this one UPDATE is the whole act. It
238
+ // touches nothing else: no entitlement is revoked, no installed copy is reached,
239
+ // and no score row is written (a delist is not a score, ADR 0359). One-way here;
240
+ // there is no relist route yet.
241
+ // Returns { ok: true, module } or { ok: false, status, code, message }.
242
+ async function delistModule(key, { db } = {}) {
243
+ const pool = db || require('./pool').pool;
244
+ const { rows: [mod] } = await pool.query(
245
+ `UPDATE store_modules SET status = 'delisted', delisted_at = now()
246
+ WHERE module_key = $1 AND status = 'listed'
247
+ RETURNING module_key, title, status, delisted_at`, [key]);
248
+ if (mod) return { ok: true, module: mod };
249
+ const { rows: [have] } = await pool.query(
250
+ 'SELECT module_key, status, delisted_at FROM store_modules WHERE module_key = $1', [key]);
251
+ if (!have) return { ok: false, status: 404, code: 'module_not_found', message: `The store has no module "${key}".` };
252
+ return { ok: false, status: 409, code: 'already_delisted', message: `"${key}" is already delisted.` };
253
+ }
254
+
255
+ // Which of these module keys the store has delisted, as a Set — so the hall's
256
+ // Modules tab can mark an installed store module that will get no more versions.
257
+ async function delistedKeySet(keys, { db } = {}) {
258
+ if (!keys.length) return new Set();
259
+ const pool = db || require('./pool').pool;
260
+ const { rows } = await pool.query(
261
+ "SELECT module_key FROM store_modules WHERE module_key = ANY($1) AND status = 'delisted'", [keys]);
262
+ return new Set(rows.map((r) => r.module_key));
263
+ }
264
+
235
265
  // The tarball on disk for a version row. artifact_path is relative to the store dir
236
266
  // (relativeArtifactPath), and artifactPath() re-validates key + version, so a row
237
267
  // can never point a read outside the store.
@@ -246,4 +276,5 @@ function versionArtifactFile(row, { dir = storeDir() } = {}) {
246
276
  module.exports = {
247
277
  stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
248
278
  isFreePrice, resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
279
+ delistModule, delistedKeySet,
249
280
  };
@@ -240,6 +240,9 @@ const EXPECTED_RANKS = {
240
240
  // Re-running a store version's assessment (task 1003796, ADR 0359) spends a Docs
241
241
  // grader call and re-opens the Security gate, so it is pinned to its own atom.
242
242
  'POST /store/modules/:key/versions/:version/reassess': 'perm:module.assessment.rerun',
243
+ // Delisting a store module (task 1003797, ADR 0338 D2) stops every new install of
244
+ // it for every instance, so it is pinned to its own atom.
245
+ 'POST /store/modules/:key/delist': 'perm:module.delist',
243
246
  };
244
247
 
245
248
  // The authz-core route pins (ADR 0174). Paths and the gating atom were renamed
@@ -35,6 +35,9 @@
35
35
  // GET /store/modules/:key/versions/:version/howto
36
36
  // the HOWTO.md that version shipped with, as text for
37
37
  // the hall's how-to page (task 1004366, ADR 0347).
38
+ // POST /store/modules/:key/delist
39
+ // Metic+ takes a module out of the store; holders
40
+ // keep it (task 1003797, ADR 0338 D2).
38
41
  //
39
42
  // Read is open to any signed-in builder (same gate as the atlas/primer pages);
40
43
  // every write (enable/disable, submit) is metic+archon, same rank as
@@ -85,6 +88,7 @@ const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } =
85
88
  const {
86
89
  stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
87
90
  resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
91
+ delistModule, delistedKeySet,
88
92
  } = require('../module-store');
89
93
  const { KEY_RE } = require('../../module-loader/manifest-schema');
90
94
  const { parseVersion } = require('../../module-loader/semver');
@@ -204,6 +208,11 @@ module.exports = function buildModulesRouter() {
204
208
  let published = new Set();
205
209
  try { published = await publishedVersionSet(all.map((m) => m.key)); }
206
210
  catch (e) { log.warn({ err: e.message }, 'store versions unreadable for the modules list'); }
211
+ // Which installed store modules the store has delisted (task 1003797), so the row
212
+ // says it keeps working but gets no more versions (ADR 0338 D2). Same tolerance.
213
+ let delistedKeys = new Set();
214
+ try { delistedKeys = await delistedKeySet(all.filter((m) => m.source === 'instance').map((m) => m.key)); }
215
+ catch (e) { log.warn({ err: e.message }, 'store status unreadable for the modules list'); }
207
216
  for (const m of all) {
208
217
  m.enabled_source = sources[m.key] || 'default';
209
218
  m.override = byKey.has(m.key)
@@ -214,6 +223,7 @@ module.exports = function buildModulesRouter() {
214
223
  m.store_howto = m.source === 'instance' && m.version && published.has(`${m.key}@${m.version}`)
215
224
  ? `/builders/modules?howto=${encodeURIComponent(m.key)}&version=${encodeURIComponent(String(m.version))}`
216
225
  : null;
226
+ m.store_delisted = m.source === 'instance' && delistedKeys.has(m.key);
217
227
  }
218
228
  res.json({
219
229
  modules: all,
@@ -446,6 +456,31 @@ module.exports = function buildModulesRouter() {
446
456
  res.status(202).json({ ok: true, queued: true, module_key: key, version, version_id: r.version.id });
447
457
  }));
448
458
 
459
+ // POST /api/bongos/store/modules/:key/delist — a Metic+ person takes a module out
460
+ // of the store (task 1003797, ADR 0338 D2). It leaves the catalog, cannot be newly
461
+ // acquired, takes no new versions and serves no further downloads: every store
462
+ // read already refuses a delisted key, so this flips the one status. What it never
463
+ // does: revoke an entitlement, reach into an instance that already holds the
464
+ // module (that copy keeps working; `bongos upgrade` and the hall tell the holder
465
+ // no more versions will come), or touch a score (ADR 0359). A reason is required;
466
+ // the audit middleware records it with who asked. There is no relist route yet.
467
+ router.post('/store/modules/:key/delist', auth.requireBuilder, auth.requirePermission('module.delist'),
468
+ asyncHandler('POST /store/modules/:key/delist', async (req, res) => {
469
+ if (validateOrRespond(req, res, { reason: { type: 'string', maxLength: 2000 } })) return;
470
+ const { key } = req.params;
471
+ if (!KEY_RE.test(key)) {
472
+ return res.fail('bad_module_key', { status: 400, message: 'A module key is lowercase kebab-case.' });
473
+ }
474
+ const reason = String(req.body?.reason || '').trim();
475
+ if (!reason) {
476
+ return res.fail('reason_required', { status: 400, message: 'Say why this module is leaving the store, in reason. People who already hold it keep it.' });
477
+ }
478
+ const r = await delistModule(key);
479
+ if (!r.ok) return res.fail(r.code, { status: r.status, message: r.message });
480
+ log.info({ key, builderId: req.builder.id }, `${key} delisted from the store`);
481
+ res.json({ ok: true, module_key: key, status: r.module.status, delisted_at: r.module.delisted_at });
482
+ }));
483
+
449
484
  // GET /api/bongos/store/entitlements and /store/modules/:key/entitlement — the
450
485
  // read side of the entitlement record (task 1003813, ADR 0338 D1). Own-scoped
451
486
  // like /me/sessions: the holder is ALWAYS the caller (holder_kind 'builder',
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.32'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.33'; // 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');
@@ -58,7 +58,7 @@ test('modules: the row is the oversight row — title with the mono key, clamped
58
58
  assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
59
  assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
60
  assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
- assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}<\/div>/, 'provenance, credit, upkeep and (for a store version, task 1004366) the how-to link are the facts line');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}\$\{howtoFact\(m\)\}\$\{delistedFact\(m\)\}<\/div>/, 'provenance, credit, upkeep, (for a store version, task 1004366) the how-to link and (task 1003797) the delisted pill are the facts line');
62
62
  assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
63
  const css = read('modules.css');
64
64
  assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
@@ -225,10 +225,11 @@ test('formatCatalog: a search with no hits says so instead of printing an empty
225
225
  assert.ok(!/KEY\s+SOURCE/.test(out), 'no header row over an empty result');
226
226
  });
227
227
 
228
- test('cmdList: --json emits the rows + summary, and honours --catalog/--search', () => {
228
+ test('cmdList: --json emits the rows + summary, and honours --catalog/--search', async () => {
229
229
  const lines = [];
230
- const code = mod.cmdList(['--json', '--catalog', '--search', 'example world'], {
230
+ const code = await mod.cmdList(['--json', '--catalog', '--search', 'example world'], {
231
231
  log: (s) => lines.push(s),
232
+ checkStore: async () => ({ checked: false, reason: 'offline' }),
232
233
  gather: () => ({ rows: sample(), skipped: [], errors: [], core: '1.19.0' }),
233
234
  });
234
235
  assert.equal(code, 0);
@@ -239,17 +240,18 @@ test('cmdList: --json emits the rows + summary, and honours --catalog/--search',
239
240
  assert.deepEqual(payload.modules.map((m) => m.key), ['provisioning']);
240
241
  });
241
242
 
242
- test('cmdList: --search with no value is a usage error, not a silent empty listing', () => {
243
+ test('cmdList: --search with no value is a usage error, not a silent empty listing', async () => {
243
244
  const errs = [];
244
- const code = mod.cmdList(['--search'], { log: () => {}, errlog: (s) => errs.push(s), gather: () => { throw new Error('must not gather'); } });
245
+ const code = await mod.cmdList(['--search'], { log: () => {}, errlog: (s) => errs.push(s), gather: () => { throw new Error('must not gather'); } });
245
246
  assert.equal(code, 2);
246
247
  assert.match(errs.join('\n'), /usage: bongos module list/);
247
248
  });
248
249
 
249
- test('cmdList: a coreVersion-skipped module is surfaced on stderr, not silently dropped', () => {
250
+ test('cmdList: a coreVersion-skipped module is surfaced on stderr, not silently dropped', async () => {
250
251
  const errs = [];
251
- const code = mod.cmdList([], {
252
+ const code = await mod.cmdList([], {
252
253
  log: () => {},
254
+ checkStore: async () => ({ checked: false, reason: 'offline' }),
253
255
  errlog: (s) => errs.push(s),
254
256
  gather: () => ({ rows: sample(), skipped: [{ key: 'rotted', reason: 'coreVersion "^0.1.0" not satisfied by core 1.19.0' }], errors: [], core: '1.19.0' }),
255
257
  });
@@ -257,6 +259,32 @@ test('cmdList: a coreVersion-skipped module is surfaced on stderr, not silently
257
259
  assert.match(errs.join('\n'), /rotted: not listed — coreVersion/);
258
260
  });
259
261
 
262
+ test('cmdList: an installed store module the store delisted is marked, and only instance modules are asked about (task 1003797)', async () => {
263
+ const lines = [];
264
+ const asked = [];
265
+ const code = await mod.cmdList([], {
266
+ log: (s) => lines.push(s),
267
+ gather: () => ({ rows: sample(), skipped: [], errors: [], core: '1.19.0' }),
268
+ checkStore: async (rows) => {
269
+ asked.push(...rows.map((r) => r.key));
270
+ return { checked: true, latest: Object.fromEntries(rows.map((r) => [r.key, { version: '9.9.9', status: 'delisted' }])) };
271
+ },
272
+ });
273
+ assert.equal(code, 0);
274
+ const instanceKeys = sample().filter((r) => r.source === 'instance').map((r) => r.key);
275
+ assert.deepEqual(asked, instanceKeys, 'a core module is never a store install, so it is not asked about');
276
+ const out = lines.join('\n');
277
+ assert.match(out, /DELISTED/);
278
+ assert.match(out, /your copy keeps working, but no more versions will come/);
279
+ const json = [];
280
+ await mod.cmdList(['--json'], {
281
+ log: (s) => json.push(s),
282
+ gather: () => ({ rows: sample(), skipped: [], errors: [], core: '1.19.0' }),
283
+ checkStore: async () => ({ checked: false, reason: 'offline' }),
284
+ });
285
+ assert.ok(JSON.parse(json.join('\n')).modules.every((m) => m.store_delisted === false), 'offline: nothing is marked');
286
+ });
287
+
260
288
  // ---------------------------------------------------------------------------
261
289
  // The live tree — the catalog must actually describe THIS checkout
262
290
  // ---------------------------------------------------------------------------