@bongos/core 1.19.660 → 1.19.661
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 +16 -16
- package/docs/copy-inventory.md +55 -54
- package/docs/copy-registry.json +81 -72
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/work.css +6 -0
- package/modules/hall-ui/public/work.html +18 -6
- package/modules/hall-ui/public/work.js +161 -66
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_sort_staleness.mjs +12 -1
- package/tests/hall_board_world.mjs +103 -0
|
@@ -62,15 +62,23 @@
|
|
|
62
62
|
// forever. Its own function because Creator/Age re-fetch it, same as the
|
|
63
63
|
// claimable feed. Best-effort: on failure the section stays as it was.
|
|
64
64
|
function loadBacklog() {
|
|
65
|
-
const q =
|
|
65
|
+
const q = backlogFilterQuery().replace(/^\?/, '&');
|
|
66
66
|
return getJSON(`${API}/tasks?status=backlog&limit=${BACKLOG_LIMIT}${q}`)
|
|
67
|
-
.then((r) => {
|
|
67
|
+
.then((r) => {
|
|
68
|
+
taskState.backlog = r.tasks || [];
|
|
69
|
+
renderBacklog(taskState.backlog);
|
|
70
|
+
// task 1003828 — the Goal filter draws on this feed too and races it.
|
|
71
|
+
// Re-offer the options now; populateGoalFilter keeps the selection.
|
|
72
|
+
populateGoalFilter();
|
|
73
|
+
})
|
|
68
74
|
.catch(() => { /* backlog stays as it was */ });
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
// The last server-facing filter
|
|
72
|
-
//
|
|
77
|
+
// The last server-facing filter queries we actually fetched with — one per
|
|
78
|
+
// feed, because the two routes answer DIFFERENT questions (see below).
|
|
79
|
+
// Compared on every filter change to decide re-fetch vs re-render.
|
|
73
80
|
let lastServerFilterQuery = '';
|
|
81
|
+
let lastBacklogQuery = '';
|
|
74
82
|
|
|
75
83
|
function serverFilterQuery() {
|
|
76
84
|
const parts = [];
|
|
@@ -81,6 +89,24 @@
|
|
|
81
89
|
return parts.length ? `?${parts.join('&')}` : '';
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
// task 1003828 — the BACKLOG's server query, deliberately NOT shared with the
|
|
93
|
+
// claimable feed's. The routes differ (GET /tasks answers version/kind/
|
|
94
|
+
// discipline/goal_id; /tasks/claimable answers only version+discipline), and the
|
|
95
|
+
// backlog read is CAPPED at BACKLOG_LIMIT while the Ready feed is not — so a
|
|
96
|
+
// client-only narrow here would lie, hiding the artist tasks at row 501.
|
|
97
|
+
// '__none__' on Goal has no server predicate and is never sent.
|
|
98
|
+
function backlogFilterQuery() {
|
|
99
|
+
const f = boardFilterValues();
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (f.creator) parts.push(`created_by=${encodeURIComponent(f.creator === '__none__' ? 'none' : f.creator)}`);
|
|
102
|
+
if (f.stale) parts.push(`stale_days=${encodeURIComponent(f.stale)}`);
|
|
103
|
+
if (f.version) parts.push(`version=${encodeURIComponent(f.version)}`);
|
|
104
|
+
if (f.kind) parts.push(`kind=${encodeURIComponent(f.kind)}`);
|
|
105
|
+
if (f.discipline) parts.push(`discipline=${encodeURIComponent(f.discipline)}`);
|
|
106
|
+
if (f.goal && f.goal !== '__none__') parts.push(`goal_id=${encodeURIComponent(f.goal)}`);
|
|
107
|
+
return parts.length ? `?${parts.join('&')}` : '';
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
// task 1003278 — the ceiling on the Backlog read. Comfortably above today's
|
|
85
111
|
// ~300 rows so nothing visibly changes now, and low enough that the page can
|
|
86
112
|
// never be asked to hold the whole table as the project grows.
|
|
@@ -289,54 +315,76 @@
|
|
|
289
315
|
return false;
|
|
290
316
|
}
|
|
291
317
|
|
|
318
|
+
// task 1003828 — the board's filter lenses, read once from the DOM. ONE reader,
|
|
319
|
+
// so a control that exists can never be one a tab forgot to consult: before
|
|
320
|
+
// this, every dropdown was read inside renderClaimable and nowhere else, which
|
|
321
|
+
// left all six silently inert on the Backlog tab. ('__none__' on Creator
|
|
322
|
+
// isolates the unattributed — task 1003278.)
|
|
323
|
+
function boardFilterValues() {
|
|
324
|
+
const val = (id) => ($(id) ? $(id).value : '');
|
|
325
|
+
return {
|
|
326
|
+
search: searchQuery(),
|
|
327
|
+
version: val('#f-version'),
|
|
328
|
+
goal: val('#f-goal'),
|
|
329
|
+
kind: val('#f-kind'),
|
|
330
|
+
discipline: val('#f-discipline'),
|
|
331
|
+
newcomer: val('#f-newcomer'),
|
|
332
|
+
creator: val('#f-creator'),
|
|
333
|
+
stale: val('#f-stale'),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Does one task survive the current lenses? Pure over (task, values) so both
|
|
338
|
+
// feeds narrow alike and a new filter cannot reach one tab alone.
|
|
339
|
+
function matchesBoardFilters(t, f) {
|
|
340
|
+
if (!matchesSearch(t, f.search)) return false;
|
|
341
|
+
if (f.version && t.version_id !== f.version) return false;
|
|
342
|
+
// task 1755: goal filter. '__none__' isolates tasks with no goal.
|
|
343
|
+
if (f.goal === '__none__' && t.goal_id != null) return false;
|
|
344
|
+
if (f.goal && f.goal !== '__none__' && String(t.goal_id) !== f.goal) return false;
|
|
345
|
+
if (f.kind && t.kind !== f.kind) return false;
|
|
346
|
+
if (f.discipline && (t.discipline || 'unclassified') !== f.discipline) return false;
|
|
347
|
+
if (f.newcomer === 'yes' && !t.newcomer_friendly) return false;
|
|
348
|
+
// Creator + Age were applied by the SERVER, so they are re-checked here only
|
|
349
|
+
// as a belt on a stale render — cheap, and it keeps the view honest if a poll
|
|
350
|
+
// lands between a control change and its re-fetch.
|
|
351
|
+
if (f.creator === '__none__' && t.created_by != null) return false;
|
|
352
|
+
if (f.creator && f.creator !== '__none__' && String(t.created_by) !== f.creator) return false;
|
|
353
|
+
if (f.stale && !isStale(taskActivityMs(t), f.stale)) return false;
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// task 1756: run-mode — a task is "parallel-safe" when its touches[] don't
|
|
358
|
+
// overlap any OTHER task in the visible set. Set-RELATIVE, which is why it runs
|
|
359
|
+
// after matchesBoardFilters rather than inside it: it answers "of what I'm
|
|
360
|
+
// looking at, what can run together." O(n²) over touches — fine at board scale.
|
|
361
|
+
function applyRunMode(list, fitFilter) {
|
|
362
|
+
if (fitFilter !== 'parallel' && fitFilter !== 'conflict') return list;
|
|
363
|
+
return list.filter((t) => {
|
|
364
|
+
const conflicts = list.some((o) => o !== t && touchesConflict(t.touches, o.touches));
|
|
365
|
+
return fitFilter === 'parallel' ? !conflicts : conflicts;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// task 1003828 — the bar sits above the four panels now, so it must say which it
|
|
370
|
+
// speaks for. Ready and Backlog are the QUEUE tabs; In Progress (live claims)
|
|
371
|
+
// and Completed (shipped history) render other objects, so the bar hides there
|
|
372
|
+
// rather than offering a control that does nothing — this task's whole defect.
|
|
373
|
+
const FILTERED_TABS = new Set(['ready', 'backlog']);
|
|
374
|
+
function syncFilterBarVisibility(tabId) {
|
|
375
|
+
const bar = $('#work-filters');
|
|
376
|
+
if (bar) bar.hidden = !FILTERED_TABS.has(String(tabId));
|
|
377
|
+
}
|
|
378
|
+
|
|
292
379
|
function renderClaimable() {
|
|
293
380
|
const root = $('#task-list');
|
|
294
381
|
if (!root) return;
|
|
295
|
-
const
|
|
296
|
-
const gFilter = $('#f-goal') ? $('#f-goal').value : '';
|
|
297
|
-
const kFilter = $('#f-kind').value;
|
|
298
|
-
const dFilter = $('#f-discipline').value;
|
|
299
|
-
const nFilter = $('#f-newcomer') ? $('#f-newcomer').value : '';
|
|
300
|
-
// task 1003278 — creator + staleness. '__none__' on Creator isolates the
|
|
301
|
-
// unattributed (created before tasks.created_by existed, or by a system path)
|
|
302
|
-
// — the goals owner filter's 'system / unattributed' convention.
|
|
303
|
-
const cFilter = $('#f-creator') ? $('#f-creator').value : '';
|
|
304
|
-
const staleFilter = $('#f-stale') ? $('#f-stale').value : '';
|
|
382
|
+
const filters = boardFilterValues();
|
|
305
383
|
const sortBy = $('#f-sort').value;
|
|
306
|
-
const sQuery = searchQuery();
|
|
307
|
-
|
|
308
|
-
let list = taskState.all.filter((t) => {
|
|
309
|
-
if (!matchesSearch(t, sQuery)) return false;
|
|
310
|
-
if (vFilter && t.version_id !== vFilter) return false;
|
|
311
|
-
// task 1755: goal filter. '__none__' isolates tasks with no goal.
|
|
312
|
-
if (gFilter === '__none__' && t.goal_id != null) return false;
|
|
313
|
-
if (gFilter && gFilter !== '__none__' && String(t.goal_id) !== gFilter) return false;
|
|
314
|
-
if (kFilter && t.kind !== kFilter) return false;
|
|
315
|
-
if (dFilter && (t.discipline || 'unclassified') !== dFilter) return false;
|
|
316
|
-
if (nFilter === 'yes' && !t.newcomer_friendly) return false;
|
|
317
|
-
// Creator + Age were applied by the SERVER (serverFilterQuery), so they are
|
|
318
|
-
// re-checked here only as a belt on a stale render — never as the primary
|
|
319
|
-
// filter. Re-applying them is cheap and keeps the view honest if a poll
|
|
320
|
-
// lands between a control change and its re-fetch.
|
|
321
|
-
if (cFilter === '__none__' && t.created_by != null) return false;
|
|
322
|
-
if (cFilter && cFilter !== '__none__' && String(t.created_by) !== cFilter) return false;
|
|
323
|
-
if (staleFilter && !isStale(taskActivityMs(t), staleFilter)) return false;
|
|
324
|
-
return true;
|
|
325
|
-
});
|
|
326
384
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// already-filtered list so it answers "of what I'm looking at, what can run
|
|
331
|
-
// together." O(n²) over touches — fine at board scale.
|
|
332
|
-
const fitFilter = $('#f-fit') ? $('#f-fit').value : '';
|
|
333
|
-
if (fitFilter === 'parallel' || fitFilter === 'conflict') {
|
|
334
|
-
const base = list;
|
|
335
|
-
list = base.filter((t) => {
|
|
336
|
-
const conflicts = base.some((o) => o !== t && touchesConflict(t.touches, o.touches));
|
|
337
|
-
return fitFilter === 'parallel' ? !conflicts : conflicts;
|
|
338
|
-
});
|
|
339
|
-
}
|
|
385
|
+
let list = taskState.all.filter((t) => matchesBoardFilters(t, filters));
|
|
386
|
+
|
|
387
|
+
list = applyRunMode(list, $('#f-fit') ? $('#f-fit').value : '');
|
|
340
388
|
|
|
341
389
|
if (sortBy === 'cpm') {
|
|
342
390
|
list = list.slice().sort((a, b) => creditsPerMinute(b) - creditsPerMinute(a));
|
|
@@ -508,7 +556,10 @@
|
|
|
508
556
|
if (!sel) return;
|
|
509
557
|
const cur = sel.value;
|
|
510
558
|
const titles = taskState.goalTitles || {};
|
|
511
|
-
|
|
559
|
+
// task 1003828 — options come from BOTH feeds: built from the claimable one
|
|
560
|
+
// alone, a goal whose only work sits at backlog was unofferable.
|
|
561
|
+
const rows = [...taskState.all, ...(taskState.backlog || [])];
|
|
562
|
+
const ids = [...new Set(rows.map((t) => t.goal_id).filter((x) => x != null).map(String))];
|
|
512
563
|
ids.sort((a, b) => String(titles[a] || `Goal #${a}`).localeCompare(String(titles[b] || `Goal #${b}`)));
|
|
513
564
|
sel.length = 1; // keep the leading "— all goals —" option
|
|
514
565
|
for (const id of ids) {
|
|
@@ -517,7 +568,7 @@
|
|
|
517
568
|
opt.textContent = titles[id] || `Goal #${id}`;
|
|
518
569
|
sel.appendChild(opt);
|
|
519
570
|
}
|
|
520
|
-
if (
|
|
571
|
+
if (rows.some((t) => t.goal_id == null)) {
|
|
521
572
|
const opt = document.createElement('option');
|
|
522
573
|
opt.value = '__none__';
|
|
523
574
|
opt.textContent = 'No goal';
|
|
@@ -1055,17 +1106,44 @@
|
|
|
1055
1106
|
root.innerHTML = '<div class="profile__placeholder">Nothing in the backlog right now.</div>';
|
|
1056
1107
|
return;
|
|
1057
1108
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1109
|
+
// task 1003828 — the SAME lenses the Ready tab uses. This read used to be
|
|
1110
|
+
// search-only, so six controls silently did nothing on the tab holding the
|
|
1111
|
+
// most rows. The server already narrowed this feed; re-applying the predicate
|
|
1112
|
+
// answers the two client-only lenses (search, Goal's '__none__') and keeps the
|
|
1113
|
+
// view honest between a control change and its re-fetch.
|
|
1114
|
+
const filters = boardFilterValues();
|
|
1115
|
+
let list = backlogRows.filter((t) => matchesBoardFilters(t, filters));
|
|
1116
|
+
list = applyRunMode(list, $('#f-fit') ? $('#f-fit').value : '');
|
|
1060
1117
|
const countEl = $('#backlog-count');
|
|
1061
1118
|
if (countEl) countEl.textContent = `${list.length} ${list.length === 1 ? 'task' : 'tasks'}`;
|
|
1119
|
+
if (!list.length) {
|
|
1120
|
+
root.innerHTML = '<div class="profile__placeholder">No backlog tasks match these filters. Try widening them.</div>';
|
|
1121
|
+
pagers.backlog.page = 1;
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1062
1124
|
const shown = paginate(list, pagers.backlog);
|
|
1063
1125
|
root.innerHTML = shown.map((t) => {
|
|
1064
1126
|
const ver = t.version_id ? `<span class="task__tag">${escapeHtml(t.version_id)}</span>` : '';
|
|
1127
|
+
// task 1003828 — the row says WHOSE CRAFT and WHICH GOAL. A Ready card has
|
|
1128
|
+
// carried its discipline tag since 1755; a backlog row carried neither, so a
|
|
1129
|
+
// correctly-goaled task (an auto-filed artist review, say) read as
|
|
1130
|
+
// uncategorised in the one place a person goes looking for it. Same
|
|
1131
|
+
// vocabulary as the card, so the two tabs describe a task alike.
|
|
1132
|
+
const discipline = t.discipline || 'unclassified';
|
|
1133
|
+
const disciplineTag = discipline !== 'unclassified'
|
|
1134
|
+
? `<span class="task__tag">${escapeHtml(discipline)}</span>`
|
|
1135
|
+
: '';
|
|
1136
|
+
// The goal's TITLE, from the map the Goal filter already loads, falling back
|
|
1137
|
+
// to its id before that resolves. A goal-less row says so rather than
|
|
1138
|
+
// rendering a gap — "no goal" is a real, findable state.
|
|
1139
|
+
const goalTitle = t.goal_id != null
|
|
1140
|
+
? ((taskState.goalTitles || {})[String(t.goal_id)] || `Goal #${t.goal_id}`)
|
|
1141
|
+
: 'No goal';
|
|
1142
|
+
const goalTag = `<span class="history__goal" title="${escapeHtml(`Goal: ${goalTitle}`)}">${escapeHtml(goalTitle)}</span>`;
|
|
1065
1143
|
return `
|
|
1066
1144
|
<div class="history__row" data-task-id="${escapeHtml(String(t.id))}" title="Open task #${escapeHtml(String(t.id))}">
|
|
1067
1145
|
<span class="history__id">#${escapeHtml(String(t.id))}</span>
|
|
1068
|
-
<span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver} ${rankTagHtml(t.requires_rank)}</span>
|
|
1146
|
+
<span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver} ${disciplineTag} ${rankTagHtml(t.requires_rank)}<br>${goalTag}</span>
|
|
1069
1147
|
<span class="history__when">${escapeHtml(t.kind || '')}</span>
|
|
1070
1148
|
</div>`;
|
|
1071
1149
|
}).join('');
|
|
@@ -1182,6 +1260,10 @@
|
|
|
1182
1260
|
taskState.goalTitles[g.id] = g.title;
|
|
1183
1261
|
taskState.goalCategories[g.id] = g.category || null;
|
|
1184
1262
|
});
|
|
1263
|
+
// task 1003828 — loadBacklog() fires ABOVE this await, so its first paint
|
|
1264
|
+
// has no titles and every row would read "Goal #1000089" until something
|
|
1265
|
+
// else repainted it. Harmless if the feed has not landed yet.
|
|
1266
|
+
renderBacklog();
|
|
1185
1267
|
} catch (_) { /* filter labels fall back to 'Goal #id' */ }
|
|
1186
1268
|
|
|
1187
1269
|
// Claimable + active claims in one call. A 403 means the primer gate — show
|
|
@@ -1242,7 +1324,10 @@
|
|
|
1242
1324
|
{ id: 'backlog', label: 'Backlog', panel: 'backlog-scroll' },
|
|
1243
1325
|
{ id: 'completed', label: 'Completed', panel: 'history-scroll' },
|
|
1244
1326
|
],
|
|
1245
|
-
onChange: () => {
|
|
1327
|
+
onChange: (id) => {
|
|
1328
|
+
if (selectionTotal()) clearSelection();
|
|
1329
|
+
syncFilterBarVisibility(id);
|
|
1330
|
+
},
|
|
1246
1331
|
});
|
|
1247
1332
|
|
|
1248
1333
|
// task 2170: filter diet — Search/Sort/Version primary, the rest behind
|
|
@@ -1266,6 +1351,16 @@
|
|
|
1266
1351
|
],
|
|
1267
1352
|
},
|
|
1268
1353
|
{ id: 'f-version', label: 'Version', options: [{ value: '', label: '— any —' }] },
|
|
1354
|
+
// task 1003828 — Discipline is PRIMARY, not overflow. A craft is the axis
|
|
1355
|
+
// this platform organises people by (CLAUDE.md's role packs; a task's work
|
|
1356
|
+
// type IS its discipline), so "show me my craft's work" is a first-class
|
|
1357
|
+
// question, not one to find behind a disclosure. The mixed option form is
|
|
1358
|
+
// kit-supported — pinned by tests/goal_sort_staleness.mjs against this id.
|
|
1359
|
+
{
|
|
1360
|
+
id: 'f-discipline',
|
|
1361
|
+
label: 'Discipline',
|
|
1362
|
+
options: [{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui', 'unclassified'],
|
|
1363
|
+
},
|
|
1269
1364
|
],
|
|
1270
1365
|
overflow: [
|
|
1271
1366
|
{ id: 'f-goal', label: 'Goal', options: [{ value: '', label: '— all goals —' }] },
|
|
@@ -1276,11 +1371,6 @@
|
|
|
1276
1371
|
'feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture',
|
|
1277
1372
|
'cleanup', 'decision', 'blocker-resolution', 'unclassified'],
|
|
1278
1373
|
},
|
|
1279
|
-
{
|
|
1280
|
-
id: 'f-discipline',
|
|
1281
|
-
label: 'Discipline',
|
|
1282
|
-
options: [{ value: '', label: '— any —' }, 'engineer', 'artist', 'ideator', 'ui', 'unclassified'],
|
|
1283
|
-
},
|
|
1284
1374
|
{
|
|
1285
1375
|
id: 'f-newcomer',
|
|
1286
1376
|
label: 'Newcomer',
|
|
@@ -1310,18 +1400,23 @@
|
|
|
1310
1400
|
// changed id (hall-kit.js:269), and comparing the query is the direct
|
|
1311
1401
|
// question anyway — a control that does not appear in it can never cost
|
|
1312
1402
|
// a round-trip, which is what keeps the other filters instant.
|
|
1403
|
+
//
|
|
1404
|
+
// task 1003828 — decided SEPARATELY per feed now, because the backlog asks
|
|
1405
|
+
// the server more than claimable can answer. Each side re-fetches (ending
|
|
1406
|
+
// in its own render) or re-renders in place; the old early return left the
|
|
1407
|
+
// other tab's client-side lenses unapplied until the next poll.
|
|
1313
1408
|
const q = serverFilterQuery();
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
loadBacklog();
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
renderClaimable();
|
|
1321
|
-
renderBacklog();
|
|
1409
|
+
const bq = backlogFilterQuery();
|
|
1410
|
+
if (q !== lastServerFilterQuery) { lastServerFilterQuery = q; refreshLive(); } else { renderClaimable(); }
|
|
1411
|
+
if (bq !== lastBacklogQuery) { lastBacklogQuery = bq; loadBacklog(); } else { renderBacklog(); }
|
|
1322
1412
|
},
|
|
1323
1413
|
});
|
|
1324
1414
|
|
|
1415
|
+
// task 1003828 — makeTabs fires onChange only on a CHANGE and restores a
|
|
1416
|
+
// deep-linked tab before this point, so the opening state is set explicitly:
|
|
1417
|
+
// without it, a reload onto #completed shows a filter bar over ship history.
|
|
1418
|
+
syncFilterBarVisibility(workTabs && workTabs.active ? workTabs.active() : 'ready');
|
|
1419
|
+
|
|
1325
1420
|
$('#task-list')?.addEventListener('click', (e) => {
|
|
1326
1421
|
// task 1759: the multi-select checkbox. Toggle the Set + the card's
|
|
1327
1422
|
// highlight class directly (cheaper than a full renderClaimable) and
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.661",
|
|
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.661",
|
|
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.661",
|
|
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/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.661'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -202,7 +202,18 @@ await test('a server-side filter change RE-FETCHES; every other filter stays ins
|
|
|
202
202
|
// direct question anyway, and means a control absent from that query can never
|
|
203
203
|
// cost a round-trip.
|
|
204
204
|
assert.match(WORK_SRC, /let lastServerFilterQuery = '';/);
|
|
205
|
-
|
|
205
|
+
// task 1003828 split this into ONE DECISION PER FEED. It used to be a single
|
|
206
|
+
// comparison that re-fetched both and returned early — which was fine while the
|
|
207
|
+
// two asked the server the same question, and wrong once the backlog started
|
|
208
|
+
// asking a narrower one (version/kind/discipline/goal_id, which GET /tasks
|
|
209
|
+
// answers and GET /tasks/claimable does not). The early return also meant a
|
|
210
|
+
// change to a CLIENT-side lens skipped the other tab's re-render until the next
|
|
211
|
+
// poll repainted it. Each side now re-fetches or re-renders on its own; the
|
|
212
|
+
// property under test is unchanged — a server-facing change costs a round-trip
|
|
213
|
+
// and nothing else does.
|
|
214
|
+
assert.match(WORK_SRC, /let lastBacklogQuery = '';/);
|
|
215
|
+
assert.match(WORK_SRC, /if \(q !== lastServerFilterQuery\) \{ lastServerFilterQuery = q; refreshLive\(\); \} else \{ renderClaimable\(\); \}/);
|
|
216
|
+
assert.match(WORK_SRC, /if \(bq !== lastBacklogQuery\) \{ lastBacklogQuery = bq; loadBacklog\(\); \} else \{ renderBacklog\(\); \}/);
|
|
206
217
|
const kit = read('modules/hall-ui/public/hall-kit.js');
|
|
207
218
|
assert.match(kit, /if \(onChange\) onChange\(values\(\)\);/,
|
|
208
219
|
'the contract this decision depends on — if it ever hands the changed id instead, revisit');
|
|
@@ -471,3 +471,106 @@ test('the tab strip is left alone — it scrolls in its own row and never moved
|
|
|
471
471
|
assert.ok(strip, 'the tab strip rule is present');
|
|
472
472
|
assert.match(strip[1], /overflow-x:\s*auto/, 'it scrolls itself, which is why it is not the overflow cause');
|
|
473
473
|
});
|
|
474
|
+
|
|
475
|
+
// ---- the board's filters drive BOTH queue tabs (task 1003828) ---------------
|
|
476
|
+
//
|
|
477
|
+
// The owner reported an auto-filed artist review as "going into uncategorized
|
|
478
|
+
// BONGOS-V2 backlog tasks" and asked for tasks to be "filterable by role type".
|
|
479
|
+
// The review was never uncategorised — cascade.js inherits its parent's goal —
|
|
480
|
+
// but the Backlog tab it lands on could not say so and could not be narrowed:
|
|
481
|
+
//
|
|
482
|
+
// * renderBacklog() applied the SEARCH BOX ONLY. Version, Goal, Kind,
|
|
483
|
+
// Discipline, Newcomer and Run mode were read inside renderClaimable and
|
|
484
|
+
// nowhere else, so six controls were silently inert over the tab holding the
|
|
485
|
+
// most rows. Nothing errored; the list simply did not change.
|
|
486
|
+
// * The filter bar was INSIDE #claim-scroll, which makeTabs hides. So on the
|
|
487
|
+
// Backlog tab those controls were not merely inert — they were off-screen.
|
|
488
|
+
// * A backlog row rendered id + title + version + rank + kind. No goal, no
|
|
489
|
+
// discipline. A correctly-goaled task genuinely read as uncategorised.
|
|
490
|
+
//
|
|
491
|
+
// Each is invisible in a browser the way this file's other entries are: the page
|
|
492
|
+
// renders, it just renders a control that does nothing.
|
|
493
|
+
|
|
494
|
+
const workJs = read('work.js');
|
|
495
|
+
|
|
496
|
+
test('the filter bar is above the panels, not inside the Ready one that gets hidden', () => {
|
|
497
|
+
const filters = workHtml.indexOf('id="work-filters"');
|
|
498
|
+
const tabs = workHtml.indexOf('id="work-tabs"');
|
|
499
|
+
const claim = workHtml.indexOf('id="claim-scroll"');
|
|
500
|
+
assert.ok(filters > 0 && tabs > 0 && claim > 0, 'all three mounts present');
|
|
501
|
+
assert.ok(filters > tabs, 'the bar follows the tab strip it belongs to');
|
|
502
|
+
assert.ok(filters < claim,
|
|
503
|
+
'and precedes the Ready panel — inside it, makeTabs hides the filters with the tab');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test('the bar shows on the two QUEUE tabs and hides on the two that it cannot narrow', () => {
|
|
507
|
+
assert.match(workJs, /const FILTERED_TABS = new Set\(\['ready', 'backlog'\]\)/,
|
|
508
|
+
'Ready and Backlog are task feeds; In Progress and Completed render other objects');
|
|
509
|
+
assert.match(workJs, /function syncFilterBarVisibility/);
|
|
510
|
+
// Wired BOTH ways: on a tab change, and once at boot — makeTabs restores a
|
|
511
|
+
// deep-linked tab (/work#completed) before the bar is built, and fires
|
|
512
|
+
// onChange only on a CHANGE, so opening state has to be set explicitly.
|
|
513
|
+
assert.match(workJs, /onChange: \(id\) => \{[\s\S]{0,160}syncFilterBarVisibility\(id\)/,
|
|
514
|
+
'a tab change re-syncs the bar');
|
|
515
|
+
assert.match(workJs, /syncFilterBarVisibility\(workTabs && workTabs\.active \? workTabs\.active\(\) : 'ready'\)/,
|
|
516
|
+
'and the opening tab decides the opening state');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('both queue tabs narrow through ONE predicate, so a filter cannot reach only one', () => {
|
|
520
|
+
assert.match(workJs, /function matchesBoardFilters\(t, f\)/,
|
|
521
|
+
'the predicate is shared, not copied per tab');
|
|
522
|
+
// Both renderers must consult it. This is the whole defect: before, only the
|
|
523
|
+
// claimable one did.
|
|
524
|
+
const claimable = workJs.slice(workJs.indexOf('function renderClaimable'), workJs.indexOf('async function populateCreatorFilter'));
|
|
525
|
+
const backlog = workJs.slice(workJs.indexOf('function renderBacklog'), workJs.indexOf('async function refreshLive'));
|
|
526
|
+
for (const [name, body] of [['renderClaimable', claimable], ['renderBacklog', backlog]]) {
|
|
527
|
+
assert.ok(body.length, `${name} body located`);
|
|
528
|
+
assert.match(body, /matchesBoardFilters\(t, filters\)/, `${name} narrows through the shared predicate`);
|
|
529
|
+
assert.match(body, /applyRunMode\(list, \$\('#f-fit'\)/, `${name} applies the set-relative run-mode lens too`);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('a backlog row names its craft and its goal', () => {
|
|
534
|
+
const backlog = workJs.slice(workJs.indexOf('function renderBacklog'));
|
|
535
|
+
// The same vocabulary the Ready card has carried since 1755 — a second spelling
|
|
536
|
+
// would let the two tabs describe one task differently.
|
|
537
|
+
assert.match(backlog, /const discipline = t\.discipline \|\| 'unclassified'/);
|
|
538
|
+
assert.match(backlog, /disciplineTag/, 'the row renders the discipline tag');
|
|
539
|
+
assert.match(backlog, /taskState\.goalTitles \|\| \{\}/, 'the goal is named by TITLE, not by bare id');
|
|
540
|
+
assert.match(backlog, /'No goal'/, 'and a goal-less row says so rather than rendering a gap');
|
|
541
|
+
assert.ok(rule(workCss, '.history__goal'), '.history__goal has a rule — an unstyled sub-line inherits the title size');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('the backlog asks the SERVER the narrow question, because its read is capped', () => {
|
|
545
|
+
// A client-side-only narrow of a capped read LIES: filtering the 500 fetched
|
|
546
|
+
// rows to the artist ones hides the artist tasks that sat at row 501.
|
|
547
|
+
assert.match(workJs, /function backlogFilterQuery/);
|
|
548
|
+
const q = workJs.slice(workJs.indexOf('function backlogFilterQuery'), workJs.indexOf('BACKLOG_LIMIT ='));
|
|
549
|
+
for (const param of ['version=', 'kind=', 'discipline=', 'goal_id=']) {
|
|
550
|
+
assert.ok(q.includes(param), `the backlog read sends ${param} — GET /tasks answers it`);
|
|
551
|
+
}
|
|
552
|
+
// '__none__' (ungoaled) has no server predicate, so it must not be sent.
|
|
553
|
+
assert.match(q, /f\.goal && f\.goal !== '__none__'/,
|
|
554
|
+
"the client-only 'No goal' lens is never sent to the route");
|
|
555
|
+
// It reads through boardFilterValues rather than re-querying the DOM, so the
|
|
556
|
+
// query and the client predicate can never disagree about a control's value.
|
|
557
|
+
assert.match(q, /const f = boardFilterValues\(\);/);
|
|
558
|
+
// And it stays SEPARATE from the claimable query: GET /tasks/claimable answers
|
|
559
|
+
// only version + discipline, so kind/goal_id there would be a silent no-op.
|
|
560
|
+
const claimQ = workJs.slice(workJs.indexOf('function serverFilterQuery'), workJs.indexOf('task 1003828 — the BACKLOG'));
|
|
561
|
+
assert.ok(!claimQ.includes('goal_id='), 'the claimable feed is not sent goal_id');
|
|
562
|
+
assert.ok(!claimQ.includes('kind='), 'nor kind');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('each feed decides its own re-fetch, so a server change cannot strand the other tab', () => {
|
|
566
|
+
// The old shape compared ONE query and returned early, which left the other
|
|
567
|
+
// tab's client-side lenses unapplied until the next poll happened to repaint.
|
|
568
|
+
assert.match(workJs, /if \(q !== lastServerFilterQuery\) \{ lastServerFilterQuery = q; refreshLive\(\); \} else \{ renderClaimable\(\); \}/);
|
|
569
|
+
assert.match(workJs, /if \(bq !== lastBacklogQuery\) \{ lastBacklogQuery = bq; loadBacklog\(\); \} else \{ renderBacklog\(\); \}/);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('the Goal filter offers goals that only have BACKLOG work', () => {
|
|
573
|
+
const populate = workJs.slice(workJs.indexOf('function populateGoalFilter'), workJs.indexOf('async function copyPromptForTask'));
|
|
574
|
+
assert.match(populate, /\[\.\.\.taskState\.all, \.\.\.\(taskState\.backlog \|\| \[\]\)\]/,
|
|
575
|
+
'built from the claimable feed alone, a backlog-only goal was unofferable');
|
|
576
|
+
});
|