@bongos/core 1.19.299 → 1.19.301
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 +45 -20
- package/docs/copy-inventory.md +53 -28
- package/docs/copy-registry.json +285 -30
- package/docs/file-map.md +1 -1
- package/modules/hall-ui/public/idea.css +89 -0
- package/modules/hall-ui/public/idea.html +55 -0
- package/modules/hall-ui/public/idea.js +289 -0
- package/modules/hall-ui/public/ideas.css +27 -0
- package/modules/hall-ui/public/ideas.html +29 -7
- package/modules/hall-ui/public/ideas.js +145 -18
- package/modules/hall-ui/public/task-detail.js +13 -0
- package/modules/ideas/inbox.js +14 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/serve-internal.js +7 -1
- package/src/module-api.js +5 -1
- package/tests/idea_detail_page.mjs +397 -0
- package/tests/idea_hall_composer.mjs +51 -0
- package/tests/idea_spark_hall.mjs +263 -0
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
// here would be a second copy of a DIFFERENT rule — the exact mistake this file
|
|
27
27
|
// is organised to avoid — so the gauge reports completeness and stops there.
|
|
28
28
|
//
|
|
29
|
+
// TWO MODES, ONE FORM (task 1002930 / R154). Opened plainly, the composer FILES a
|
|
30
|
+
// new Full Idea. Opened as ?develop=<id> from the spark queue below, it DEVELOPS
|
|
31
|
+
// an existing spark: the spark's own words are shown above the fields, its title
|
|
32
|
+
// becomes a read-only fact rather than an editable input (developing an idea must
|
|
33
|
+
// not quietly rename it out from under its author), and filing calls
|
|
34
|
+
// POST /inbox/:id/develop instead of POST /inbox. The five fields, the meter, the
|
|
35
|
+
// validation and the draft behaviour are identical — which is the point of reusing
|
|
36
|
+
// this surface rather than building a second one that drifts from it.
|
|
37
|
+
//
|
|
29
38
|
// DRAFTS ARE LOCAL. "A partial draft is not lost on navigation" is a done-when,
|
|
30
39
|
// and a half-written thought is not something to put in the database: it has no
|
|
31
40
|
// title yet, no grade, and nothing downstream should be able to see it. It lives
|
|
@@ -38,9 +47,17 @@
|
|
|
38
47
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
39
48
|
const { escapeHtml, $, toast } = window.OTB;
|
|
40
49
|
|
|
41
|
-
// One key
|
|
42
|
-
//
|
|
43
|
-
|
|
50
|
+
// One key per THING BEING WRITTEN. A new idea and a spark being developed are
|
|
51
|
+
// different drafts, and sharing one key would let a half-written new idea
|
|
52
|
+
// overwrite someone's development work (or restore itself into it). Versioned by
|
|
53
|
+
// template key at save time too, so a project that changes its template (R156)
|
|
54
|
+
// cannot restore a draft shaped for the old one.
|
|
55
|
+
const developId = () => {
|
|
56
|
+
const m = /[?&]develop=(\d+)\b/.exec(location.search);
|
|
57
|
+
return m ? m[1] : null;
|
|
58
|
+
};
|
|
59
|
+
const DEVELOP_ID = developId();
|
|
60
|
+
const DRAFT_KEY = DEVELOP_ID ? `hall-full-idea-draft:develop:${DEVELOP_ID}` : 'hall-full-idea-draft';
|
|
44
61
|
const SCORE_DEBOUNCE_MS = 300;
|
|
45
62
|
const DRAFT_DEBOUNCE_MS = 500;
|
|
46
63
|
|
|
@@ -187,7 +204,9 @@
|
|
|
187
204
|
// problem, not a capture error). Matching that exactly means the button never
|
|
188
205
|
// promises an outcome the server will refuse, and never blocks one it accepts.
|
|
189
206
|
const missingRequired = template.fields.some((f) => f.required && !fieldEl(f.key).value.trim());
|
|
190
|
-
|
|
207
|
+
// In develop mode the title is the existing idea's and is not editable, so it
|
|
208
|
+
// cannot be a reason to keep the button off.
|
|
209
|
+
$('#fi-submit').disabled = filing || missingRequired || (!DEVELOP_ID && !titleEl().value.trim());
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
async function scoreNow() {
|
|
@@ -209,7 +228,7 @@
|
|
|
209
228
|
// round-trip: it depends only on what is typed.
|
|
210
229
|
if (lastScore) renderScore(lastScore);
|
|
211
230
|
else $('#fi-submit').disabled = filing
|
|
212
|
-
|| !titleEl().value.trim()
|
|
231
|
+
|| (!DEVELOP_ID && !titleEl().value.trim())
|
|
213
232
|
|| template.fields.some((f) => f.required && !fieldEl(f.key).value.trim());
|
|
214
233
|
}
|
|
215
234
|
|
|
@@ -251,14 +270,22 @@
|
|
|
251
270
|
showErr('');
|
|
252
271
|
$('#fi-submit').disabled = true;
|
|
253
272
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
273
|
+
// The one behavioural fork between the two modes. Developing is an act on an
|
|
274
|
+
// EXISTING idea, so it carries no title and no grade: the row already has
|
|
275
|
+
// both, and R146 preserves them along with its author.
|
|
276
|
+
const r = DEVELOP_ID
|
|
277
|
+
? await api.inbox.postInboxIdDevelop({
|
|
278
|
+
id: DEVELOP_ID,
|
|
279
|
+
body: { field_values: collect(), template_key: template.key },
|
|
280
|
+
})
|
|
281
|
+
: await api.inbox.postInbox({
|
|
282
|
+
body: {
|
|
283
|
+
title: titleEl().value.trim(),
|
|
284
|
+
grade: 'full',
|
|
285
|
+
template_key: template.key,
|
|
286
|
+
field_values: collect(),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
262
289
|
|
|
263
290
|
filing = false;
|
|
264
291
|
if (!r.ok) {
|
|
@@ -275,17 +302,28 @@
|
|
|
275
302
|
// grade write did not, and says so. Repeat that here instead of claiming a
|
|
276
303
|
// Full Idea was stored when the response says otherwise.
|
|
277
304
|
const landedFull = idea && idea.grade === 'full';
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
if (DEVELOP_ID) {
|
|
306
|
+
// The split is the developer's business: they earned a share of it and the
|
|
307
|
+
// author kept theirs, and saying so is how the handoff stops feeling like
|
|
308
|
+
// giving work away (R147 / ADR 0185).
|
|
309
|
+
const credited = r.data && r.data.idea && r.data.idea.ideator_credit;
|
|
310
|
+
toast(`Developed${score == null ? '' : ` — ${score}/100`}${credited ? `. ${credited} credits split with its author.` : '.'}`);
|
|
311
|
+
} else {
|
|
312
|
+
toast(landedFull
|
|
313
|
+
? `Filed as a Full Idea${score == null ? '' : ` — ${score}/100`}.`
|
|
314
|
+
: 'Filed, but stored as a quick idea — the grade could not be written.');
|
|
315
|
+
}
|
|
281
316
|
|
|
282
|
-
titleEl().value = '';
|
|
317
|
+
if (!DEVELOP_ID) titleEl().value = '';
|
|
283
318
|
for (const f of template.fields) fieldEl(f.key).value = '';
|
|
284
319
|
lastScore = null;
|
|
285
320
|
renderScore(null);
|
|
286
321
|
note('');
|
|
287
322
|
if (idea && idea.id) {
|
|
288
|
-
|
|
323
|
+
// The detail PAGE (task 1002929 / R153), not the #/idea/N overlay: the
|
|
324
|
+
// thing just filed has five prose answers, which is what the page is for.
|
|
325
|
+
const base = location.hostname.startsWith('builders.') ? '' : '/builders';
|
|
326
|
+
$('#fi-sub').innerHTML = `Filed <a href="${base}/idea/${encodeURIComponent(idea.id)}">idea ${escapeHtml(String(idea.id))}</a>. Write another below.`;
|
|
289
327
|
}
|
|
290
328
|
}
|
|
291
329
|
|
|
@@ -305,6 +343,79 @@
|
|
|
305
343
|
// boot
|
|
306
344
|
// ---------------------------------------------------------------------------
|
|
307
345
|
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// the spark queue (task 1002930 / R154)
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
const hallBase = () => (location.hostname.startsWith('builders.') ? '' : '/builders');
|
|
351
|
+
|
|
352
|
+
function renderQueue(sparks, count) {
|
|
353
|
+
const host = $('#fi-queue');
|
|
354
|
+
if (!sparks || sparks.length === 0) {
|
|
355
|
+
// An answer, not a blank space — and it says how one gets here, since the
|
|
356
|
+
// only way to fill this queue is for an author to offer something up.
|
|
357
|
+
host.innerHTML = '<p class="fi-queue-empty">Nothing is waiting. A spark appears here when its author offers a quick idea up for development.</p>';
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const total = Number.isInteger(count) ? count : sparks.length;
|
|
361
|
+
const more = total > sparks.length ? `<p class="fi-note">Showing ${sparks.length} of ${total}.</p>` : '';
|
|
362
|
+
host.innerHTML = `<div class="fi-queue">${sparks.map((sp) => {
|
|
363
|
+
const who = sp.filed_by_login ? `@${escapeHtml(sp.filed_by_login)}` : 'an unnamed builder';
|
|
364
|
+
const when = sp.captured_at ? ` · ${escapeHtml(String(sp.captured_at).slice(0, 10))}` : '';
|
|
365
|
+
return `
|
|
366
|
+
<div class="fi-spark">
|
|
367
|
+
<p class="fi-spark__title"><a href="${hallBase()}/idea/${encodeURIComponent(sp.id)}">${escapeHtml(sp.title || `Idea ${sp.id}`)}</a></p>
|
|
368
|
+
<span class="fi-spark__who">had by ${who}${when}</span>
|
|
369
|
+
<a class="fi-spark__go btn btn--ghost" href="?develop=${encodeURIComponent(sp.id)}">Develop this →</a>
|
|
370
|
+
</div>`;
|
|
371
|
+
}).join('')}</div>${more}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function loadQueue() {
|
|
375
|
+
const r = await api.inbox.getInboxSparks({});
|
|
376
|
+
// The queue is secondary to the composer: an outage here must not stop
|
|
377
|
+
// someone writing, so it degrades to a line rather than an error state.
|
|
378
|
+
if (!r.ok || !r.data) {
|
|
379
|
+
$('#fi-queue').innerHTML = '<p class="fi-queue-empty">Could not load the queue just now.</p>';
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
renderQueue(r.data.sparks, r.data.count);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// develop mode
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
// Show the spark's own words above the fields, and make the title a FACT rather
|
|
390
|
+
// than an input. Returns false when the idea cannot be developed, so boot can
|
|
391
|
+
// say why instead of presenting a form that will 409.
|
|
392
|
+
function enterDevelopMode(idea) {
|
|
393
|
+
if (idea.grade !== 'spark') {
|
|
394
|
+
$('#fi-sub').textContent = idea.grade === 'full'
|
|
395
|
+
? `Idea ${idea.id} has already been developed into a Full Idea.`
|
|
396
|
+
: `Idea ${idea.id} is not a Big Idea Spark — only its author can offer it up for development.`;
|
|
397
|
+
$('#fi-form').hidden = true;
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
$('#fi-develop').hidden = false;
|
|
401
|
+
$('#fi-develop-title').textContent = idea.title || `Idea ${idea.id}`;
|
|
402
|
+
$('#fi-develop-body').textContent = String(idea.body_md || '').trim();
|
|
403
|
+
$('#fi-develop-who').textContent = idea.filed_by_login
|
|
404
|
+
? `had by @${idea.filed_by_login}${idea.captured_at ? ` · ${String(idea.captured_at).slice(0, 10)}` : ''} — they keep the credit for the thought`
|
|
405
|
+
: 'they keep the credit for the thought';
|
|
406
|
+
// The title is the existing idea's. Shown, not editable: developing an idea
|
|
407
|
+
// must not quietly rename it out from under its author.
|
|
408
|
+
const t = titleEl();
|
|
409
|
+
t.value = idea.title || '';
|
|
410
|
+
t.readOnly = true;
|
|
411
|
+
t.tabIndex = -1;
|
|
412
|
+
$('#fi-title-label').hidden = true;
|
|
413
|
+
t.hidden = true;
|
|
414
|
+
$('#fi-submit').textContent = 'Develop this idea';
|
|
415
|
+
$('#fi-sub').textContent = 'Answer the five questions and this spark becomes a Full Idea.';
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
308
419
|
async function main() {
|
|
309
420
|
const r = await api.inbox.getInboxTemplate({});
|
|
310
421
|
if (!r.ok || !r.data || !r.data.template) {
|
|
@@ -323,6 +434,22 @@
|
|
|
323
434
|
$('#fi-form').addEventListener('submit', file);
|
|
324
435
|
$('#fi-clear').addEventListener('click', discard);
|
|
325
436
|
|
|
437
|
+
// The queue loads on every visit, develop mode or not: having just picked one
|
|
438
|
+
// up, seeing what else is waiting is the natural next thing.
|
|
439
|
+
loadQueue();
|
|
440
|
+
|
|
441
|
+
if (DEVELOP_ID) {
|
|
442
|
+
const d = await api.inbox.getInboxId({ id: DEVELOP_ID });
|
|
443
|
+
if (!d.ok || !d.data || !d.data.idea) {
|
|
444
|
+
$('#fi-sub').textContent = `Could not load idea ${DEVELOP_ID}.`;
|
|
445
|
+
$('#fi-form').hidden = true;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!enterDevelopMode(d.data.idea)) return;
|
|
449
|
+
// Re-run the button rule now that the title requirement is lifted.
|
|
450
|
+
onEdit();
|
|
451
|
+
}
|
|
452
|
+
|
|
326
453
|
if (usable) {
|
|
327
454
|
note('Draft restored.');
|
|
328
455
|
scoreNow();
|
|
@@ -118,6 +118,19 @@
|
|
|
118
118
|
}
|
|
119
119
|
if (r.shipped_at) nodes.push(h('p', 'deeplink-card__note', 'Shipped ' + fmtRel(r.shipped_at)));
|
|
120
120
|
|
|
121
|
+
// task 1002929 (R153): an idea now has a real page, and this card cannot show
|
|
122
|
+
// a Full Idea's five prose answers legibly — so it stops pretending to and
|
|
123
|
+
// points at the page instead. The overlay KEEPS the triage panel below (~320
|
|
124
|
+
// lines of promote/merge/discard); this is the summary→detail relationship the
|
|
125
|
+
// Work board already has with /task/:id, not a replacement.
|
|
126
|
+
if (kind === 'idea') {
|
|
127
|
+
var fv = h('p', 'deeplink-card__note');
|
|
128
|
+
var fl = h('a', 'deeplink-card__link', r.grade === 'full' ? 'Read the full idea →' : 'Open this idea →');
|
|
129
|
+
fl.href = hallBase() + '/idea/' + encodeURIComponent(r.id);
|
|
130
|
+
fv.appendChild(fl);
|
|
131
|
+
nodes.push(fv);
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
// task 759 → task 1002447: Metic+ idea triage, right in the overlay (no
|
|
122
135
|
// /idea-triage skill and no LLM in the loop). 759 shipped Discard alone;
|
|
123
136
|
// 1002447 adds the other two verbs so the whole verdict set is reachable
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -507,11 +507,24 @@ async function listOpenIdeas({ kind = null, limit = 50, viewerRank = null } = {}
|
|
|
507
507
|
return rows;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
// The by-id read. It carries the two people's LOGINS as well as their ids
|
|
511
|
+
// (task 1002929 / R153): an idea's attribution is a claim about humans, and
|
|
512
|
+
// "captured_by: 41" is not attribution to anyone reading it. Named to match the
|
|
513
|
+
// R145 spark queue's `filed_by_login` so the two idea surfaces agree.
|
|
514
|
+
//
|
|
515
|
+
// Both joins are LEFT: a builder row can be missing (an FK that went
|
|
516
|
+
// ON DELETE SET NULL), and an idea nobody developed has no developer at all —
|
|
517
|
+
// which is the common case, not an error. A plain JOIN here would drop most of
|
|
518
|
+
// the inbox, the same trap the grade join documents above.
|
|
510
519
|
async function getIdea(id) {
|
|
511
520
|
const { rows } = await pool.query(
|
|
512
|
-
`SELECT ${IDEA_COLS_Q}, ${GRADE_COLS_Q}
|
|
521
|
+
`SELECT ${IDEA_COLS_Q}, ${GRADE_COLS_Q},
|
|
522
|
+
ba.github_login AS filed_by_login,
|
|
523
|
+
bd.github_login AS developed_by_login
|
|
513
524
|
FROM idea_inbox i
|
|
514
525
|
${IDEA_JOIN_GRADE}
|
|
526
|
+
LEFT JOIN builders ba ON ba.id = i.captured_by
|
|
527
|
+
LEFT JOIN builders bd ON bd.id = g.developed_by
|
|
515
528
|
WHERE i.id = $1`,
|
|
516
529
|
[id]
|
|
517
530
|
);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.301",
|
|
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.301",
|
|
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.301",
|
|
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",
|
|
@@ -881,9 +881,15 @@ function mountInternalSurfaces(app) {
|
|
|
881
881
|
// needs its own two-segment match ahead of the single-segment resolution
|
|
882
882
|
// below. Same gate tier as Work/Roadmap/Goals (SIGNEDIN_BUILDERS_PAGE_RE).
|
|
883
883
|
const TASK_DETAIL_PAGE_RE = /^\/builders\/task\/(\d+)\/?$/;
|
|
884
|
+
// task 1002929 (R153 of goal 1000062): the idea-detail page is the SECOND
|
|
885
|
+
// two-segment hall page — /builders/idea/:id — for the same reason task 751 gave
|
|
886
|
+
// the task one: a record with real prose in it deserves a shareable URL and
|
|
887
|
+
// working back/forward, not a modal. Same gate tier as the task page.
|
|
888
|
+
const IDEA_DETAIL_PAGE_RE = /^\/builders\/idea\/(\d+)\/?$/;
|
|
884
889
|
function buildersHtmlName(reqPath) {
|
|
885
890
|
if (reqPath === '/builders' || reqPath === '/builders/') return 'index.html';
|
|
886
891
|
if (TASK_DETAIL_PAGE_RE.test(reqPath)) return buildersHtml['task.html'] ? 'task.html' : null;
|
|
892
|
+
if (IDEA_DETAIL_PAGE_RE.test(reqPath)) return buildersHtml['idea.html'] ? 'idea.html' : null;
|
|
887
893
|
// e.g. /builders/ranks or /builders/ranks.html → ranks.html
|
|
888
894
|
const m = reqPath.match(BUILDERS_PAGE_RE);
|
|
889
895
|
if (m) {
|
|
@@ -956,7 +962,7 @@ function mountInternalSurfaces(app) {
|
|
|
956
962
|
// behind the proxy's path normalization.
|
|
957
963
|
const norm = path.posix.normalize(req.path);
|
|
958
964
|
if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || SIGNEDIN_BUILDERS_PREFIX_RE.test(norm)
|
|
959
|
-
|| TASK_DETAIL_PAGE_RE.test(norm)) {
|
|
965
|
+
|| TASK_DETAIL_PAGE_RE.test(norm) || IDEA_DETAIL_PAGE_RE.test(norm)) {
|
|
960
966
|
return gdsAuth.requireBuilderPage(req, res, next);
|
|
961
967
|
}
|
|
962
968
|
next();
|
package/src/module-api.js
CHANGED
|
@@ -1025,8 +1025,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1025
1025
|
// landed since 1.19.297 with no explicit bump. run 32597859474. (task 1002620)
|
|
1026
1026
|
// 1.19.299 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1027
1027
|
// landed since 1.19.298 with no explicit bump. run 32600322268. (task 1002620)
|
|
1028
|
+
// 1.19.300 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1029
|
+
// landed since 1.19.299 with no explicit bump. run 32601521458. (task 1002620)
|
|
1030
|
+
// 1.19.301 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1031
|
+
// landed since 1.19.300 with no explicit bump. run 32602146266. (task 1002620)
|
|
1028
1032
|
// ---------------------------------------------------------------------------
|
|
1029
|
-
const CORE_VERSION = '1.19.
|
|
1033
|
+
const CORE_VERSION = '1.19.301'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1030
1034
|
|
|
1031
1035
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1032
1036
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|