@bongos/core 1.19.249 → 1.19.250
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 +52 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +14 -0
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +131 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +257 -3
- package/docs/api-reference.md +10 -1
- package/docs/file-map.md +1 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +10 -0
- package/modules/hall-ui/public/copy-desk.css +119 -0
- package/modules/hall-ui/public/copy-desk.html +139 -0
- package/modules/hall-ui/public/copy-desk.js +352 -0
- package/modules/hall-ui/public/shell.js +9 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +11 -1
- package/src/bongos/module-scope-map.js +17 -0
- package/src/bongos/serve-internal.js +11 -1
- package/src/module-api.js +3 -1
- package/tests/copy_desk_flags_db.mjs +250 -0
- package/tests/hall_nav.mjs +7 -1
- package/tests/module_loader.mjs +5 -2
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// modules/hall-ui/public/copy-desk.js — the hall "Copy desk" tab
|
|
2
|
+
// (/builders/copy-desk). Task 1003113 / R02 of goal 1000074, the artist's loop.
|
|
3
|
+
//
|
|
4
|
+
// The surface half of the flag: R01 built the inventory of every string the
|
|
5
|
+
// product shows a person; the copy-desk module turned "this one is wrong" into a
|
|
6
|
+
// row; this page is where an artist reads those rows and where everyone else
|
|
7
|
+
// files them.
|
|
8
|
+
//
|
|
9
|
+
// FOUR PANELS, IN THE ORDER AN ARTIST WOULD WORK:
|
|
10
|
+
// 1. the counts — what is outstanding, and the fast pile
|
|
11
|
+
// 2. Flagged — the queue itself, newest first, open by default
|
|
12
|
+
// 3. Said more than once — the deterministic duplicate signal, minus what has
|
|
13
|
+
// already been flagged or dismissed
|
|
14
|
+
// 4. Find something to flag — search the whole inventory
|
|
15
|
+
//
|
|
16
|
+
// NO LIVE CMS. Nothing on this page edits a string. The composer collects a
|
|
17
|
+
// JUDGEMENT (kind + reason); the fix ships as a claimed, graded task. There is
|
|
18
|
+
// deliberately no "new text" field — see modules/copy-desk/tests/copy_no_cms.mjs.
|
|
19
|
+
|
|
20
|
+
(() => {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const API = '/api/bongos';
|
|
24
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
25
|
+
const { escapeHtml, $, toast, fmtInt } = window.OTB;
|
|
26
|
+
|
|
27
|
+
let viewer = null;
|
|
28
|
+
let queue = null;
|
|
29
|
+
// The composer's current target, set when a "Flag" button is pressed.
|
|
30
|
+
let target = null;
|
|
31
|
+
let closing = null;
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// rendering helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
// A string shown as evidence is UNTRUSTED text from the product's own source
|
|
38
|
+
// tree, rendered into this page — so it is escaped like any other user data,
|
|
39
|
+
// and clipped so one runaway template literal cannot push the queue off screen.
|
|
40
|
+
function quote(text, max = 240) {
|
|
41
|
+
const s = String(text == null ? '' : text);
|
|
42
|
+
const clipped = s.length > max ? `${s.slice(0, max)}…` : s;
|
|
43
|
+
return `<q class="cd-quote">${escapeHtml(clipped)}</q>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function where(file, line) {
|
|
47
|
+
if (!file) return '';
|
|
48
|
+
return `<span class="cd-where">${escapeHtml(file)}${Number.isInteger(line) ? `:${line}` : ''}</span>`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const KIND_LABEL = {
|
|
52
|
+
'off-voice': 'off voice', slop: 'slop', unclear: 'unclear',
|
|
53
|
+
stale: 'stale', duplicate: 'duplicate', typo: 'typo', other: 'flagged',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// The three states a flag's target can be in (registry.js annotateFlag).
|
|
57
|
+
// 'gone' is the interesting one and is spelled out rather than iconified: it
|
|
58
|
+
// means the sentence was edited or deleted since the flag was filed, which is
|
|
59
|
+
// a strong hint the flag is resolvable — but a hint for a person to act on,
|
|
60
|
+
// never something the server closes on its own.
|
|
61
|
+
function targetState(f) {
|
|
62
|
+
if (f.target_state === 'gone') {
|
|
63
|
+
return '<span class="cd-tag cd-tag--gone" title="The string this flag names is no longer in the registry — it was edited or removed. Probably already fixed; confirm and close it.">target gone</span>';
|
|
64
|
+
}
|
|
65
|
+
if (f.moved) {
|
|
66
|
+
return '<span class="cd-tag cd-tag--moved" title="Same text, different place — it moved file or line since the flag was filed.">moved</span>';
|
|
67
|
+
}
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canClose(f) {
|
|
72
|
+
if (!viewer) return false;
|
|
73
|
+
if (String(f.flagged_by) === String(viewer.id)) return true;
|
|
74
|
+
return viewer.rank === 'metic' || viewer.rank === 'archon';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function flagRow(f) {
|
|
78
|
+
const who = escapeHtml(f.flagged_by_name || f.flagged_by_login || 'someone');
|
|
79
|
+
const when = f.flagged_at ? new Date(f.flagged_at).toLocaleDateString() : '';
|
|
80
|
+
// Show the live text when the string is still there, and the snapshot when it
|
|
81
|
+
// is not — labelled, so "what it says now" is never confused with "what it
|
|
82
|
+
// said when someone objected".
|
|
83
|
+
const body = f.current
|
|
84
|
+
? `${quote(f.current.text)} ${where(f.current.file, f.current.line)}`
|
|
85
|
+
: `<span class="cd-snapshot-label">said, when flagged:</span> ${quote(f.text_at_flag)} ${where(f.file_at_flag, f.line_at_flag)}`;
|
|
86
|
+
|
|
87
|
+
const scopeLine = f.scope === 'surface'
|
|
88
|
+
? `<span class="cd-tag">whole surface</span> <span class="cd-where">${escapeHtml(f.surface)}</span>`
|
|
89
|
+
: body;
|
|
90
|
+
|
|
91
|
+
const closed = f.status !== 'open'
|
|
92
|
+
? `<p class="cd-closed"><strong>${escapeHtml(f.status)}</strong>${f.resolved_task_id ? ` in task ${escapeHtml(String(f.resolved_task_id))}` : ''} — ${escapeHtml(f.resolution_note || '')}</p>`
|
|
93
|
+
: '';
|
|
94
|
+
|
|
95
|
+
return `<article class="cd-card" data-flag="${escapeHtml(String(f.id))}">
|
|
96
|
+
<div class="cd-card__head">
|
|
97
|
+
<span class="cd-tag cd-tag--kind">${escapeHtml(KIND_LABEL[f.kind] || f.kind)}</span>
|
|
98
|
+
${targetState(f)}
|
|
99
|
+
<span class="cd-byline">${who} · ${escapeHtml(when)}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="cd-card__target">${scopeLine}</div>
|
|
102
|
+
<p class="cd-reason">${escapeHtml(f.reason)}</p>
|
|
103
|
+
${closed}
|
|
104
|
+
${f.status === 'open' && canClose(f)
|
|
105
|
+
? `<div class="cd-card__actions"><button class="btn btn--ghost cd-close-btn" data-flag="${escapeHtml(String(f.id))}">Close…</button></div>`
|
|
106
|
+
: ''}
|
|
107
|
+
</article>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function dupeRow(d) {
|
|
111
|
+
const state = d.flagged
|
|
112
|
+
? '<span class="cd-tag cd-tag--flagged">already flagged</span>'
|
|
113
|
+
: d.settled
|
|
114
|
+
? '<span class="cd-tag cd-tag--settled">looked at</span>'
|
|
115
|
+
: '<span class="cd-tag cd-tag--untouched">untouched</span>';
|
|
116
|
+
const places = (d.at || []).map((a) => `<li>${escapeHtml(a)}</li>`).join('');
|
|
117
|
+
return `<article class="cd-card">
|
|
118
|
+
<div class="cd-card__head">
|
|
119
|
+
${state}
|
|
120
|
+
<span class="cd-byline">${escapeHtml(String(d.occurrences))} places · ${escapeHtml(d.surface)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="cd-card__target">${quote(d.text)}</div>
|
|
123
|
+
<ul class="cd-places">${places}</ul>
|
|
124
|
+
${d.flagged ? '' : `<div class="cd-card__actions"><button class="btn btn--ghost cd-flag-btn"
|
|
125
|
+
data-id="${escapeHtml(d.id)}" data-surface="${escapeHtml(d.surface)}" data-text="${escapeHtml(d.text)}">Flag this…</button></div>`}
|
|
126
|
+
</article>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resultRow(e) {
|
|
130
|
+
return `<article class="cd-card cd-card--slim">
|
|
131
|
+
<div class="cd-card__target">${quote(e.text)} ${where(e.file, e.line)}</div>
|
|
132
|
+
<div class="cd-card__actions">
|
|
133
|
+
<span class="cd-byline">${escapeHtml(e.surface)} · ${escapeHtml(e.origin || '')}</span>
|
|
134
|
+
${e.flagged
|
|
135
|
+
? '<span class="cd-tag cd-tag--flagged">already flagged</span>'
|
|
136
|
+
: `<button class="btn btn--ghost cd-flag-btn" data-id="${escapeHtml(e.id)}" data-surface="${escapeHtml(e.surface)}" data-text="${escapeHtml(e.text)}">Flag this…</button>`}
|
|
137
|
+
</div>
|
|
138
|
+
</article>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// paint
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function paintStats() {
|
|
146
|
+
const c = queue.counts;
|
|
147
|
+
const r = queue.registry || {};
|
|
148
|
+
const total = (r.counts && r.counts.strings) || 0;
|
|
149
|
+
$('#cd-stats').innerHTML = [
|
|
150
|
+
['Open flags', c.flags_open],
|
|
151
|
+
['Target since edited', c.flags_target_gone],
|
|
152
|
+
['Duplicates untouched', c.duplicates_untouched],
|
|
153
|
+
['Strings inventoried', total],
|
|
154
|
+
].map(([label, n]) =>
|
|
155
|
+
`<div class="cd-stat"><span class="cd-stat__n">${escapeHtml(String(fmtInt ? fmtInt(n) : n))}</span><span class="cd-stat__l">${escapeHtml(label)}</span></div>`
|
|
156
|
+
).join('');
|
|
157
|
+
|
|
158
|
+
// Say which artifact this page is describing, and how to check it is current.
|
|
159
|
+
// The registry has no timestamp by design (its generator is deterministic and
|
|
160
|
+
// carries no clock), so this page must not imply one — it names the file and
|
|
161
|
+
// the command instead of showing an age that would be the checkout's.
|
|
162
|
+
const blind = (r.blind_spots || []).length;
|
|
163
|
+
$('#cd-registry-note').innerHTML =
|
|
164
|
+
`Read from <code>${escapeHtml(r.at || 'the copy registry')}</code> as committed. `
|
|
165
|
+
+ `It is generated, not live: run <code>${escapeHtml(r.freshness_check || 'node scripts/gds/copy-inventory.js --check')}</code> to see whether it still matches the tree.`
|
|
166
|
+
+ (blind ? ` This inventory has ${blind} documented blind spot${blind === 1 ? '' : 's'} — server-side messages, skill prose and runtime-composed strings are not in it, so it is not every word the project says.` : '');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function paintFlags() {
|
|
170
|
+
const el = $('#cd-flags');
|
|
171
|
+
if (!queue.flags.length) {
|
|
172
|
+
el.innerHTML = '<div class="cd-empty">Nothing flagged here. That is either good news or an unread queue — the duplicates below are the cheapest place to find out.</div>';
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
el.innerHTML = queue.flags.map(flagRow).join('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function paintDupes() {
|
|
179
|
+
const el = $('#cd-dupes');
|
|
180
|
+
const rows = queue.duplicates || [];
|
|
181
|
+
if (!rows.length) { el.innerHTML = '<div class="cd-empty">No duplicated strings in the registry.</div>'; return; }
|
|
182
|
+
// Untouched first — it is the only part of this list that is work.
|
|
183
|
+
const ordered = [...rows].sort((a, b) =>
|
|
184
|
+
(Number(a.flagged || a.settled) - Number(b.flagged || b.settled)) || (b.occurrences - a.occurrences));
|
|
185
|
+
el.innerHTML = ordered.map(dupeRow).join('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function paintSurfaces() {
|
|
189
|
+
const sel = $('#cd-surface');
|
|
190
|
+
const surfaces = Object.keys((queue.registry && queue.registry.surfaces) || {});
|
|
191
|
+
sel.innerHTML = '<option value="">All surfaces</option>'
|
|
192
|
+
+ surfaces.map((s) => `<option value="${escapeHtml(s)}">${escapeHtml(s)}</option>`).join('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// load
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
async function loadQueue() {
|
|
200
|
+
const status = $('#cd-status').value;
|
|
201
|
+
const surface = $('#cd-surface').value;
|
|
202
|
+
const qs = new URLSearchParams({ status });
|
|
203
|
+
if (surface) qs.set('surface', surface);
|
|
204
|
+
|
|
205
|
+
const res = await api.request('GET', `${API}/copy-desk/queue?${qs}`);
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const code = res.data && res.data.error && res.data.error.code;
|
|
208
|
+
const detail = (res.data && res.data.error && res.data.error.details) || {};
|
|
209
|
+
$('#cd-sub').textContent = code === 'registry_missing'
|
|
210
|
+
? `No copy registry on this instance yet. Run ${detail.run || 'node scripts/gds/copy-inventory.js'} and commit it.`
|
|
211
|
+
: 'Could not load the copy desk.';
|
|
212
|
+
$('#cd-flags').innerHTML = '<div class="cd-empty">—</div>';
|
|
213
|
+
$('#cd-dupes').innerHTML = '<div class="cd-empty">—</div>';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
queue = res.data;
|
|
217
|
+
if (!$('#cd-surface').value && $('#cd-surface').options.length <= 1) paintSurfaces();
|
|
218
|
+
$('#cd-sub').textContent = 'The words this project shows people — what has been flagged, and what nobody has looked at.';
|
|
219
|
+
paintStats();
|
|
220
|
+
paintFlags();
|
|
221
|
+
paintDupes();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let searchTimer = null;
|
|
225
|
+
async function runSearch() {
|
|
226
|
+
const q = $('#cd-search').value.trim();
|
|
227
|
+
const surface = $('#cd-surface').value;
|
|
228
|
+
if (!q) { $('#cd-results').innerHTML = ''; $('#cd-search-count').textContent = ''; return; }
|
|
229
|
+
|
|
230
|
+
const qs = new URLSearchParams({ q });
|
|
231
|
+
if (surface) qs.set('surface', surface);
|
|
232
|
+
const res = await api.request('GET', `${API}/copy-desk/strings?${qs}`);
|
|
233
|
+
if (!res.ok) { $('#cd-search-count').textContent = 'Search failed.'; return; }
|
|
234
|
+
|
|
235
|
+
const { entries, matched, truncated, limit } = res.data;
|
|
236
|
+
// Say what is NOT shown. A silently capped list reads as a complete one.
|
|
237
|
+
$('#cd-search-count').textContent = truncated
|
|
238
|
+
? `${matched} matches — showing the first ${limit}. Narrow the search to see the rest.`
|
|
239
|
+
: `${matched} match${matched === 1 ? '' : 'es'}.`;
|
|
240
|
+
$('#cd-results').innerHTML = entries.map(resultRow).join('');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// the composer
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
function openComposer(t) {
|
|
248
|
+
target = t;
|
|
249
|
+
$('#cd-dialog-target').innerHTML = t.string_id
|
|
250
|
+
? `${quote(t.text)}<br><span class="cd-where">${escapeHtml(t.surface)}</span>`
|
|
251
|
+
: `<span class="cd-tag">whole surface</span> <span class="cd-where">${escapeHtml(t.surface)}</span>`;
|
|
252
|
+
$('#cd-reason').value = '';
|
|
253
|
+
$('#cd-dialog-err').textContent = '';
|
|
254
|
+
$('#cd-dialog').showModal();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function submitFlag() {
|
|
258
|
+
const reason = $('#cd-reason').value.trim();
|
|
259
|
+
// Mirror the server's floor locally so the common case answers instantly —
|
|
260
|
+
// but the server and the DB constraint are the real gates, not this.
|
|
261
|
+
if (reason.replace(/\s+/g, ' ').length < 10) {
|
|
262
|
+
$('#cd-dialog-err').textContent = 'A flag needs a reason an artist can act on. What is wrong with it, and what would better look like?';
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const body = {
|
|
266
|
+
scope: target.string_id ? 'string' : 'surface',
|
|
267
|
+
surface: target.surface,
|
|
268
|
+
reason,
|
|
269
|
+
kind: $('#cd-kind').value,
|
|
270
|
+
};
|
|
271
|
+
if (target.string_id) body.string_id = target.string_id;
|
|
272
|
+
|
|
273
|
+
const res = await api.request('POST', `${API}/copy-desk/flags`, { body });
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
const err = (res.data && res.data.error) || {};
|
|
276
|
+
const d = err.details || {};
|
|
277
|
+
$('#cd-dialog-err').textContent = d.what_this_means || d.why || err.message || 'Could not file the flag.';
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
$('#cd-dialog').close();
|
|
281
|
+
toast('Flagged. It is in the queue.');
|
|
282
|
+
await loadQueue();
|
|
283
|
+
if ($('#cd-search').value.trim()) await runSearch();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function openCloser(id) {
|
|
287
|
+
closing = queue.flags.find((f) => String(f.id) === String(id));
|
|
288
|
+
if (!closing) return;
|
|
289
|
+
$('#cd-close-target').innerHTML = `${quote(closing.reason, 160)}`;
|
|
290
|
+
$('#cd-close-note').value = '';
|
|
291
|
+
$('#cd-close-task').value = '';
|
|
292
|
+
$('#cd-close-err').textContent = '';
|
|
293
|
+
$('#cd-close-dialog').showModal();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function submitClose() {
|
|
297
|
+
const note = $('#cd-close-note').value.trim();
|
|
298
|
+
if (note.replace(/\s+/g, ' ').length < 10) {
|
|
299
|
+
$('#cd-close-err').textContent = 'Closing is a judgement too — say what you decided, so the next person does not re-litigate it.';
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const body = { status: $('#cd-close-status').value, resolution_note: note };
|
|
303
|
+
const taskId = $('#cd-close-task').value.trim();
|
|
304
|
+
if (taskId) body.resolved_task_id = Number(taskId);
|
|
305
|
+
|
|
306
|
+
const res = await api.request('POST', `${API}/copy-desk/flags/${encodeURIComponent(closing.id)}/close`, { body });
|
|
307
|
+
if (!res.ok) {
|
|
308
|
+
const err = (res.data && res.data.error) || {};
|
|
309
|
+
const d = err.details || {};
|
|
310
|
+
$('#cd-close-err').textContent = d.what_this_means || d.why || err.message || 'Could not close the flag.';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
$('#cd-close-dialog').close();
|
|
314
|
+
toast('Closed.');
|
|
315
|
+
await loadQueue();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// wire
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
document.addEventListener('click', (ev) => {
|
|
323
|
+
const flagBtn = ev.target.closest('.cd-flag-btn');
|
|
324
|
+
if (flagBtn) {
|
|
325
|
+
openComposer({
|
|
326
|
+
string_id: flagBtn.dataset.id,
|
|
327
|
+
surface: flagBtn.dataset.surface,
|
|
328
|
+
text: flagBtn.dataset.text,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const closeBtn = ev.target.closest('.cd-close-btn');
|
|
333
|
+
if (closeBtn) openCloser(closeBtn.dataset.flag);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
$('#cd-cancel').addEventListener('click', () => $('#cd-dialog').close());
|
|
337
|
+
$('#cd-submit').addEventListener('click', submitFlag);
|
|
338
|
+
$('#cd-close-cancel').addEventListener('click', () => $('#cd-close-dialog').close());
|
|
339
|
+
$('#cd-close-submit').addEventListener('click', submitClose);
|
|
340
|
+
$('#cd-status').addEventListener('change', loadQueue);
|
|
341
|
+
$('#cd-surface').addEventListener('change', () => { loadQueue(); if ($('#cd-search').value.trim()) runSearch(); });
|
|
342
|
+
$('#cd-search').addEventListener('input', () => {
|
|
343
|
+
clearTimeout(searchTimer);
|
|
344
|
+
searchTimer = setTimeout(runSearch, 200);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
(async () => {
|
|
348
|
+
const me = await api.request('GET', `${API}/me`);
|
|
349
|
+
if (me.ok) viewer = me.data.builder || me.data;
|
|
350
|
+
await loadQueue();
|
|
351
|
+
})();
|
|
352
|
+
})();
|
|
@@ -122,6 +122,15 @@
|
|
|
122
122
|
// active-nav detection (#nav-work), the 'work' gate case, and the server
|
|
123
123
|
// page-gate all keep working — only the visible label and order change.
|
|
124
124
|
{ id: 'work', label: 'Tasks', icon: 'work', href: '/work', gate: 'signed' },
|
|
125
|
+
// task 1003113 (R02 of goal 1000074): the artist's queue over the words the
|
|
126
|
+
// product shows people. gate 'signed' and NOT a craft gate — filing a flag
|
|
127
|
+
// is open to every role on purpose (the engineer who wrote the toast and
|
|
128
|
+
// the newcomer who could not read the button are the two people most
|
|
129
|
+
// likely to notice bad copy, and neither is the artist), and an artist is a
|
|
130
|
+
// craft rather than a rank, so hiding this behind one would hide the
|
|
131
|
+
// artist's own queue from the artist. The module gate drops the tab on an
|
|
132
|
+
// instance that disables copy-desk.
|
|
133
|
+
{ id: 'copy-desk', label: 'Copy desk', icon: 'docs', href: '/copy-desk', gate: 'signed', module: 'copy-desk' },
|
|
125
134
|
] },
|
|
126
135
|
// Your own numbers. task 2174 folded Achievements into Standing and this task
|
|
127
136
|
// retires its nav entry; VIEW_ALIASES still routes #achievements here, so old
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.250",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.250",
|
|
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.19.
|
|
3
|
+
"version": "1.19.250",
|
|
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",
|
|
@@ -73,7 +73,17 @@ const INTEGRATION = new Set([
|
|
|
73
73
|
// platform-server — network + package build + a spawned server, run by its own
|
|
74
74
|
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
75
75
|
'consumer_layout_boot',
|
|
76
|
-
'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog',
|
|
76
|
+
'claim_gate_open_blocker', 'claim_gate_rebase', 'clawback_confirmed_backlog',
|
|
77
|
+
// task 1003113: the copy_desk_flags schema half. The module's own tests are
|
|
78
|
+
// DB-free and cover the pure layer; the rules that MATTER here live in the
|
|
79
|
+
// schema — a CHECK-constraint reason floor whose whole argument is that no
|
|
80
|
+
// future write path evades it, a PARTIAL unique index whose partiality is the
|
|
81
|
+
// design, and a close guarded on the open status so two closers cannot both
|
|
82
|
+
// win. None of those are testable without a real transaction engine.
|
|
83
|
+
// (Prose here avoids single-quoted tokens: the pairing guard below parses this
|
|
84
|
+
// set out of the source, so a quoted word in a comment reads as an entry.)
|
|
85
|
+
'copy_desk_flags_db',
|
|
86
|
+
'discord_autojoin', 'discord_oauth',
|
|
77
87
|
// RENAMED by ADR 0174 (goal 1000068) — these entries said governance_* until task
|
|
78
88
|
// 1003091. A curated set keyed by a stem that no longer exists silently stops
|
|
79
89
|
// covering its file: both of these dropped OUT of the integration set and INTO the
|
|
@@ -216,6 +216,23 @@ const MODULE_GLOBS = {
|
|
|
216
216
|
'modules/game/', 'src/preview-server.js',
|
|
217
217
|
],
|
|
218
218
|
'art-pipeline': ['modules/art-pipeline/'],
|
|
219
|
+
// --- the COPY half of the design contract: the artist's loop over user-facing
|
|
220
|
+
// TEXT (goal 1000074). ADR 0081 gave design TOKENS a repo-owned source of
|
|
221
|
+
// truth; text had none. R01 built the inventory (the copy-inventory
|
|
222
|
+
// generator + its committed registry under docs/), R02 the flag + queue on
|
|
223
|
+
// top of it. The generator travels with the domain but stays under
|
|
224
|
+
// scripts/gds/ — the memory/ideas precedent (a command travels with its
|
|
225
|
+
// domain, design 02§1). The generated artifacts are listed as exact paths
|
|
226
|
+
// so a copy task's touches[] can name them without claiming all of docs/.
|
|
227
|
+
// KEY IS 'copy-desk', NOT 'copy': the branding contract already owns a
|
|
228
|
+
// `copy` block (branding.copy.tagline/hallName/notFound), so the bare word
|
|
229
|
+
// collides with kernel code and reds the "core carries no module code"
|
|
230
|
+
// fitness check on the English word alone.
|
|
231
|
+
'copy-desk': [
|
|
232
|
+
'modules/copy-desk/',
|
|
233
|
+
'scripts/gds/copy-inventory.js',
|
|
234
|
+
'docs/copy-registry.json', 'docs/copy-inventory.md',
|
|
235
|
+
],
|
|
219
236
|
// --- the declarative agent registry (goal 1000038, ADR 0083 default:false):
|
|
220
237
|
// agent definitions reconciled at deploy into agents_definitions + a per-fire
|
|
221
238
|
// agents_runs ledger. Carved to modules/agents/ at foundation time, so the
|
|
@@ -922,8 +922,18 @@ function mountInternalSurfaces(app) {
|
|
|
922
922
|
// thetes redirected exactly that reader to /builders/not-ready. Owner's call
|
|
923
923
|
// (task 1002710) — the RANK floor is gone; sign-in is unchanged.
|
|
924
924
|
// The link-hiding in shell.js is only cosmetic; these are the real gates.
|
|
925
|
+
// task 1003113: copy-desk joins this tier rather than a higher one. Filing a
|
|
926
|
+
// flag is open to every rank by design (see the nav comment in shell.js), so
|
|
927
|
+
// the page's only real gate is sign-in — the writes behind it self-gate, and
|
|
928
|
+
// closing someone else's flag is checked server-side at metic-or-author.
|
|
929
|
+
//
|
|
930
|
+
// NB the assignment below must stay `const NAME_RE = /…/;` with NOTHING between
|
|
931
|
+
// the `=` and the literal: tests/hall_page_gate_map.mjs reconstructs the whole
|
|
932
|
+
// URL→gate map by parsing this file statically, and a comment in that gap drops
|
|
933
|
+
// the regex out of its map — which reads as "every page on this tier is
|
|
934
|
+
// ungated". It fails loudly by design; keep prose above the const.
|
|
925
935
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
926
|
-
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
936
|
+
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|copy-desk|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
927
937
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
928
938
|
// page that embeds them.
|
|
929
939
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
package/src/module-api.js
CHANGED
|
@@ -925,8 +925,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
925
925
|
// landed since 1.19.247 with no explicit bump. run 32198208766. (task 1002620)
|
|
926
926
|
// 1.19.249 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
927
927
|
// landed since 1.19.248 with no explicit bump. run 32199293372. (task 1002620)
|
|
928
|
+
// 1.19.250 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
929
|
+
// landed since 1.19.249 with no explicit bump. run 32203357427. (task 1002620)
|
|
928
930
|
// ---------------------------------------------------------------------------
|
|
929
|
-
const CORE_VERSION = '1.19.
|
|
931
|
+
const CORE_VERSION = '1.19.250'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
930
932
|
|
|
931
933
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
932
934
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|