@bongos/core 1.19.879 → 1.19.880
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 +23 -18
- package/docs/copy-inventory.md +174 -174
- package/docs/copy-registry.json +206 -206
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/dom-utils.js +18 -2
- package/modules/hall-ui/public/goals-render.js +8 -3
- package/modules/hall-ui/public/task.js +6 -2
- package/modules/hall-ui/public/work.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_record_world.mjs +5 -0
- package/tests/kind_axis_unset.mjs +195 -0
|
@@ -2232,5 +2232,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2232
2232
|
landed since 1.19.877 with no explicit bump. run 35439004814. (task 1002620)
|
|
2233
2233
|
1.19.879 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2234
2234
|
landed since 1.19.878 with no explicit bump. run 35439529685. (task 1002620)
|
|
2235
|
+
1.19.880 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2236
|
+
landed since 1.19.879 with no explicit bump. run 35453118533. (task 1002620)
|
|
2235
2237
|
---------------------------------------------------------------------------
|
|
2236
2238
|
```
|
|
@@ -146,7 +146,10 @@
|
|
|
146
146
|
for (const k of list) {
|
|
147
147
|
const opt = document.createElement('option');
|
|
148
148
|
opt.value = k;
|
|
149
|
-
|
|
149
|
+
// The unset bucket is named for a reader on EVERY surface, filters included
|
|
150
|
+
// — `label` is the caller's choice for the real kinds, never for this one
|
|
151
|
+
// (task 1004038). roleOptions() names the role twin the same way.
|
|
152
|
+
opt.textContent = kindIsUnset(k) ? kindLabel(k) : label(k);
|
|
150
153
|
sel.appendChild(opt);
|
|
151
154
|
}
|
|
152
155
|
// A refill must not drop the reader's pick; `selected` is the filing default.
|
|
@@ -154,10 +157,23 @@
|
|
|
154
157
|
if (want) sel.value = want;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
// A task with no kind carries the DB default 'unclassified' — the twin of
|
|
161
|
+
// ROLE_UNSET one axis over (task 1004038, filed by task 1003936 which fixed the
|
|
162
|
+
// role axis and left this one). A word a column chose is not a word a person
|
|
163
|
+
// should read (docs/voice.md V2), so a picker shows "No kind yet" and a row tag
|
|
164
|
+
// renders NOTHING. kindIsUnset() is how a caller asks. The STORED value and the
|
|
165
|
+
// filter VALUE stay 'unclassified'; only what is read changes.
|
|
166
|
+
const KIND_UNSET = 'unclassified';
|
|
167
|
+
function kindIsUnset(k) { return !k || String(k) === KIND_UNSET; }
|
|
168
|
+
|
|
169
|
+
// NOT ESCAPE-SAFE: like roleLabel() below, this returns a transformed copy of
|
|
170
|
+
// its input, so a caller rendering it into markup must escapeHtml() the result.
|
|
171
|
+
// Every caller today assigns it to textContent, which escapes for them.
|
|
157
172
|
// How a kind is named in a picker — 'blocker-resolution' → 'Blocker resolution'.
|
|
158
173
|
// The same shape roleLabel takes below, and it reproduces every label the
|
|
159
174
|
// hand-written <option> list in ideas.html carried, so no copy changes.
|
|
160
175
|
function kindLabel(k) {
|
|
176
|
+
if (kindIsUnset(k)) return 'No kind yet';
|
|
161
177
|
const s = String(k || '').replace(/-/g, ' ');
|
|
162
178
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
163
179
|
}
|
|
@@ -407,7 +423,7 @@
|
|
|
407
423
|
escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
408
424
|
// task 1003936 — ONE word for the role axis, all six surfaces
|
|
409
425
|
roleAxisLabel, roleLabel, roleOptions, roleIsUnset, ROLE_UNSET,
|
|
410
|
-
fillKindFilter, kindLabel, // task 1003453 — one Kind filler, all three surfaces
|
|
426
|
+
fillKindFilter, kindLabel, kindIsUnset, KIND_UNSET, // task 1003453 — one Kind filler, all three surfaces
|
|
411
427
|
errCode, errMessage, errDetails, // task 1002462 — ADR 0116 envelope readers
|
|
412
428
|
contributeSettingsPanel, settingsPanels,
|
|
413
429
|
contributeHallWidget, hallWidgets, hallWidgetIds,
|
|
@@ -321,6 +321,11 @@
|
|
|
321
321
|
const disciplineTag = window.OTB.roleIsUnset(t.discipline)
|
|
322
322
|
? ''
|
|
323
323
|
: `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
|
|
324
|
+
// A kind nobody set is no tag at all, not the column's default word — the
|
|
325
|
+
// role tag above is the precedent this follows (task 1004038).
|
|
326
|
+
const kindTag = window.OTB.kindIsUnset(t.kind)
|
|
327
|
+
? ''
|
|
328
|
+
: `<span class="task__tag">${escapeHtml(t.kind)}</span>`;
|
|
324
329
|
const newcomerTag = t.newcomer_friendly ? '<span class="task__tag task__tag--newcomer">newcomer</span>' : '';
|
|
325
330
|
const rankTag = rankTagHtml(t.requires_rank);
|
|
326
331
|
const touches = Array.isArray(t.touches) ? t.touches : [];
|
|
@@ -361,7 +366,7 @@
|
|
|
361
366
|
<p class="task__title"><a class="task__link" href="${escapeHtml(taskHref(id))}">${escapeHtml(t.title || '')}</a></p>
|
|
362
367
|
<div class="task__meta">
|
|
363
368
|
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
364
|
-
|
|
369
|
+
${kindTag}
|
|
365
370
|
${disciplineTag}
|
|
366
371
|
${newcomerTag}
|
|
367
372
|
${rankTag}
|
|
@@ -403,7 +408,7 @@
|
|
|
403
408
|
<tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
|
|
404
409
|
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
405
410
|
<td>${escapeHtml(t.title || '')}</td>
|
|
406
|
-
<td>${
|
|
411
|
+
<td>${window.OTB.kindIsUnset(t.kind) ? '—' : escapeHtml(t.kind)}</td>
|
|
407
412
|
<td class="ledger__drachmae">${fmtNum(creditVal(t))}${taskSettled(t) ? '' : ' <span class="ledger__est" title="Estimate — credits settle when the task is confirmed">est.</span>'}</td>
|
|
408
413
|
<td class="ledger__date">${fmtDate(t.shipped_at)}</td>
|
|
409
414
|
</tr>`).join('');
|
|
@@ -502,7 +507,7 @@
|
|
|
502
507
|
const sortBy = $('#f-goal-sort')?.value || 'cpm';
|
|
503
508
|
let list = state.work.todo.filter((t) => {
|
|
504
509
|
if (!matchesTodoSearch(t, sQuery)) return false;
|
|
505
|
-
if (kFilter && (t.kind ||
|
|
510
|
+
if (kFilter && (t.kind || window.OTB.KIND_UNSET) !== kFilter) return false;
|
|
506
511
|
if (dFilter && (t.discipline || window.OTB.ROLE_UNSET) !== dFilter) return false;
|
|
507
512
|
if (nFilter === 'yes' && !t.newcomer_friendly) return false;
|
|
508
513
|
return true;
|
|
@@ -536,8 +536,12 @@
|
|
|
536
536
|
// word is the thing the round's stat-tile reference exists to fix.
|
|
537
537
|
const meta = [];
|
|
538
538
|
if (t.version_id) meta.push(`<b>${escapeHtml(t.version_id)}</b>`);
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
// Neither axis says the word its column defaults to (task 1004038). `if
|
|
540
|
+
// (t.kind)` was never a guard for it: 'unclassified' is a truthy string, so
|
|
541
|
+
// the run read it out loud. The role line below asked the same question with
|
|
542
|
+
// a literal — it is right, but a copy of roleIsUnset() is free to drift.
|
|
543
|
+
if (!window.OTB.kindIsUnset(t.kind)) meta.push(escapeHtml(t.kind));
|
|
544
|
+
if (!window.OTB.roleIsUnset(t.discipline)) meta.push(escapeHtml(t.discipline));
|
|
541
545
|
// rankText() answers "any rank" for an open task, which read as
|
|
542
546
|
// "min rank any rank" once the label was prefixed. An open floor is not a
|
|
543
547
|
// minimum, so it is said as the absence of one.
|
|
@@ -429,6 +429,8 @@
|
|
|
429
429
|
const disciplineTag = window.OTB.roleIsUnset(t.discipline)
|
|
430
430
|
? ''
|
|
431
431
|
: `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
|
|
432
|
+
// A kind nobody set is no tag at all, like the role above (task 1004038).
|
|
433
|
+
const kindTag = window.OTB.kindIsUnset(t.kind) ? '' : `<span class="task__tag">${escapeHtml(t.kind)}</span>`;
|
|
432
434
|
const newcomerTag = t.newcomer_friendly
|
|
433
435
|
? '<span class="task__tag task__tag--newcomer">newcomer</span>'
|
|
434
436
|
: '';
|
|
@@ -459,7 +461,7 @@
|
|
|
459
461
|
<p class="task__title"><a class="task__link" href="${escapeHtml(taskHref(t.id))}">${escapeHtml(t.title)}</a></p>
|
|
460
462
|
<div class="task__meta">
|
|
461
463
|
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
462
|
-
|
|
464
|
+
${kindTag}
|
|
463
465
|
${disciplineTag}
|
|
464
466
|
${newcomerTag}
|
|
465
467
|
${rankTag}
|
|
@@ -1142,7 +1144,7 @@
|
|
|
1142
1144
|
<div class="history__row" data-task-id="${escapeHtml(String(t.id))}" title="Open task #${escapeHtml(String(t.id))}">
|
|
1143
1145
|
<span class="history__id">#${escapeHtml(String(t.id))}</span>
|
|
1144
1146
|
<span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver} ${disciplineTag} ${rankTagHtml(t.requires_rank)}<br>${goalTag}</span>
|
|
1145
|
-
<span class="history__when">${
|
|
1147
|
+
<span class="history__when">${window.OTB.kindIsUnset(t.kind) ? '' : escapeHtml(t.kind)}</span>
|
|
1146
1148
|
</div>`;
|
|
1147
1149
|
}).join('');
|
|
1148
1150
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.880",
|
|
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.880",
|
|
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.880",
|
|
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.880'; // 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');
|
|
@@ -354,6 +354,11 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
354
354
|
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
355
355
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
356
356
|
ROLE_UNSET: 'unclassified',
|
|
357
|
+
// task 1004038 — the kind axis asks the same question. Stubbed for the
|
|
358
|
+
// reason the comment above gives: an undefined helper throws mid-render,
|
|
359
|
+
// and the page then renders its "feed unavailable" card instead.
|
|
360
|
+
kindIsUnset: (k) => !k || k === 'unclassified',
|
|
361
|
+
KIND_UNSET: 'unclassified',
|
|
357
362
|
escapeHtml,
|
|
358
363
|
$: (sel) => documentObj.querySelector(sel),
|
|
359
364
|
fmtDate: (iso) => String(iso).slice(0, 10),
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// tests/kind_axis_unset.mjs — the KIND axis never reads its column default to a
|
|
2
|
+
// person (task 1004038, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// The twin of the role axis task 1003936 fixed. `tasks.kind` defaults to
|
|
5
|
+
// 'unclassified'; docs/voice.md V2 says a value that exists because a column
|
|
6
|
+
// needed a default is not a word a person should read. Before this, the word
|
|
7
|
+
// reached a reader on four surfaces: the Goals page row tag, the Tasks board row
|
|
8
|
+
// tag, the backlog history row, and every Kind filter's option list.
|
|
9
|
+
//
|
|
10
|
+
// The line this holds — the same one 1003936 drew: the STORED value and the
|
|
11
|
+
// FILTER VALUE stay 'unclassified'. Only what is READ changes. A test that let
|
|
12
|
+
// the value change would be pinning the wrong half.
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
23
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
24
|
+
|
|
25
|
+
// The real dom-utils.js, booted the way the pages boot it, so these are the
|
|
26
|
+
// functions that ship and not a restatement of them.
|
|
27
|
+
function loadOTB() {
|
|
28
|
+
const mkEl = () => ({
|
|
29
|
+
value: '', textContent: '', length: 0, children: [],
|
|
30
|
+
appendChild(c) { this.children.push(c); this.length = this.children.length; },
|
|
31
|
+
setAttribute() {}, addEventListener() {},
|
|
32
|
+
});
|
|
33
|
+
const sandbox = {
|
|
34
|
+
window: { __BRANDING__: {}, __MODULES__: { enabled: {} }, addEventListener() {} },
|
|
35
|
+
document: { createElement: mkEl, getElementById: () => null, querySelector: () => null,
|
|
36
|
+
querySelectorAll: () => [], addEventListener() {} },
|
|
37
|
+
console,
|
|
38
|
+
};
|
|
39
|
+
sandbox.globalThis = sandbox;
|
|
40
|
+
vm.createContext(sandbox);
|
|
41
|
+
vm.runInContext(read('dom-utils.js'), sandbox, { filename: 'dom-utils.js' });
|
|
42
|
+
return { OTB: sandbox.window.OTB };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('the unset kind is asked about, not spelled, and the stored value is untouched', () => {
|
|
46
|
+
const { OTB } = loadOTB();
|
|
47
|
+
assert.equal(OTB.KIND_UNSET, 'unclassified',
|
|
48
|
+
'the STORED value is still the column default — this fix changes reading, not writing');
|
|
49
|
+
assert.equal(OTB.kindIsUnset('unclassified'), true);
|
|
50
|
+
assert.equal(OTB.kindIsUnset(''), true, 'a missing kind is unset too');
|
|
51
|
+
assert.equal(OTB.kindIsUnset(null), true);
|
|
52
|
+
assert.equal(OTB.kindIsUnset(undefined), true);
|
|
53
|
+
assert.equal(OTB.kindIsUnset('bug'), false, 'a real kind is never unset');
|
|
54
|
+
assert.equal(OTB.kindIsUnset('verify'), false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a kind is named for a reader, and the unset one is named a phrase a person would say', () => {
|
|
58
|
+
const { OTB } = loadOTB();
|
|
59
|
+
assert.equal(OTB.kindLabel('unclassified'), 'No kind yet',
|
|
60
|
+
"the column's word never reaches a reader");
|
|
61
|
+
assert.doesNotMatch(OTB.kindLabel('unclassified'), /unclassified/i);
|
|
62
|
+
// The labels task 1003453 shipped are unchanged — this fix adds a case, it
|
|
63
|
+
// does not restyle the vocabulary.
|
|
64
|
+
assert.equal(OTB.kindLabel('bug'), 'Bug');
|
|
65
|
+
assert.equal(OTB.kindLabel('blocker-resolution'), 'Blocker resolution');
|
|
66
|
+
assert.equal(OTB.kindLabel('learning-capture'), 'Learning capture');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('EVERY Kind picker names the unset bucket, filters included — and keeps its value', () => {
|
|
70
|
+
const { OTB } = loadOTB();
|
|
71
|
+
const sel = { value: '', length: 1, options: [],
|
|
72
|
+
appendChild(o) { this.options.push(o); this.length = this.options.length + 1; } };
|
|
73
|
+
// A FILTER: `label` is identity, as work.js and goals-page.js pass it. Before
|
|
74
|
+
// this fix that identity function is exactly what leaked the column's word.
|
|
75
|
+
OTB.fillKindFilter(sel, ['bug', 'verify', 'unclassified'], { keepFirst: 1 });
|
|
76
|
+
const unset = sel.options.find((o) => o.value === 'unclassified');
|
|
77
|
+
assert.ok(unset, 'the unset bucket is still SELECTABLE — a filter you cannot apply is a lost lens');
|
|
78
|
+
assert.equal(unset.value, 'unclassified', 'and it still filters on the stored value');
|
|
79
|
+
assert.equal(unset.textContent, 'No kind yet', 'but it READS as a phrase');
|
|
80
|
+
// The real kinds still honour the caller's label choice.
|
|
81
|
+
assert.equal(sel.options.find((o) => o.value === 'bug').textContent, 'bug',
|
|
82
|
+
"a filter's raw-value labelling for real kinds is unchanged");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ---- the render sites ------------------------------------------------------
|
|
86
|
+
// Source pins, the idiom tests/hall_board_world.mjs uses for the role twin: a
|
|
87
|
+
// row tag is built in a template literal that no unit test can reach without
|
|
88
|
+
// booting the whole page.
|
|
89
|
+
|
|
90
|
+
const goalsRender = read('goals-render.js');
|
|
91
|
+
const workJs = read('work.js');
|
|
92
|
+
const taskJs = read('task.js');
|
|
93
|
+
|
|
94
|
+
// Every `t.kind` USE in the two files is accounted for — the structural form,
|
|
95
|
+
// not a pattern match. task 1004038's first round asserted the literal
|
|
96
|
+
// `t.kind || 'unclassified'` was gone, which was true and still missed
|
|
97
|
+
// taskRowsDone()'s `t.kind || '—'`: a different spelling of the same bug, in a
|
|
98
|
+
// file the diff was already editing. `||` cannot guard this axis AT ALL, because
|
|
99
|
+
// the default is the truthy string 'unclassified' — so the test has to enumerate
|
|
100
|
+
// every use and demand each one either guards or is a declared non-render.
|
|
101
|
+
// Each scanned file split ONCE at module load — the scan below reads these, so
|
|
102
|
+
// adding a case costs no re-read (the per-test split was the round-2 note).
|
|
103
|
+
const scanned = (src) => {
|
|
104
|
+
const all = src.split('\n');
|
|
105
|
+
return { raw: src, all, hits: all.map((l, i) => [l, i]).filter(([l]) => l.includes('t.kind')) };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const USES = {
|
|
109
|
+
'goals-render.js': {
|
|
110
|
+
src: scanned(goalsRender),
|
|
111
|
+
// A use that never reaches a reader, each with the reason it is exempt.
|
|
112
|
+
nonRender: {
|
|
113
|
+
"case 'kind': return String(t.kind || '').toLowerCase();": 'a SORT key — compared, never displayed',
|
|
114
|
+
"if (kFilter && (t.kind || window.OTB.KIND_UNSET) !== kFilter) return false;": 'the filter PREDICATE — must use the stored value',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
'work.js': {
|
|
118
|
+
src: scanned(workJs),
|
|
119
|
+
nonRender: {
|
|
120
|
+
'if (f.kind && t.kind !== f.kind) return false;': 'the board filter PREDICATE — stored value again',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
// Round 2 found the RECORD page reading the word too, via `if (t.kind)` — which
|
|
124
|
+
// is not a guard, because the default is truthy. Enumerating the file here is
|
|
125
|
+
// what stops the next one being found by a grader instead of by this test.
|
|
126
|
+
'task.js': {
|
|
127
|
+
src: scanned(taskJs),
|
|
128
|
+
nonRender: {
|
|
129
|
+
"if (t.kind === 'verify' && !window.confirm(": 'a BRANCH on the value — gates a confirm prompt, renders nothing',
|
|
130
|
+
"const body = t.kind === 'verify' ? { verified: true } : {};": 'a REQUEST body — sent to the route, never displayed',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
test('every t.kind use either goes through kindIsUnset or is a declared non-render', () => {
|
|
136
|
+
for (const [name, { src, nonRender }] of Object.entries(USES)) {
|
|
137
|
+
const { all, hits } = src;
|
|
138
|
+
assert.ok(hits.length, `${name} has t.kind uses to check`);
|
|
139
|
+
for (const [raw, i] of hits) {
|
|
140
|
+
const line = raw.trim();
|
|
141
|
+
if (line.startsWith('//') || line.startsWith('*')) continue; // prose about the axis
|
|
142
|
+
if (Object.keys(nonRender).some((k) => line.includes(k.trim()))) continue;
|
|
143
|
+
// The guard may be a multi-line ternary whose `?`/`:` arms sit on their own
|
|
144
|
+
// lines (goals-render.js writes it that way), so the window is the
|
|
145
|
+
// statement, not the line — two lines back is enough for every form here.
|
|
146
|
+
const stmt = all.slice(Math.max(0, i - 2), i + 1).join('\n');
|
|
147
|
+
assert.match(stmt, /kindIsUnset/,
|
|
148
|
+
`${name}: this t.kind use neither guards nor is declared a non-render — `
|
|
149
|
+
+ `if it renders, the column's word reaches a reader:\n ${line}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('the column default is TRUTHY, so `||` can never be the guard', () => {
|
|
155
|
+
// The fact that makes the finding above a bug rather than a style note, and
|
|
156
|
+
// the reason no render site may fall back with `||`. Pinned so a future reader
|
|
157
|
+
// does not "simplify" a kindIsUnset() call back into a falsy check.
|
|
158
|
+
const { OTB } = loadOTB();
|
|
159
|
+
assert.ok(OTB.KIND_UNSET, 'the default is a non-empty string');
|
|
160
|
+
assert.equal(!!(OTB.KIND_UNSET || '—'), true, '`t.kind || fallback` keeps the default, never the fallback');
|
|
161
|
+
assert.equal(OTB.KIND_UNSET || '—', 'unclassified', 'which is exactly the word that must not be read');
|
|
162
|
+
for (const [name, { src }] of Object.entries(USES)) {
|
|
163
|
+
assert.doesNotMatch(src.raw, /escapeHtml\(t\.kind \|\|/,
|
|
164
|
+
`${name} never renders a \`||\` fallback for the kind axis — it cannot fire`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('a kind nobody set renders NO tag, exactly as an unset role does', () => {
|
|
169
|
+
for (const [name, src] of [['goals-render.js', goalsRender], ['work.js', workJs]]) {
|
|
170
|
+
assert.match(src, /window\.OTB\.kindIsUnset\(t\.kind\)/,
|
|
171
|
+
`${name} asks the vocabulary whether the kind is set`);
|
|
172
|
+
assert.match(src, /kindTag/, `${name} renders through the guarded tag`);
|
|
173
|
+
}
|
|
174
|
+
// The backlog history row is the third surface and the easiest to miss — it
|
|
175
|
+
// rendered `t.kind || ''`, which is empty-safe but says 'unclassified' out loud.
|
|
176
|
+
const history = workJs.slice(workJs.indexOf('class="history__row"'));
|
|
177
|
+
assert.match(history, /kindIsUnset\(t\.kind\) \? '' : escapeHtml\(t\.kind\)/,
|
|
178
|
+
'the backlog history row is guarded too');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a TABLE cell keeps its placeholder — 'no tag' is not 'no cell'", () => {
|
|
182
|
+
// taskRowsDone()'s Completed ledger is a table: a vanished <td> would shift
|
|
183
|
+
// every following column. It guards like the rest but falls back to the
|
|
184
|
+
// em-dash the ledger already used for an empty cell.
|
|
185
|
+
const done = goalsRender.slice(goalsRender.indexOf('function taskRowsDone'));
|
|
186
|
+
assert.match(done.slice(0, done.indexOf('function ', 10)), /kindIsUnset\(t\.kind\) \? '—' : escapeHtml\(t\.kind\)/,
|
|
187
|
+
'the ledger cell is guarded and still renders a placeholder');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('the filter PREDICATE still compares against the stored value', () => {
|
|
191
|
+
// If this ever reads the label instead, every Kind filter silently matches
|
|
192
|
+
// nothing and the board looks empty rather than broken.
|
|
193
|
+
assert.match(goalsRender, /t\.kind \|\| window\.OTB\.KIND_UNSET/,
|
|
194
|
+
'the goals-page predicate defaults to the stored value, through the constant');
|
|
195
|
+
});
|