@bongos/core 1.19.878 → 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 +43 -83
- package/docs/copy-inventory.md +368 -456
- package/docs/copy-registry.json +457 -1344
- package/docs/file-map.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/module.json +1 -1
- package/modules/hall-ui/public/builders.js +1 -13
- package/modules/hall-ui/public/dom-utils.js +22 -6
- package/modules/hall-ui/public/goals-render.js +8 -3
- package/modules/hall-ui/public/hall-render.js +2 -18
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/panel.css +8 -9
- package/modules/hall-ui/public/settings.css +0 -5
- package/modules/hall-ui/public/settings.html +0 -29
- package/modules/hall-ui/public/settings.js +1 -26
- package/modules/hall-ui/public/shell.js +0 -1
- package/modules/hall-ui/public/style.css +0 -44
- package/modules/hall-ui/public/task.js +6 -2
- package/modules/hall-ui/public/watch.html +0 -7
- package/modules/hall-ui/public/watch.js +1 -37
- package/modules/hall-ui/public/work.js +4 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/serve-internal.js +11 -11
- package/src/module-api.js +1 -1
- package/tests/government_page_gates.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_nav.mjs +1 -1
- package/tests/hall_oversight_world.mjs +5 -5
- package/tests/hall_page_gate_map.mjs +11 -2
- package/tests/hall_record_world.mjs +5 -0
- package/tests/hall_settings_world.mjs +21 -18
- package/tests/kind_axis_unset.mjs +195 -0
- package/modules/hall-ui/public/box-panel.js +0 -456
- package/modules/hall-ui/public/harbor.css +0 -9
- package/modules/hall-ui/public/harbor.html +0 -69
- package/modules/hall-ui/public/harbor.js +0 -367
- package/modules/hall-ui/public/harbor.states.json +0 -77
- package/modules/hall-ui/public/pair.css +0 -23
- package/modules/hall-ui/public/pair.html +0 -58
- package/modules/hall-ui/public/pair.js +0 -132
- package/modules/hall-ui/public/pair.states.json +0 -14
|
@@ -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",
|
|
@@ -927,16 +927,16 @@ function mountInternalSurfaces(app) {
|
|
|
927
927
|
return res.redirect(301, `https://builders.${apex}/${rest}${qs}`);
|
|
928
928
|
});
|
|
929
929
|
|
|
930
|
-
// The /watch monitoring page and the /
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
930
|
+
// The /watch monitoring page and the /gate page (task 1179) both sit behind
|
|
931
|
+
// `page.view.government` — Metic and up since ADR 0157. Same server-side page
|
|
932
|
+
// gate, same posture: a builder without the permission never receives the shell
|
|
933
|
+
// (requireGovernmentPage — named requireArchonPage before ADR 0157 — redirects
|
|
934
|
+
// them to the hall, and an unauthenticated visitor into sign-in). The subdomain
|
|
935
|
+
// shim above has already rewritten the bare /watch + /gate onto /builders/*.
|
|
936
|
+
// (Defense in depth: every data endpoint these pages call carries its own
|
|
937
|
+
// server-side gate.) /harbor sat here too until task 1003890 removed the dev
|
|
938
|
+
// box: its page is gone, and a gate in front of nothing is a dead guard.
|
|
938
939
|
const WATCH_PAGE_RE = /^\/builders\/watch(?:\.html)?\/?$/;
|
|
939
|
-
const HARBOR_PAGE_RE = /^\/builders\/harbor(?:\.html)?\/?$/;
|
|
940
940
|
const GATE_PAGE_RE = /^\/builders\/gate(?:\.html)?\/?$/;
|
|
941
941
|
// /sessions (the session-log ledger) belongs to that same Governance group and
|
|
942
942
|
// its data route (`session.search`) carries the same metic floor since ADR 0157
|
|
@@ -1020,7 +1020,7 @@ function mountInternalSurfaces(app) {
|
|
|
1020
1020
|
if (isModuleSurfaceHost(req)) return next();
|
|
1021
1021
|
if (!req.path.startsWith('/builders')) return next();
|
|
1022
1022
|
const norm = path.posix.normalize(req.path);
|
|
1023
|
-
if (WATCH_PAGE_RE.test(norm) ||
|
|
1023
|
+
if (WATCH_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
|
|
1024
1024
|
|| SESSIONS_PAGE_RE.test(norm) || PROJECT_SETTINGS_PAGE_RE.test(norm)) {
|
|
1025
1025
|
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
1026
1026
|
}
|
|
@@ -1201,7 +1201,7 @@ function mountInternalSurfaces(app) {
|
|
|
1201
1201
|
// read behind it is gated server-side, so this closed an inconsistency rather
|
|
1202
1202
|
// than an exposure; the point is that one URL cannot be two tiers at once.
|
|
1203
1203
|
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
1204
|
-
/^\/builders\/(?:work|
|
|
1204
|
+
/^\/builders\/(?:work|settings|atlas|modules|goals|roadmap|copy-desk|studio|ideas|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
1205
1205
|
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
1206
1206
|
// page that embeds them.
|
|
1207
1207
|
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
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');
|
|
@@ -64,7 +64,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
|
64
64
|
// unaffected — it held right up to the deletion.
|
|
65
65
|
//
|
|
66
66
|
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
|
|
67
|
-
// the monitoring pages (/watch, /
|
|
67
|
+
// the monitoring pages (/watch, /gate) now open to the working rank. That gate
|
|
68
68
|
// was spelled `requireArchonPage` through R107 and ADR 0157 — the name the rows below
|
|
69
69
|
// were first written against — and was renamed once the floor moved; the audience here,
|
|
70
70
|
// not the name, is the contract.
|
|
@@ -91,7 +91,7 @@ async function boot({ meStatus }) {
|
|
|
91
91
|
wirePager: () => {},
|
|
92
92
|
},
|
|
93
93
|
OTBHallRender: () => ({
|
|
94
|
-
SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {},
|
|
94
|
+
SETUP_CONFIG_KEY: 'k', analyticsState: {}, lbState: {},
|
|
95
95
|
shipState: {}, shipsPager: { page: 1 }, welcomeDismissalKey: () => 'k',
|
|
96
96
|
mountHallWidgets: spy('mountHallWidgets'),
|
|
97
97
|
renderLanding: spy('renderLanding'),
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -287,7 +287,7 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
287
287
|
// 'deploy' (task 1003159, ADR 0293) is the SECOND archon item, beside 'government':
|
|
288
288
|
// its gate is the `core.pin.move` atom, archon-floor and system:true. It sits with
|
|
289
289
|
// the steering surfaces because that is what it is — the owner's third release gate.
|
|
290
|
-
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', '
|
|
290
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'gate', 'sessions', 'project-settings', 'deploy', 'government']);
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
test('Economy lives in Docs and points at the reward-schedule page', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tests/hall_oversight_world.mjs — the oversight family in the chrome world
|
|
2
|
-
// (task 1003469, goal 1000081): government, watch
|
|
2
|
+
// (task 1003469, goal 1000081): government, watch and gate share one
|
|
3
3
|
// archetype sheet (oversight.css) the way the records share record.css and the
|
|
4
4
|
// reading rooms room.css. These are static pins over the shipped source: the
|
|
5
5
|
// family sheet is linked and spends only the fifteen, every page renders the
|
|
@@ -17,7 +17,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
18
|
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
19
19
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
|
|
20
|
-
const PAGES = ['government', 'watch', '
|
|
20
|
+
const PAGES = ['government', 'watch', 'gate'];
|
|
21
21
|
|
|
22
22
|
test('every oversight page links the family sheet, before its own', () => {
|
|
23
23
|
for (const p of PAGES) {
|
|
@@ -76,8 +76,8 @@ test('Government renders the rank description the create form collects', () => {
|
|
|
76
76
|
assert.match(read('government.html'), /id="gov-create-desc"/, 'the form still collects it');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test('
|
|
80
|
-
for (const p of ['
|
|
79
|
+
test('Gate carries no coloured band: state is a pill', () => {
|
|
80
|
+
for (const p of ['gate']) {
|
|
81
81
|
assert.ok(!/__roof|__seal/.test(read(`${p}.js`)), `${p}.js emits no roof or seal band`);
|
|
82
82
|
assert.ok(!/__roof|__seal/.test(read(`${p}.css`)), `${p}.css has no band rule`);
|
|
83
83
|
assert.match(read(`${p}.js`), /fact-pill fact-pill--/, `${p}.js paints state as a .fact-pill`);
|
|
@@ -91,7 +91,7 @@ test('the shared confirm modal sits on --z-modal, closed by one hairline and no
|
|
|
91
91
|
const modal = css.slice(css.indexOf('.ov-modal {'), css.indexOf('.ov-modal .scroll__h'));
|
|
92
92
|
assert.ok(!/box-shadow/.test(modal), 'no shadow under the dialog');
|
|
93
93
|
assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
|
|
94
|
-
for (const p of ['
|
|
94
|
+
for (const p of ['gate']) {
|
|
95
95
|
assert.match(read(`${p}.js`), /className = 'ov-modal-bg'/, `${p}.js opens the family modal`);
|
|
96
96
|
assert.ok(!/-modal-bg/.test(read(`${p}.css`)), `${p}.css no longer styles a modal of its own`);
|
|
97
97
|
}
|
|
@@ -44,7 +44,6 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
44
44
|
const ORACLE = {
|
|
45
45
|
// Any signed-in builder — no rank floor.
|
|
46
46
|
'/builders/work': 'requireBuilderPage',
|
|
47
|
-
'/builders/pair': 'requireBuilderPage',
|
|
48
47
|
'/builders/settings': 'requireBuilderPage',
|
|
49
48
|
'/builders/atlas': 'requireBuilderPage',
|
|
50
49
|
'/builders/modules': 'requireBuilderPage',
|
|
@@ -74,7 +73,6 @@ const ORACLE = {
|
|
|
74
73
|
|
|
75
74
|
// Metic+ operator surfaces (the Governance nav group).
|
|
76
75
|
'/builders/watch': 'requireGovernmentPage',
|
|
77
|
-
'/builders/harbor': 'requireGovernmentPage',
|
|
78
76
|
'/builders/gate': 'requireGovernmentPage',
|
|
79
77
|
'/builders/sessions': 'requireGovernmentPage',
|
|
80
78
|
// The five fundamentals of the project (task 1003280, goal 1000072). Same tier as
|
|
@@ -155,6 +153,14 @@ const ORACLE = {
|
|
|
155
153
|
// catch. GONE_PAGES below keeps the row honest if the file ever returns.
|
|
156
154
|
'/builders/my-projects': null,
|
|
157
155
|
|
|
156
|
+
// Gone with the dev box (task 1003890, goal 1000091). Harbor was the Archon
|
|
157
|
+
// view of the box fleet and pair the device-pairing page; the fleet was turned
|
|
158
|
+
// off at the control plane by task 1003886, so both pages drove an API that no
|
|
159
|
+
// longer answers. Their gates left serve-internal.js with them — a gate in
|
|
160
|
+
// front of nothing is the dead-guard shape the check below exists to catch.
|
|
161
|
+
'/builders/harbor': null,
|
|
162
|
+
'/builders/pair': null,
|
|
163
|
+
|
|
158
164
|
// Gone too, and for a reason worth keeping in front of the next reader: this
|
|
159
165
|
// was a bespoke refusal PAGE ("not yet — ship three tasks to graduate"), the
|
|
160
166
|
// deniedTo of the thetes+ gate. Task 1002613 replaced that whole idea with a
|
|
@@ -278,6 +284,9 @@ const GONE_PAGES = {
|
|
|
278
284
|
'/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
|
|
279
285
|
// Task 1003450 — the shell's ?denied= banner replaced the bespoke refusal page.
|
|
280
286
|
'/builders/not-ready': 'task 1003450 — the shell renders the refusal from ?denied=',
|
|
287
|
+
// Task 1003890 — the dev-box deprecation removed the fleet these two drove.
|
|
288
|
+
'/builders/harbor': 'task 1003890 — the dev box is gone, so its Archon view is too',
|
|
289
|
+
'/builders/pair': 'task 1003890 — the dev box is gone, so its pairing page is too',
|
|
281
290
|
};
|
|
282
291
|
|
|
283
292
|
test('a row that is null because the page left the hall serves no page', () => {
|
|
@@ -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),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// tests/hall_settings_world.mjs — the PANEL family in the chrome world
|
|
2
|
-
// (task 1003308, goal 1000081): settings
|
|
2
|
+
// (task 1003308, goal 1000081): settings and profile. Pair left with the dev
|
|
3
|
+
// box (task 1003890).
|
|
3
4
|
//
|
|
4
5
|
// tests/hall_tokens.mjs pins the TIER these pages spend; this pins how they
|
|
5
6
|
// spend it. Everything here is a fact that is silent in a browser: the page
|
|
@@ -9,9 +10,9 @@
|
|
|
9
10
|
//
|
|
10
11
|
// What it locks:
|
|
11
12
|
// * ONE ARCHETYPE SHEET. panel.css holds the row, the switch, the field, the
|
|
12
|
-
// chip control and the family's voices; the
|
|
13
|
-
//
|
|
14
|
-
// * THE ONE h1. None of
|
|
13
|
+
// chip control and the family's voices; the page sheets hold only what one
|
|
14
|
+
// page has. Every page links panel.css BEFORE its own sheet.
|
|
15
|
+
// * THE ONE h1. None of them had one — the hall predates the floor — so
|
|
15
16
|
// every render failed one-header-main-h1. The h1 is the page's name in the
|
|
16
17
|
// room voice, and it lives in the static markup so the DOM has it in every
|
|
17
18
|
// state, not only the happy path.
|
|
@@ -26,14 +27,14 @@
|
|
|
26
27
|
// * WHITE ON THE ACCENT, AND THE ONE EXCEPTION. Eight literals across three
|
|
27
28
|
// sheets are gone; the Discord blurple pair stays, because it is Discord's
|
|
28
29
|
// mark and not the world's, and it is named as the exception in a comment.
|
|
29
|
-
// * THE COMPAT ALIASES.
|
|
30
|
+
// * THE COMPAT ALIASES. Every sheet in the family, zero aliases spent.
|
|
30
31
|
//
|
|
31
32
|
// Reproduce the renders these came from (the family needs the fixture harness —
|
|
32
33
|
// every panel is filled from /me/* reads the kit stub does not carry):
|
|
33
34
|
// node scripts/hall-preview/server.js --port 4621 --fixture-me
|
|
34
35
|
// node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json \
|
|
35
36
|
// --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621
|
|
36
|
-
// (and profile
|
|
37
|
+
// (and profile .states.json the same way)
|
|
37
38
|
|
|
38
39
|
import { test } from 'node:test';
|
|
39
40
|
import assert from 'node:assert/strict';
|
|
@@ -48,9 +49,10 @@ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
|
48
49
|
|
|
49
50
|
// not-ready was the fourth until task 1003450 deleted the page — see
|
|
50
51
|
// tests/hall_page_gate_map.mjs (GONE_PAGES) for why, and for the row that stops
|
|
51
|
-
// it coming back ungated
|
|
52
|
-
|
|
53
|
-
const
|
|
52
|
+
// it coming back ungated; pair left the same way in task 1003890. The archetype
|
|
53
|
+
// is unaffected: the pages that remain still spend it.
|
|
54
|
+
const PAGES = ['settings', 'profile'];
|
|
55
|
+
const SHEETS = ['panel.css', 'settings.css', 'profile.css'];
|
|
54
56
|
|
|
55
57
|
const panelCss = read('panel.css');
|
|
56
58
|
const settingsCss = read('settings.css');
|
|
@@ -94,7 +96,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
|
|
|
94
96
|
for (const sel of ['.prow', '.sw', '.sw__track', '.sw__knob', '.field-label', '.pinput', '.chip-ctl', '.panel-note', '.panel-status', '.gate']) {
|
|
95
97
|
assert.ok(ruleOf(panelCss, sel), `panel.css defines ${sel}`);
|
|
96
98
|
}
|
|
97
|
-
for (const sheet of ['settings.css', '
|
|
99
|
+
for (const sheet of ['settings.css', 'profile.css']) {
|
|
98
100
|
const css = read(sheet);
|
|
99
101
|
for (const sel of ['.prow', '.sw', '.sw__track', '.pinput', '.chip-ctl']) {
|
|
100
102
|
assert.equal(ruleOf(css, sel), null, `${sheet} must not redefine ${sel} — that is how the family would drift`);
|
|
@@ -104,7 +106,7 @@ test('the archetype holds the row, the switch, the field and the voices; the pag
|
|
|
104
106
|
|
|
105
107
|
// ---- 2. the one h1, in the static markup ----------------------------------
|
|
106
108
|
|
|
107
|
-
test('each
|
|
109
|
+
test('each page in the family has exactly one h1, in the family\'s page head', () => {
|
|
108
110
|
for (const p of PAGES) {
|
|
109
111
|
const html = read(`${p}.html`).replace(/<!--[\s\S]*?-->/g, '');
|
|
110
112
|
const h1s = html.match(/<h1\b/g) || [];
|
|
@@ -301,7 +303,7 @@ test('no sheet in the family spends a compat alias', () => {
|
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
test('white on the accent is gone, and the Discord mark is the one named exception', () => {
|
|
304
|
-
for (const sheet of ['panel.css', '
|
|
306
|
+
for (const sheet of ['panel.css', 'profile.css']) {
|
|
305
307
|
const bare = stripComments(read(sheet));
|
|
306
308
|
assert.doesNotMatch(bare, /#[0-9a-f]{3,8}\b/i, `${sheet} carries no colour literal`);
|
|
307
309
|
assert.doesNotMatch(bare, /:\s*white\b/i, `${sheet} carries no named white`);
|
|
@@ -318,16 +320,17 @@ test('white on the accent is gone, and the Discord mark is the one named excepti
|
|
|
318
320
|
assert.match(selector, /\.settings-btn--discord/, `${lit} is spent only on the Discord button`);
|
|
319
321
|
}
|
|
320
322
|
assert.match(settingsCss, /THIRD-PARTY MARK, KEPT ON PURPOSE/, 'the exception is named in a comment so a literal sweep leaves it alone');
|
|
321
|
-
// the save/accent buttons take --accent-on. `.
|
|
322
|
-
//
|
|
323
|
-
//
|
|
323
|
+
// the save/accent buttons take --accent-on. `.craft-save` is a member of the
|
|
324
|
+
// base pill rule AND the save rule, so the save rule is found by its own name
|
|
325
|
+
// and the member is asserted against it. (`.pair-approve` was its sibling here
|
|
326
|
+
// until task 1003890 removed the pair page.)
|
|
324
327
|
const save = ruleOf(panelCss, '.pbtn--save');
|
|
325
328
|
assert.ok(save, '.pbtn--save has a rule');
|
|
326
329
|
assert.match(save, /background:\s*var\(--accent\)/);
|
|
327
330
|
assert.match(save, /color:\s*var\(--accent-on\)/, 'the dark ink for the salmon — white on it fails AA');
|
|
328
331
|
const saveSelector = stripComments(panelCss).match(/([^{}]*\.pbtn--save[^{}]*)\{/);
|
|
329
332
|
assert.ok(saveSelector, 'the save rule\'s selector list is readable');
|
|
330
|
-
for (const member of ['.craft-save'
|
|
333
|
+
for (const member of ['.craft-save']) {
|
|
331
334
|
assert.ok(saveSelector[1].split(',').map((s) => s.trim()).includes(member), `${member} is the accent door on its page`);
|
|
332
335
|
}
|
|
333
336
|
});
|
|
@@ -394,14 +397,14 @@ test('the profile identity band is a MODIFIER on the shell\'s .profile, never a
|
|
|
394
397
|
|
|
395
398
|
// ---- 6. the floors are reproducible ------------------------------------------
|
|
396
399
|
|
|
397
|
-
test('
|
|
400
|
+
test('every page in the family has a states file, so the floors are reproducible', () => {
|
|
398
401
|
for (const p of PAGES) {
|
|
399
402
|
const json = JSON.parse(read(`${p}.states.json`));
|
|
400
403
|
assert.equal(json.surface, 'hall-ui', `${p}.states.json names its surface`);
|
|
401
404
|
assert.ok(Object.keys(json.states).length >= 2, `${p}.states.json renders more than the default state`);
|
|
402
405
|
}
|
|
403
406
|
assert.ok(Object.keys(JSON.parse(read('settings.states.json')).states).length >= 4, 'settings renders every rail group');
|
|
404
|
-
for (const fx of ['me__sessions'
|
|
407
|
+
for (const fx of ['me__sessions']) {
|
|
405
408
|
assert.ok(fs.existsSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', `${fx}.json`)),
|
|
406
409
|
`the harness serves ${fx} — without it the page only ever rendered its empty state`);
|
|
407
410
|
}
|
|
@@ -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
|
+
});
|