@bongos/core 1.24.30 → 1.24.32
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 +153 -103
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +139 -1
- package/docs/api-reference.md +7 -3
- package/docs/copy-inventory.md +192 -187
- package/docs/copy-registry.json +530 -458
- package/docs/module-api-changelog.md +4 -2
- package/docs/page-inventory.json +15 -35
- package/docs/page-readings.json +228 -265
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +62 -86
- package/modules/hall-ui/public/home-city-draw.js +360 -52
- package/modules/hall-ui/public/home-city.css +47 -11
- package/modules/hall-ui/public/home-city.js +170 -40
- package/modules/hall-ui/public/home-gallery.css +129 -0
- package/modules/hall-ui/public/home-gallery.js +349 -0
- package/modules/hall-ui/public/home-studio.css +72 -0
- package/modules/hall-ui/public/home-studio.js +203 -0
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +4 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.js +6 -1
- package/modules/hall-ui/public/market-watch.html +141 -54
- package/modules/hall-ui/public/market-watch.js +54 -0
- package/modules/hall-ui/public/market-watch.states.json +2 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +2 -3
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +9 -1
- package/modules/hall-ui/public/style.css +74 -27
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/home-city.md +133 -0
- package/modules/hall-ui/records/loader.md +1 -1
- package/modules/hall-ui/records/market-watch-settings.md +16 -3
- package/modules/hall-ui/records/market-watch.md +3 -1
- package/modules/lifecycle/home-city.js +58 -10
- package/modules/lifecycle/home-gallery.js +320 -0
- package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/home-gallery.js +37 -0
- package/modules/market-watch/routes/settings.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +35 -1
- package/scripts/bongos/leak-scan-allowlist.js +8 -0
- package/scripts/hall-preview/README.md +1 -0
- package/scripts/hall-preview/server.js +10 -0
- package/src/bongos/serve-internal.js +17 -3
- package/src/module-api.js +1 -1
- package/tests/hall_builders_page_pass.mjs +18 -27
- package/tests/hall_loading.mjs +1 -1
- package/tests/hall_nav.mjs +11 -4
- package/tests/hall_page_gate_map.mjs +7 -4
- package/tests/home_city.mjs +200 -5
- package/tests/home_city_widget.mjs +187 -27
- package/tests/home_gallery.mjs +255 -0
- package/tests/home_gallery_widget.mjs +220 -0
- package/tests/home_studio.mjs +169 -0
- package/tests/home_studio_widget.mjs +157 -0
- package/tests/market_watch_pages.mjs +162 -0
- package/tests/nav_permission_atoms.mjs +2 -2
- package/modules/hall-ui/public/market-watch-settings.html +0 -115
- package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/home-gallery.js — the works behind the artist's Home, and the
|
|
4
|
+
// one heart a builder may give each (task 1005060, Home v16 step 3).
|
|
5
|
+
//
|
|
6
|
+
// WHY. In Artistry, Home is a bench in the museum facing a wall of the project's
|
|
7
|
+
// signed works (modules/hall-ui/public/home-gallery.js, record
|
|
8
|
+
// modules/hall-ui/records/home-city.md § "The gallery"). The owner's rule: "not
|
|
9
|
+
// about quality but about appreciation" — each work carries how many builders
|
|
10
|
+
// appreciated it and one heart to add yours; Most popular (the default) is that
|
|
11
|
+
// count, Latest the ship date. No grade, rank or leaderboard reads or writes here.
|
|
12
|
+
//
|
|
13
|
+
// WHAT A WORK IS, and why this source. A work is a SHIPPED task of the art crafts
|
|
14
|
+
// (discipline `artist` or `ui` — economy's credits-by-craft rule, the city's
|
|
15
|
+
// ART_DISCIPLINES) that carries a picture of what it made:
|
|
16
|
+
// · a page tweak round (`source = 'page-tweak'`) hangs its own AFTER renders, the
|
|
17
|
+
// desktop light and dark slots /tweak attached for the Approval queue
|
|
18
|
+
// (lifecycle_task_visual_slots, TWEAK_RENDER_SLOTS) — the artist's approved page;
|
|
19
|
+
// · any other art ship hangs its ship-time visual (tasks.visual_url).
|
|
20
|
+
// That is the hall's own record of finished, signed art: the artist is the task's
|
|
21
|
+
// `shipped_by` (a page tweak's is its artist, page-tweak-artist.js), the date its
|
|
22
|
+
// ship, the line under it its value summary. The design mocks under docs/ are drafts
|
|
23
|
+
// and the page readings are words with no picture, so neither is a work.
|
|
24
|
+
//
|
|
25
|
+
// THE PICTURE INHERITS THE TASK'S AUDIENCE (the task-visuals rule): a
|
|
26
|
+
// security-sensitive task never hangs here, and a work under a private goal the
|
|
27
|
+
// viewer is neither a member of nor an Archon over is left off the wall entirely
|
|
28
|
+
// (ADR 0112) — its picture is the prose. Visibility comes from the signed-in
|
|
29
|
+
// viewer, never the query.
|
|
30
|
+
//
|
|
31
|
+
// BOUNDED. One query, LIMIT MAX_WORKS, ordered in SQL by the order asked for, with
|
|
32
|
+
// the whole wall's size as a window count — the payload never grows with the corpus.
|
|
33
|
+
//
|
|
34
|
+
// MODULE BOUNDARY: reaches core only through the published doorway. The hearts are
|
|
35
|
+
// this module's own table (lifecycle_020); lifecycle is default-on, but the table is
|
|
36
|
+
// still probed with to_regclass so a database the migration has not reached answers
|
|
37
|
+
// a wall with no hearts rather than failing Home.
|
|
38
|
+
|
|
39
|
+
const fs = require('node:fs');
|
|
40
|
+
const api = require('../../src/module-api');
|
|
41
|
+
const taskVisuals = require('./task-visuals');
|
|
42
|
+
|
|
43
|
+
const MAX_WORKS = 60;
|
|
44
|
+
const ORDERS = Object.freeze(['popular', 'latest']);
|
|
45
|
+
const ART_DISCIPLINES = Object.freeze(['artist', 'ui']);
|
|
46
|
+
const SLOT_LIGHT = 'after-desktop-light';
|
|
47
|
+
const SLOT_DARK = 'after-desktop-dark';
|
|
48
|
+
const SMOKE_TITLE = '\\_\\_smoke\\_\\_%';
|
|
49
|
+
const HEARTS = 'lifecycle_work_appreciations';
|
|
50
|
+
// where a page tweak's page hangs, by the page inventory's surface (docs/page-inventory.json)
|
|
51
|
+
const SURFACES = Object.freeze({ builders: 'The builders’ hall', landing: 'The public site', status: 'The status page' });
|
|
52
|
+
|
|
53
|
+
// A picture whose file is gone (an old upgrade erased the store once, task 1004381)
|
|
54
|
+
// would hang an empty frame; such a URL is dropped, and a work left with none is not
|
|
55
|
+
// hung. The checks are async and run together (never a blocking stat in a request),
|
|
56
|
+
// and each answer is held for ON_DISK_TTL_MS: a stored picture's name never changes,
|
|
57
|
+
// so a wall reloaded within the window costs no file system reads.
|
|
58
|
+
const ON_DISK_TTL_MS = 10 * 60 * 1000;
|
|
59
|
+
const onDiskSeen = new Map();
|
|
60
|
+
async function onDisk(url) {
|
|
61
|
+
const hit = onDiskSeen.get(url);
|
|
62
|
+
if (hit && Date.now() - hit.at < ON_DISK_TTL_MS) return hit.ok;
|
|
63
|
+
const p = taskVisuals.resolveSafe(String(url || '').split('/').pop());
|
|
64
|
+
let ok = false;
|
|
65
|
+
if (p) { try { await fs.promises.access(p); ok = true; } catch (_) { ok = false; } }
|
|
66
|
+
if (onDiskSeen.size > 2000) onDiskSeen.clear();
|
|
67
|
+
onDiskSeen.set(url, { ok, at: Date.now() });
|
|
68
|
+
return ok;
|
|
69
|
+
}
|
|
70
|
+
async function keepHung(rows, exists) {
|
|
71
|
+
const urls = [...new Set(rows.flatMap((r) => [r.light, r.dark, r.visual_url]).filter(Boolean))];
|
|
72
|
+
const there = new Set();
|
|
73
|
+
await Promise.all(urls.map(async (u) => { if (await exists(u)) there.add(u); }));
|
|
74
|
+
const keep = (u) => (u && there.has(u) ? u : null);
|
|
75
|
+
const out = rows.map((r) => Object.assign({}, r, { light: keep(r.light), dark: keep(r.dark), visual_url: keep(r.visual_url) }))
|
|
76
|
+
.filter((r) => r.light || r.dark || r.visual_url);
|
|
77
|
+
const gone = rows.length - out.length;
|
|
78
|
+
if (gone) out.forEach((r) => { r.total = Math.max(out.length, (Number(r.total) || 0) - gone); });
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cleanOrder(v) { return ORDERS.includes(v) ? v : 'popular'; }
|
|
83
|
+
|
|
84
|
+
// Once the table is there it stays there, so a found table is remembered for the
|
|
85
|
+
// process; an absent one is asked again (the migration may land while it runs).
|
|
86
|
+
let heartsFound = false;
|
|
87
|
+
async function heartsExist(pool) {
|
|
88
|
+
if (heartsFound && pool === api.pool) return true;
|
|
89
|
+
const { rows } = await pool.query('SELECT to_regclass($1) IS NOT NULL AS ok', [`public.${HEARTS}`]);
|
|
90
|
+
const ok = !!(rows[0] && rows[0].ok);
|
|
91
|
+
if (ok && pool === api.pool) heartsFound = true;
|
|
92
|
+
return ok;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The WHERE that makes a task a work THIS viewer may see. Shared by the wall and the
|
|
96
|
+
// heart, so a heart can only ever land on a work the wall would hang. $1 = the art
|
|
97
|
+
// disciplines, $2 = the viewer is an Archon, $3 = the viewer's id.
|
|
98
|
+
const WORK_WHERE = `
|
|
99
|
+
t.status = 'shipped' AND t.shipped_at IS NOT NULL
|
|
100
|
+
AND (t.discipline = ANY($1::text[]) OR t.source = 'page-tweak')
|
|
101
|
+
AND t.security_sensitive IS NOT TRUE
|
|
102
|
+
AND t.title NOT LIKE '${SMOKE_TITLE}' ESCAPE '\\'
|
|
103
|
+
AND (t.visual_url IS NOT NULL OR sl.light IS NOT NULL OR sl.dark IS NOT NULL)
|
|
104
|
+
AND (g.id IS NULL OR g.visibility IS DISTINCT FROM 'private' OR $2::boolean
|
|
105
|
+
OR EXISTS (SELECT 1 FROM goal_members gm WHERE gm.goal_id = g.id AND gm.builder_id = $3::bigint))`;
|
|
106
|
+
const WORK_FROM = `
|
|
107
|
+
FROM tasks t
|
|
108
|
+
JOIN builders b ON b.id = t.shipped_by AND b.system_role IS NULL
|
|
109
|
+
LEFT JOIN goals g ON g.id = t.goal_id
|
|
110
|
+
LEFT JOIN LATERAL (
|
|
111
|
+
SELECT max(s.visual_url) FILTER (WHERE s.slot = '${SLOT_LIGHT}') AS light,
|
|
112
|
+
max(s.visual_url) FILTER (WHERE s.slot = '${SLOT_DARK}') AS dark
|
|
113
|
+
FROM lifecycle_task_visual_slots s
|
|
114
|
+
WHERE s.task_id = t.id AND t.source = 'page-tweak'
|
|
115
|
+
) sl ON true`;
|
|
116
|
+
|
|
117
|
+
function viewerOf(viewer) {
|
|
118
|
+
const id = viewer && viewer.id != null ? String(viewer.id) : null;
|
|
119
|
+
const archon = !!viewer && String(viewer.rank || '').toLowerCase() === 'archon';
|
|
120
|
+
return { id, archon };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The columns a work is shaped from. `hearts` and `mine` read the table only when
|
|
124
|
+
// it is there; $3 is always the viewer.
|
|
125
|
+
function workCols(hearts) {
|
|
126
|
+
const count = hearts ? `(SELECT count(*) FROM ${HEARTS} h WHERE h.task_id = t.id)` : '0';
|
|
127
|
+
const mine = hearts ? `EXISTS (SELECT 1 FROM ${HEARTS} h WHERE h.task_id = t.id AND h.builder_id = $3::bigint)` : 'false';
|
|
128
|
+
return `t.id::text AS id, t.title, t.value_summary, t.shipped_at, t.source, t.source_ref,
|
|
129
|
+
t.visual_url, t.visual_alt, sl.light, sl.dark, g.title AS goal_title,
|
|
130
|
+
b.id::text AS artist_id, b.display_name, b.github_login, b.created_at AS artist_since,
|
|
131
|
+
${count}::int AS hearts, ${mine} AS mine,
|
|
132
|
+
count(*) OVER ()::int AS total`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readGallery({ order, viewer }, deps = {}) {
|
|
136
|
+
const pool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : api.pool;
|
|
137
|
+
const o = cleanOrder(order);
|
|
138
|
+
const v = viewerOf(viewer);
|
|
139
|
+
const hearts = await heartsExist(pool);
|
|
140
|
+
const { rows } = await pool.query(
|
|
141
|
+
`SELECT ${workCols(hearts)}
|
|
142
|
+
${WORK_FROM}
|
|
143
|
+
WHERE ${WORK_WHERE}
|
|
144
|
+
ORDER BY ${o === 'popular' ? 'hearts DESC, ' : ''}t.shipped_at DESC, t.id DESC
|
|
145
|
+
LIMIT ${MAX_WORKS}`,
|
|
146
|
+
[ART_DISCIPLINES, v.archon, v.id],
|
|
147
|
+
);
|
|
148
|
+
return { order: o, rows: await keepHung(rows, deps.exists || onDisk) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// THE STUDIO'S TWO SHELVES (task 1005061, Home v16 step 4): the works this viewer
|
|
152
|
+
// appreciated on the bench (newest heart first — a heart pins a work here) and the
|
|
153
|
+
// works they signed themselves (most appreciated first). Each is the wall's own
|
|
154
|
+
// WHERE, so a pin never shows a work the bench would not hang, and each is LIMIT
|
|
155
|
+
// MAX_SHELF with its whole size as a window count.
|
|
156
|
+
const MAX_SHELF = 6;
|
|
157
|
+
// A page id is <surface>:<page> (the page inventory's id rule); the easel asks for its
|
|
158
|
+
// page's newest work by the round's source ref, `page-tweak/<page id>/r<n>`.
|
|
159
|
+
const PAGE_ID_RE = /^[a-z0-9-]+:[a-z0-9-]+$/;
|
|
160
|
+
async function readStudioWorks({ viewer, page }, deps = {}) {
|
|
161
|
+
const pool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : api.pool;
|
|
162
|
+
const v = viewerOf(viewer);
|
|
163
|
+
const exists = deps.exists || onDisk;
|
|
164
|
+
const hearts = await heartsExist(pool);
|
|
165
|
+
const args = [ART_DISCIPLINES, v.archon, v.id];
|
|
166
|
+
const easelPage = typeof page === 'string' && PAGE_ID_RE.test(page) ? page : null;
|
|
167
|
+
const [pinned, yours, easel] = await Promise.all([
|
|
168
|
+
hearts ? pool.query(
|
|
169
|
+
`SELECT ${workCols(true)}
|
|
170
|
+
${WORK_FROM}
|
|
171
|
+
JOIN ${HEARTS} pin ON pin.task_id = t.id AND pin.builder_id = $3::bigint
|
|
172
|
+
WHERE ${WORK_WHERE}
|
|
173
|
+
ORDER BY pin.at DESC, t.id DESC
|
|
174
|
+
LIMIT ${MAX_SHELF}`,
|
|
175
|
+
args,
|
|
176
|
+
) : Promise.resolve({ rows: [] }),
|
|
177
|
+
pool.query(
|
|
178
|
+
`SELECT ${workCols(hearts)}
|
|
179
|
+
${WORK_FROM}
|
|
180
|
+
WHERE ${WORK_WHERE} AND t.shipped_by = $3::bigint
|
|
181
|
+
ORDER BY hearts DESC, t.shipped_at DESC, t.id DESC
|
|
182
|
+
LIMIT ${MAX_SHELF}`,
|
|
183
|
+
args,
|
|
184
|
+
),
|
|
185
|
+
easelPage ? pool.query(
|
|
186
|
+
`SELECT ${workCols(hearts)}
|
|
187
|
+
${WORK_FROM}
|
|
188
|
+
WHERE ${WORK_WHERE} AND t.source = 'page-tweak' AND starts_with(t.source_ref, $4)
|
|
189
|
+
ORDER BY t.shipped_at DESC, t.id DESC
|
|
190
|
+
LIMIT 1`,
|
|
191
|
+
[...args, `page-tweak/${easelPage}/r`],
|
|
192
|
+
) : Promise.resolve({ rows: [] }),
|
|
193
|
+
]);
|
|
194
|
+
const [p, y, e] = await Promise.all([keepHung(pinned.rows, exists), keepHung(yours.rows, exists), keepHung(easel.rows, exists)]);
|
|
195
|
+
return { pinned: p, yours: y, easel: e[0] || null };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function shapeShelf(rows) {
|
|
199
|
+
const works = rows.map(shapeWork);
|
|
200
|
+
return { total: rows.length ? Math.max(Number(rows[0].total) || 0, works.length) : 0, works };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function studioWorksHandler({ log, read = readStudioWorks } = {}) {
|
|
204
|
+
return async function homeStudioWorksRoute(req, res) {
|
|
205
|
+
try {
|
|
206
|
+
const page = req.query && typeof req.query.page === 'string' ? req.query.page : null;
|
|
207
|
+
const raw = await read({ viewer: { id: req.builder.id, rank: req.builder.rank }, page });
|
|
208
|
+
res.set('Cache-Control', 'private, no-store');
|
|
209
|
+
res.json({ pinned: shapeShelf(raw.pinned), yours: shapeShelf(raw.yours), easel: raw.easel ? shapeWork(raw.easel) : null });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (log) log.error('GET /home/studio/works', err);
|
|
212
|
+
res.fail('home_studio_works_failed', { status: 500, message: 'internal error' });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The task title of a page tweak round is `Tweak: <page title> (<surface>:<page>)`
|
|
218
|
+
// (copy-desk/pages.js); its source ref ends `/r<n>`.
|
|
219
|
+
function tweakParts(row) {
|
|
220
|
+
const t = /^Tweak:\s*(.+?)\s*\(([a-z0-9-]+):([a-z0-9-]+)\)?\s*$/.exec(String(row.title || ''));
|
|
221
|
+
const r = /\/r([1-9][0-9]*)$/.exec(String(row.source_ref || ''));
|
|
222
|
+
return { title: t ? t[1] : String(row.title || ''), surface: t ? t[2] : null, page: t ? t[3] : null, round: r ? Number(r[1]) : null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isoDay(d) {
|
|
226
|
+
const x = d instanceof Date ? d : new Date(d);
|
|
227
|
+
return isNaN(x) ? null : x.toISOString().slice(0, 10);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// One row → one work as the wall hangs it. Pure: a test feeds it rows.
|
|
231
|
+
function shapeWork(row) {
|
|
232
|
+
const tweak = row.source === 'page-tweak';
|
|
233
|
+
const p = tweak ? tweakParts(row) : null;
|
|
234
|
+
const one = row.visual_url || null;
|
|
235
|
+
return {
|
|
236
|
+
id: String(row.id),
|
|
237
|
+
page: tweak && p.surface && p.page ? `${p.surface}:${p.page}` : null,
|
|
238
|
+
// a ship's title can open with its plan code ("WA6.RP04 · "); a wall label does not
|
|
239
|
+
title: tweak ? p.title : String(row.title || '').replace(/^[A-Z][A-Z0-9]*(\.[A-Z0-9]+)*\s*·\s*/, ''),
|
|
240
|
+
medium: tweak ? (p.round > 1 ? `A page tweak, round ${p.round}` : 'A page tweak') : 'Page design',
|
|
241
|
+
place: tweak ? (SURFACES[p.surface] || null) : (row.goal_title || null),
|
|
242
|
+
words: row.value_summary || '',
|
|
243
|
+
shipped: isoDay(row.shipped_at),
|
|
244
|
+
art: { light: row.light || row.dark || one, dark: row.dark || row.light || one, alt: row.visual_alt || null },
|
|
245
|
+
artist: {
|
|
246
|
+
id: String(row.artist_id),
|
|
247
|
+
name: row.display_name || row.github_login || 'An artist',
|
|
248
|
+
login: row.github_login || '',
|
|
249
|
+
since: isoDay(row.artist_since),
|
|
250
|
+
},
|
|
251
|
+
hearts: Math.max(0, Number(row.hearts) || 0),
|
|
252
|
+
mine: row.mine === true,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shapeGallery(raw) {
|
|
257
|
+
const works = raw.rows.map(shapeWork);
|
|
258
|
+
return {
|
|
259
|
+
order: raw.order,
|
|
260
|
+
total: raw.rows.length ? Math.max(Number(raw.rows[0].total) || 0, works.length) : 0,
|
|
261
|
+
works,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Appreciate a work, or take it back. `on` is the state wanted, so a double click or
|
|
266
|
+
// a retried request lands the same row (never a second heart). Refuses anything the
|
|
267
|
+
// viewer's wall would not hang.
|
|
268
|
+
async function setHeart({ taskId, on, viewer }, deps = {}) {
|
|
269
|
+
const pool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : api.pool;
|
|
270
|
+
const v = viewerOf(viewer);
|
|
271
|
+
if (!(await heartsExist(pool))) return { ok: false, code: 'gallery_unavailable', status: 503 };
|
|
272
|
+
const hit = await pool.query(
|
|
273
|
+
`SELECT t.id::text AS id ${WORK_FROM} WHERE t.id = $4::bigint AND ${WORK_WHERE}`,
|
|
274
|
+
[ART_DISCIPLINES, v.archon, v.id, taskId],
|
|
275
|
+
);
|
|
276
|
+
if (!hit.rows.length) return { ok: false, code: 'not_a_work', status: 404 };
|
|
277
|
+
if (on) {
|
|
278
|
+
await pool.query(`INSERT INTO ${HEARTS} (task_id, builder_id) VALUES ($1::bigint, $2::bigint) ON CONFLICT DO NOTHING`, [taskId, v.id]);
|
|
279
|
+
} else {
|
|
280
|
+
await pool.query(`DELETE FROM ${HEARTS} WHERE task_id = $1::bigint AND builder_id = $2::bigint`, [taskId, v.id]);
|
|
281
|
+
}
|
|
282
|
+
const { rows } = await pool.query(
|
|
283
|
+
`SELECT count(*)::int AS hearts, bool_or(builder_id = $2::bigint) AS mine FROM ${HEARTS} WHERE task_id = $1::bigint`,
|
|
284
|
+
[taskId, v.id],
|
|
285
|
+
);
|
|
286
|
+
return { ok: true, id: String(taskId), hearts: Number(rows[0] && rows[0].hearts) || 0, mine: !!(rows[0] && rows[0].mine) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function galleryHandler({ log, read = readGallery } = {}) {
|
|
290
|
+
return async function homeGalleryRoute(req, res) {
|
|
291
|
+
try {
|
|
292
|
+
const viewer = req.builder ? { id: req.builder.id, rank: req.builder.rank } : null;
|
|
293
|
+
const raw = await read({ order: req.query && req.query.order, viewer });
|
|
294
|
+
res.set('Cache-Control', 'private, no-store');
|
|
295
|
+
res.json(shapeGallery(raw));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (log) log.error('GET /home/gallery', err);
|
|
298
|
+
res.fail('home_gallery_failed', { status: 500, message: 'internal error' });
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function heartHandler({ log, write = setHeart } = {}) {
|
|
304
|
+
return async function homeGalleryHeartRoute(req, res) {
|
|
305
|
+
const taskId = Number(req.params && req.params.id);
|
|
306
|
+
const on = req.body && req.body.on;
|
|
307
|
+
if (!Number.isInteger(taskId) || taskId <= 0) return res.fail('bad_work_id', { status: 400, message: 'work id must be a positive integer' });
|
|
308
|
+
if (typeof on !== 'boolean') return res.fail('bad_heart', { status: 400, message: 'send { on: true } to appreciate a work, { on: false } to take it back' });
|
|
309
|
+
try {
|
|
310
|
+
const out = await write({ taskId, on, viewer: { id: req.builder.id, rank: req.builder.rank } });
|
|
311
|
+
if (!out.ok) return res.fail(out.code, { status: out.status, message: out.code === 'not_a_work' ? 'no work with that id hangs in your gallery' : 'the gallery is not set up on this project yet' });
|
|
312
|
+
res.json({ id: out.id, hearts: out.hearts, mine: out.mine });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (log) log.error('PUT /home/gallery/:id/heart', err);
|
|
315
|
+
res.fail('home_gallery_heart_failed', { status: 500, message: 'internal error' });
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { readStudioWorks, studioWorksHandler, ORDERS, ART_DISCIPLINES, SURFACES, cleanOrder, keepHung, readGallery, shapeWork, shapeGallery, setHeart, galleryHandler, heartHandler };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- lifecycle_020_work_appreciations.sql — a builder may appreciate a work in the
|
|
2
|
+
-- gallery, once (task 1005060, Home v16 step 3).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. In Artistry, Home is a gallery of the project's signed works: shipped
|
|
5
|
+
-- artist and ui tasks that carry a picture (modules/lifecycle/home-gallery.js says
|
|
6
|
+
-- which). The owner's rule for it is "not about quality but about appreciation":
|
|
7
|
+
-- a work carries how many builders appreciated it and one heart to add yours, and
|
|
8
|
+
-- Most popular (the gallery's default order) is that count. It is never a grade,
|
|
9
|
+
-- a vote or a rank, so it lives apart from peer_votes and task grades, and
|
|
10
|
+
-- nothing reads it but the gallery.
|
|
11
|
+
--
|
|
12
|
+
-- ONE ROW PER (work, builder). Appreciating twice is the same row; taking it back
|
|
13
|
+
-- deletes it. A work is a task, so it references tasks and goes with it.
|
|
14
|
+
--
|
|
15
|
+
-- A MODULE TABLE, lifecycle_-namespaced (ADR 0083): it references the core tasks
|
|
16
|
+
-- and builders tables the way lifecycle_013 does and alters neither.
|
|
17
|
+
--
|
|
18
|
+
-- PORTABLE and replay-safe: IF NOT EXISTS throughout, no instance data.
|
|
19
|
+
|
|
20
|
+
BEGIN;
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS lifecycle_work_appreciations (
|
|
23
|
+
task_id bigint NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
24
|
+
builder_id bigint NOT NULL REFERENCES builders(id) ON DELETE CASCADE,
|
|
25
|
+
at timestamptz NOT NULL DEFAULT now(),
|
|
26
|
+
PRIMARY KEY (task_id, builder_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- "Your pins" in the studio reads one builder's hearts, newest first.
|
|
30
|
+
CREATE INDEX IF NOT EXISTS lifecycle_work_appreciations_builder_idx
|
|
31
|
+
ON lifecycle_work_appreciations (builder_id, at DESC);
|
|
32
|
+
|
|
33
|
+
COMMENT ON TABLE lifecycle_work_appreciations IS
|
|
34
|
+
'One row per builder who appreciated a gallery work (a shipped artist/ui task with a picture). Appreciation, never a grade: Most popular in the artist''s Home orders by its count (task 1005060).';
|
|
35
|
+
|
|
36
|
+
COMMIT;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/routes/home-gallery.js — the artist's Home: the gallery's works
|
|
4
|
+
// and the one heart a builder may give each (task 1005060, Home v16 step 3).
|
|
5
|
+
//
|
|
6
|
+
// Its own route file for the reason routes/home-city.js has one: the path is
|
|
7
|
+
// top-level `/home/gallery`, never under `/tasks`, whose `/:id` would swallow a
|
|
8
|
+
// literal segment. The reads and handlers live in ../home-gallery.js so a test
|
|
9
|
+
// drives them with no express and no database.
|
|
10
|
+
//
|
|
11
|
+
// MODULE BOUNDARY: reaches core only through the published doorway
|
|
12
|
+
// `../../../src/module-api`; `../home-gallery` is this module's own.
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const api = require('../../../src/module-api');
|
|
16
|
+
const gallery = require('../home-gallery');
|
|
17
|
+
|
|
18
|
+
const log = api.logger('lifecycle');
|
|
19
|
+
|
|
20
|
+
module.exports = function buildHomeGalleryRouter() {
|
|
21
|
+
const router = express.Router();
|
|
22
|
+
// rank: any-builder — GET /home/gallery?order=popular|latest, at most 60 works.
|
|
23
|
+
// Every work is a shipped task whose picture a signed-in builder can already open
|
|
24
|
+
// (the task-visuals audience); a work under a private goal the viewer cannot read
|
|
25
|
+
// is left off. Each carries its heart count and whether the viewer gave one.
|
|
26
|
+
router.get('/home/gallery', api.requireBuilder, gallery.galleryHandler({ log }));
|
|
27
|
+
// rank: any-builder — PUT /home/gallery/:id/heart { on }. Appreciation, never a
|
|
28
|
+
// grade: one heart per builder per work, given or taken back by the builder
|
|
29
|
+
// themself (the caller, never the body). Refused for anything their wall would
|
|
30
|
+
// not hang.
|
|
31
|
+
router.put('/home/gallery/:id/heart', api.requireBuilder, gallery.heartHandler({ log }));
|
|
32
|
+
// rank: any-builder — GET /home/studio/works?page=<page id>, the studio's two shelves
|
|
33
|
+
// (task 1005061): the works the CALLER appreciated and the works the caller signed,
|
|
34
|
+
// at most six each, and the easel page's newest work, all by the wall's own rule.
|
|
35
|
+
router.get('/home/studio/works', api.requireBuilder, gallery.studioWorksHandler({ log }));
|
|
36
|
+
return router;
|
|
37
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// modules/market-watch/routes/settings.js — the owner's Market Watch settings (task 1004938,
|
|
2
|
-
// goal 1000137). The page is the hall's /market-watch
|
|
2
|
+
// goal 1000137). The page is the owner's Settings tab on the hall's /market-watch (task 1005045), built to
|
|
3
3
|
// docs/design/mocks/market-watch/settings.html (option B, the fuel gauge).
|
|
4
4
|
//
|
|
5
5
|
// GET /market-watch/settings the switch, cap, cadence, this month's spend, the next
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.32",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.32",
|
|
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.24.
|
|
3
|
+
"version": "1.24.32",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11108,7 +11108,35 @@
|
|
|
11108
11108
|
"text": "Your agent settings now live in one place, My agents: Settings shows a short link there instead of repeating them, and every old Settings link to them opens My agents."
|
|
11109
11109
|
}
|
|
11110
11110
|
],
|
|
11111
|
-
"1.24.
|
|
11111
|
+
"1.24.31": [
|
|
11112
|
+
{
|
|
11113
|
+
"id": "1005062",
|
|
11114
|
+
"text": "Releases work again: a false alarm about the GitHub logo on the new sign-in card had stopped every update from going out since this morning."
|
|
11115
|
+
},
|
|
11116
|
+
{
|
|
11117
|
+
"id": "1005061",
|
|
11118
|
+
"text": "The gallery now has a studio behind it. \"Your studio\" opens a quiet room with the page you are working on and a button back into it, the pages whose words changed since an artist last designed them, the newest words flagged on"
|
|
11119
|
+
},
|
|
11120
|
+
{
|
|
11121
|
+
"id": "1005060",
|
|
11122
|
+
"text": "In Artistry, Home is now a gallery: you sit on a bench in the museum and swipe through the project's signed works, most popular first or latest. Each work has a small museum label that opens closer on a click, the artist's sig"
|
|
11123
|
+
},
|
|
11124
|
+
{
|
|
11125
|
+
"id": "1005059",
|
|
11126
|
+
"text": "In Ideating mode, Home is now a night sky of the project's ideas over the city's real skyline. Each working area's ideas form a constellation, bigger stars have more votes, and a ringed star is waiting on you. Pointing at the"
|
|
11127
|
+
},
|
|
11128
|
+
{
|
|
11129
|
+
"id": "1005058",
|
|
11130
|
+
"text": "Home is now one city around the government's core: the new buildings lead, the core and the art museum are quiet doors, and time can be rewound smoothly from live."
|
|
11131
|
+
},
|
|
11132
|
+
{
|
|
11133
|
+
"id": "1005045",
|
|
11134
|
+
"text": "The owner's Market Watch settings are now a Settings tab on the Market Watch page, so the sidebar shows Market Watch once and old settings links still land in the right place."
|
|
11135
|
+
},
|
|
11136
|
+
{
|
|
11137
|
+
"id": "1005057",
|
|
11138
|
+
"text": "The signed-out page is now a clean sign-in screen: no side panel or top bar, just the logo, the project name, one \"Sign in with GitHub\" button and a small \"Request access\" link for newcomers."
|
|
11139
|
+
},
|
|
11112
11140
|
{
|
|
11113
11141
|
"id": "1005056",
|
|
11114
11142
|
"text": "The \"waiting on you\" dots on Home's project city now show every person the matters actually wait on, not just the owner."
|
|
@@ -11117,5 +11145,11 @@
|
|
|
11117
11145
|
"id": "1005054",
|
|
11118
11146
|
"text": "Home now shows the whole project as a living city: towers that grow as work ships, the people building them, and a slider to replay the last ten weeks."
|
|
11119
11147
|
}
|
|
11148
|
+
],
|
|
11149
|
+
"1.24.32": [
|
|
11150
|
+
{
|
|
11151
|
+
"id": "1005064",
|
|
11152
|
+
"text": "The governor's Home is now the street: choosing Owner on Home takes you straight to it, so each of the four crafts has its own Home."
|
|
11153
|
+
}
|
|
11120
11154
|
]
|
|
11121
11155
|
}
|
|
@@ -204,6 +204,14 @@ const FILE_ALLOWLIST = [
|
|
|
204
204
|
+ 'attribute, the same "why not a shape rule" secret-scrub.js records. The leading-zero guard '
|
|
205
205
|
+ 'catches the variants that have one; this is the residue. No topology here, only geometry.',
|
|
206
206
|
},
|
|
207
|
+
{
|
|
208
|
+
path: 'modules/hall-ui/public/hall-render.js',
|
|
209
|
+
classes: ['public-ip'],
|
|
210
|
+
reason:
|
|
211
|
+
'The same inlined forge logo, on the hall\'s signed-out sign-in card (task 1005057): its SVG '
|
|
212
|
+
+ 'path data carries the same "…1.08.58 1.23.82.72 1.21…" run as projects.html above. '
|
|
213
|
+
+ 'Geometry, not topology (task 1005062).',
|
|
214
|
+
},
|
|
207
215
|
{
|
|
208
216
|
path: 'modules/security/tests/artifact_scan.mjs',
|
|
209
217
|
classes: ['public-ip'],
|
|
@@ -22,6 +22,7 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
|
|
|
22
22
|
| `--modules-off discord,provisioning` | report those modules disabled in `window.__MODULES__` — verifies `moduleOn()` gating (the Discord links, Instances/Start-a-project widgets must vanish) |
|
|
23
23
|
| `--empty` | drain every array in every fixture — the empty-state screenshot pass |
|
|
24
24
|
| `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
|
|
25
|
+
| `--signed-out` | answer `/me` with a 401 — preview the signed-out sign-in card |
|
|
25
26
|
| `--latency 3000` | hold every data read that many ms before answering, so a page's loading state (the hall's `.hall-loading` mark) stays on screen long enough to look at |
|
|
26
27
|
|
|
27
28
|
Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly` — except
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
// --fixture-me do NOT read your own account live; serve the checked-in
|
|
25
25
|
// fixtures/me.json snapshot (deterministic screenshot passes,
|
|
26
26
|
// and the offline/signed-out path) — task 1003315
|
|
27
|
+
// --signed-out answer /me with a 401, so the hall renders the signed-out
|
|
28
|
+
// sign-in card (the sign-in redesign)
|
|
27
29
|
//
|
|
28
30
|
// The account is YOURS: when this machine holds a GDS CLI session the me-scoped
|
|
29
31
|
// reads are fetched live at boot and layered over the fixtures, so the hall
|
|
@@ -65,6 +67,8 @@ const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.tri
|
|
|
65
67
|
// own account live. The screenshot matrix in the README wants a stable subject,
|
|
66
68
|
// and it is the explicit way to preview the signed-out fallback (task 1003315).
|
|
67
69
|
const FIXTURE_ME = process.argv.includes('--fixture-me');
|
|
70
|
+
// --signed-out: /me answers 401, the way prod answers a visitor with no session.
|
|
71
|
+
const SIGNED_OUT = process.argv.includes('--signed-out');
|
|
68
72
|
// --latency: hold every data read this many ms before answering (task 1005043),
|
|
69
73
|
// so a page's loading state stays on screen long enough to be looked at. The
|
|
70
74
|
// fixtures answer in a millisecond, which is why no loader had ever been seen.
|
|
@@ -194,6 +198,8 @@ const CANNED_WRITES = [
|
|
|
194
198
|
['POST', /^specialities$/, 'specialities.post'],
|
|
195
199
|
// the Market Watch weekly ask's answer (task 1004940), so its "sent." state renders.
|
|
196
200
|
['POST', /^market-watch\/ask\/answer$/, 'market-watch__ask__answer.post'],
|
|
201
|
+
// the artist's Home (task 1005060): a heart given in the gallery, so it stays given.
|
|
202
|
+
['PUT', /^home\/gallery\/\d+\/heart$/, 'home__gallery__heart.put'],
|
|
197
203
|
['PATCH', /^specialities\/\d+$/, 'specialities__12.patch'],
|
|
198
204
|
// the rest of the speciality tools, now on My agents too (task 1005052): stop
|
|
199
205
|
// using one, and train your own, so their toasts and redraws render here.
|
|
@@ -329,6 +335,10 @@ app.use((req, res, next) => {
|
|
|
329
335
|
return res.status(400).json({ error: { code: 'bad_path', message: 'path escapes the fixtures' } });
|
|
330
336
|
}
|
|
331
337
|
|
|
338
|
+
if (SIGNED_OUT && apiPath === 'me') {
|
|
339
|
+
return res.status(401).json({ error: { code: 'auth_required', message: 'signed out (--signed-out)' } });
|
|
340
|
+
}
|
|
341
|
+
|
|
332
342
|
// SSE live channel: emit one nudge (so "live refresh" paths run once), then
|
|
333
343
|
// keep-alive comments. Pages treat this exactly like prod's stream.
|
|
334
344
|
if (apiPath === 'live') {
|
|
@@ -1034,6 +1034,20 @@ function mountInternalSurfaces(app) {
|
|
|
1034
1034
|
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/roster${qs}`);
|
|
1035
1035
|
});
|
|
1036
1036
|
|
|
1037
|
+
// task 1005045 (owner decision 2026-11-03, idea 1001310): the owner's Market Watch
|
|
1038
|
+
// settings left their own page (task 1004938) for an owner-only Settings tab on
|
|
1039
|
+
// /market-watch, so the sidebar says "Market Watch" once. The old URL lands on that
|
|
1040
|
+
// tab; 302 for the same reason as the directory above, and no page gate: the redirect
|
|
1041
|
+
// discloses nothing, /market-watch has its own, and the settings routes are the wall.
|
|
1042
|
+
const RETIRED_MW_SETTINGS_RE = /^\/builders\/market-watch-settings(?:\.html)?\/?$/;
|
|
1043
|
+
app.use((req, res, next) => {
|
|
1044
|
+
if (isModuleSurfaceHost(req)) return next();
|
|
1045
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
1046
|
+
if (!RETIRED_MW_SETTINGS_RE.test(path.posix.normalize(req.path))) return next();
|
|
1047
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
1048
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/market-watch${qs}#settings`);
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1037
1051
|
// /deploy EXISTS ONLY WHERE THE DOOR DOES (task 1004296). The owner, 2026-09-25: /deploy is
|
|
1038
1052
|
// "only for bongos builders hall itself, not for all production ones". Every datum and
|
|
1039
1053
|
// both acts on that page are provisioning-module routes, so a hall that does not run the
|
|
@@ -1308,13 +1322,13 @@ function mountInternalSurfaces(app) {
|
|
|
1308
1322
|
// Claude works for them. Every read behind it is the viewer's own and
|
|
1309
1323
|
// requireBuilder (GET /me and its /me/* prefs, /specialities), so sign-in is the
|
|
1310
1324
|
// whole gate; the governing reads it adds are gated by their own routes.
|
|
1311
|
-
//
|
|
1325
|
+
// task 1004921 `ai-studio` (the viewer's own routes).
|
|
1312
1326
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1313
|
-
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ai-studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|my-agents|
|
|
1327
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ai-studio|tweak-editor|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams|my-agents|commands)(?:\.html)?\/?$/;
|
|
1314
1328
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1315
1329
|
// page that embeds them.
|
|
1316
1330
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
|
1317
|
-
const MARKET_WATCH_PAGE_RE = /^\/builders\/market-watch(?:-
|
|
1331
|
+
const MARKET_WATCH_PAGE_RE = /^\/builders\/market-watch(?:-(?:item|rules))?(?:\.html)?\/?$/; // task 1004941: every read behind them is requireBuilder
|
|
1318
1332
|
app.use((req, res, next) => {
|
|
1319
1333
|
if (isModuleSurfaceHost(req)) return next();
|
|
1320
1334
|
// Cheap early-out: only /builders/* paths can ever be gated, so skip the
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.32'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|