@bongos/core 1.19.945 → 1.19.947

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.
@@ -0,0 +1,236 @@
1
+ // tests/goal_map_page.mjs — the goal map page, EXECUTED (task 1003479, goal
2
+ // 1000089; ADR 0227 §5 and §7).
3
+ //
4
+ // The real hall-kit.js, the real goal-map-lib.js and the real goal-map.js run in
5
+ // one vm against a stub DOM and a stubbed GET /goals/graph. What the ADR made
6
+ // non-negotiable is asserted here as behaviour, not read off the source:
7
+ // - the TABLE is on the page by default (§5.1) — no toggle hides it;
8
+ // - category is TEXT on every node as well as a colour (§5.3);
9
+ // - the picture scrolls inside its own box and never widens the page (§5.2);
10
+ // - there is no control that could reach view A (every task) or view D
11
+ // (densified) (§7) — the only fetch is the aggregate;
12
+ // - the stand-alone goals are a list, not dots (§10.4 item 1), and the split is
13
+ // stated on the page.
14
+ // Plus the one thing the browser adds that a lib test cannot see: a goal title is
15
+ // builder-authored text and must never become markup.
16
+ //
17
+ // Run: node tests/goal_map_page.mjs
18
+ import assert from 'node:assert/strict';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import vm from 'node:vm';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { makeRunner, makeEl, walk } from './helpers.mjs';
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
27
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
28
+ const KIT_JS = read('hall-kit.js');
29
+ const LIB_JS = read('goal-map-lib.js');
30
+ const PAGE_JS = read('goal-map.js');
31
+ const PAGE_HTML = read('goal-map.html');
32
+ const PAGE_CSS = read('goal-map.css');
33
+ const FIXTURE = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/hall-preview/fixtures/goals__graph.json'), 'utf8'));
34
+
35
+ const { test, summary } = makeRunner();
36
+ const tick = () => new Promise((r) => setTimeout(r, 0));
37
+ const settle = async () => { for (let i = 0; i < 20; i++) await tick(); };
38
+
39
+ const node = (id, extra = {}) => ({ goal_id: String(id), title: `Goal ${id}`, status: 'open', version_id: 'V1', category: null, task_total: 5, task_done: 2, depends_on: 0, depended_on_by: 0, ...extra });
40
+
41
+ async function boot(payload, { status = 200, fail = false } = {}) {
42
+ const root = makeEl('div');
43
+ const reg = (id, hidden = false) => { const el = makeEl('div', { id }); if (hidden) el.hidden = true; root.appendChild(el); return el; };
44
+ ['gm-sub', 'gm-graph', 'gm-legend', 'gm-alone', 'gm-alone-lede', 'gm-table'].forEach((id) => reg(id));
45
+ ['gm-alone-scroll', 'gm-table-scroll'].forEach((id) => reg(id, true));
46
+
47
+ const documentObj = {
48
+ getElementById: (id) => walk(root, (el) => (el.id === id ? el : null)),
49
+ // SVG elements: the stub's own class/id only follow attrs at construction, so
50
+ // mirror what a browser does when setAttribute writes them.
51
+ createElementNS(_ns, tag) {
52
+ const el = makeEl(tag);
53
+ const orig = el.setAttribute.bind(el);
54
+ el.setAttribute = (k, v) => { orig(k, v); if (k === 'class') el.className = String(v); if (k === 'id') el.id = String(v); };
55
+ return el;
56
+ },
57
+ };
58
+ const requests = [];
59
+ const navigations = [];
60
+ const locationObj = { hostname: 'builders.example.test', assign: (u) => navigations.push(u) };
61
+ const windowObj = {
62
+ OTB: { escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])) },
63
+ localStorage: { getItem: () => null, setItem() {} },
64
+ location: locationObj,
65
+ addEventListener() {},
66
+ };
67
+ const sandbox = {
68
+ window: windowObj, self: windowObj, document: documentObj, location: locationObj, console: { error() {}, log() {}, warn() {} },
69
+ setTimeout, clearTimeout, Promise, Date, Math, JSON, Error, Number, String, Object, Array, Boolean, Set, Map, RegExp, encodeURIComponent,
70
+ fetch: async (url) => {
71
+ requests.push(url);
72
+ if (fail) throw new Error('offline');
73
+ return { ok: status === 200, status, json: async () => payload };
74
+ },
75
+ };
76
+ sandbox.globalThis = sandbox;
77
+ vm.createContext(sandbox);
78
+ vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
79
+ vm.runInContext(LIB_JS, sandbox, { filename: 'goal-map-lib.js' });
80
+ vm.runInContext(PAGE_JS, sandbox, { filename: 'goal-map.js' });
81
+ await settle();
82
+ const $ = (id) => documentObj.getElementById(id);
83
+ // The stub matches one compound selector at a time, so `tbody tr` is two steps.
84
+ const rows = () => { const tb = $('gm-table').querySelector('tbody'); return tb ? tb.querySelectorAll('tr') : []; };
85
+ return { $, requests, navigations, rows, svg: () => $('gm-graph').querySelector('svg') };
86
+ }
87
+
88
+ // ---- the table is the page ---------------------------------------------------------
89
+
90
+ await test('the TABLE is on the page by default — every goal is a row, and nothing hides it behind a toggle', async () => {
91
+ const h = await boot(FIXTURE);
92
+ assert.equal(h.$('gm-table-scroll').hidden, false);
93
+ const rows = h.rows();
94
+ assert.equal(rows.length, FIXTURE.nodes.length, 'stand-alone goals are rows too');
95
+ assert.ok(h.$('gm-table').querySelector('table').getAttribute('aria-label'), 'the table is named for a screen reader');
96
+ });
97
+
98
+ await test('the picture is the enhancement, and it is a role=img figure: no focus stops, no links inside the drawing', async () => {
99
+ const h = await boot(FIXTURE);
100
+ const svg = h.svg();
101
+ assert.ok(svg, 'the map is drawn');
102
+ assert.equal(svg.getAttribute('role'), 'img');
103
+ assert.equal(svg.getAttribute('aria-labelledby'), 'gm-cap');
104
+ assert.equal(svg.querySelectorAll('a').length, 0, 'thirty-six hidden interactive elements from a drawing is the wrong trade');
105
+ assert.equal(svg.querySelectorAll('[tabindex]').length, 0);
106
+ assert.match(h.$('gm-graph').querySelector('figcaption').textContent, /also in the table below/);
107
+ });
108
+
109
+ await test('every goal in the map is a table row too, and every drawn dependency is named in the table', async () => {
110
+ const h = await boot(FIXTURE);
111
+ const drawn = h.svg().querySelectorAll('.gm-node').length;
112
+ assert.equal(drawn, FIXTURE.counts.connected);
113
+ const tableText = h.$('gm-table').textContent;
114
+ for (const e of FIXTURE.edges) {
115
+ const dependent = FIXTURE.nodes.find((n) => n.goal_id === e.from);
116
+ const dependency = FIXTURE.nodes.find((n) => n.goal_id === e.to);
117
+ assert.ok(tableText.includes(dependency.title || 'A private goal'), `the table names ${dependency.title}`);
118
+ assert.ok(tableText.includes(dependent.title || 'A private goal'));
119
+ }
120
+ });
121
+
122
+ // ---- category is text, not only colour ------------------------------------------------
123
+
124
+ await test('CATEGORY IS A WORD on every drawn node, not only a colour — and "No category" is said too', async () => {
125
+ const h = await boot(FIXTURE);
126
+ const cats = h.svg().querySelectorAll('.gm-cat').map((t) => t.textContent);
127
+ assert.equal(cats.length, FIXTURE.counts.connected);
128
+ assert.ok(cats.every((c) => c.length > 0));
129
+ assert.ok(cats.includes('No category'), 'an uncategorised goal is legal and named as such (ADR 0192)');
130
+ assert.ok(cats.some((c) => /Modules|Security|Builders Hall|Economics/.test(c)));
131
+ assert.ok(h.$('gm-legend').textContent.includes('Modules'), 'and the legend spells each category out');
132
+ });
133
+
134
+ // ---- the split is stated, the alone goals are a list ----------------------------------
135
+
136
+ await test('the stand-alone goals are a LIST, never dots, and the split is stated in the page head', async () => {
137
+ const h = await boot(FIXTURE);
138
+ const drawnIds = new Set(h.svg().querySelectorAll('.gm-node').map((g) => g.attrs.transform));
139
+ assert.equal(drawnIds.size, FIXTURE.counts.connected);
140
+ assert.equal(h.$('gm-alone-scroll').hidden, false);
141
+ assert.equal(h.$('gm-alone').querySelectorAll('li').length, FIXTURE.counts.isolated);
142
+ assert.match(h.$('gm-sub').textContent, new RegExp(`the other ${FIXTURE.counts.isolated} stand alone`));
143
+ });
144
+
145
+ // ---- no view A, no view D -------------------------------------------------------------------
146
+
147
+ await test('NEITHER VIEW A NOR VIEW D is reachable: one aggregate fetch, and no control on the page that changes what is drawn', async () => {
148
+ const h = await boot(FIXTURE);
149
+ assert.deepEqual(h.requests, ['/api/bongos/goals/graph'], 'the only read is the aggregate — it carries no task identity');
150
+ const controls = /<(?:button|select|input|textarea)\b/i;
151
+ assert.equal(controls.test(PAGE_HTML), false, 'the page markup has no control at all');
152
+ assert.equal(h.$('gm-graph').querySelectorAll('button, select, input').length, 0);
153
+ assert.equal(/include=|\/tasks|task_id|densif/i.test(PAGE_JS.replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')), false,
154
+ 'no path in the page script can ask for a task');
155
+ });
156
+
157
+ // ---- scrolling ----------------------------------------------------------------------------
158
+
159
+ await test('the picture scrolls INSIDE its own region and names both overflow axes (the page never scrolls sideways)', async () => {
160
+ const h = await boot(FIXTURE);
161
+ const region = h.$('gm-graph').querySelector('.gm-scroll');
162
+ assert.ok(region && region.contains(h.svg()));
163
+ assert.equal(region.getAttribute('tabindex'), '0', 'a keyboard reader can scroll it');
164
+ assert.equal(region.getAttribute('role'), 'region');
165
+ const rule = PAGE_CSS.match(/\.gm-scroll\s*\{[^}]*\}/)[0];
166
+ assert.match(rule, /overflow-x:\s*auto/);
167
+ assert.match(rule, /overflow-y:\s*hidden/, 'one axis named alone makes the other auto (task 1004130)');
168
+ assert.match(rule, /max-width:\s*100%/);
169
+ });
170
+
171
+ // ---- text is never markup -------------------------------------------------------------------
172
+
173
+ await test('a goal title is builder-authored text: it never becomes markup, in the picture or the table or the chips', async () => {
174
+ const evil = '<img src=x onerror=alert(1)>';
175
+ const h = await boot({
176
+ nodes: [node(1, { title: evil, depends_on: 1 }), node(2, { depended_on_by: 1 }), node(3, { title: evil })],
177
+ edges: [{ from: '1', to: '2', count: 1 }],
178
+ counts: {},
179
+ });
180
+ assert.equal(h.svg().querySelectorAll('img').length, 0);
181
+ assert.ok(h.svg().querySelectorAll('.gm-name').some((t) => t.textContent.includes('<img')), 'as literal text in the drawing');
182
+ assert.equal(h.$('gm-table').querySelectorAll('img').length, 0);
183
+ assert.equal(h.$('gm-alone').querySelectorAll('img').length, 0);
184
+ assert.ok(h.$('gm-table').textContent.includes(evil), 'shown as the characters the author typed');
185
+ });
186
+
187
+ // ---- honest states ----------------------------------------------------------------------------
188
+
189
+ await test('a private goal the viewer cannot read is named as one and is not a link', async () => {
190
+ const h = await boot({ nodes: [node(1, { title: null, depended_on_by: 1 }), node(2, { depends_on: 1 })], edges: [{ from: '2', to: '1', count: 2 }], counts: {} });
191
+ const row = h.rows().find((r) => r.textContent.includes('A private goal'));
192
+ assert.ok(row, 'the row exists');
193
+ assert.equal(row.querySelectorAll('a').length, 0, 'nothing to open');
194
+ });
195
+
196
+ await test('no dependency at all says so plainly, draws no picture, and the table is still the whole map', async () => {
197
+ const h = await boot({ nodes: [node(1), node(2)], edges: [], counts: {} });
198
+ assert.equal(h.svg(), null);
199
+ assert.match(h.$('gm-graph').textContent, /no graph to draw — the table below is the whole map/);
200
+ assert.equal(h.rows().length, 2);
201
+ assert.equal(h.$('gm-alone-scroll').hidden, true, 'nothing to split when nothing is connected');
202
+ });
203
+
204
+ await test('an empty corpus renders the empty state and no table', async () => {
205
+ const h = await boot({ nodes: [], edges: [], counts: {} });
206
+ assert.match(h.$('gm-graph').textContent, /No goal carries any work yet/);
207
+ assert.equal(h.$('gm-table-scroll').hidden, true);
208
+ });
209
+
210
+ await test('a failed read is one honest line — not a blank page, not a stack', async () => {
211
+ const h = await boot(null, { fail: true });
212
+ assert.match(h.$('gm-sub').textContent, /Couldn.t load the goal map/);
213
+ assert.equal(h.svg(), null);
214
+ assert.equal(h.$('gm-table-scroll').hidden, true);
215
+ });
216
+
217
+ await test('signed out (401) goes to sign-in like every other hall read', async () => {
218
+ const h = await boot(null, { status: 401 });
219
+ assert.deepEqual(h.navigations, ['/api/bongos/auth/web/start']);
220
+ });
221
+
222
+ // ---- the shell ---------------------------------------------------------------------------------
223
+
224
+ await test('the page has its h1 in static markup (so every state has one) and loads the lib before the page script', () => {
225
+ assert.match(PAGE_HTML, /<h1 class="page-head__h">Goal map<\/h1>/);
226
+ assert.ok(PAGE_HTML.indexOf('goal-map-lib.js') > -1 && PAGE_HTML.indexOf('goal-map-lib.js') < PAGE_HTML.indexOf('goal-map.js'));
227
+ assert.ok(PAGE_HTML.indexOf('hall-kit.js') < PAGE_HTML.indexOf('goal-map.js'));
228
+ });
229
+
230
+ await test('it takes NO nav slot (the sky’s rule) — the Goals page is its door', () => {
231
+ const shell = read('shell.js');
232
+ assert.equal(/goal-map/.test(shell), false, 'no nav item');
233
+ assert.match(read('goals.html'), /href="\/builders\/goal-map"/);
234
+ });
235
+
236
+ summary();
@@ -48,6 +48,9 @@ const ORACLE = {
48
48
  '/builders/atlas': 'requireBuilderPage',
49
49
  '/builders/modules': 'requireBuilderPage',
50
50
  '/builders/goals': 'requireBuilderPage',
51
+ // task 1003479: the goal map. GET /goals/graph is requireBuilder like the goal board it
52
+ // maps, so the page sits on the same tier — and takes no nav slot (the sky's rule).
53
+ '/builders/goal-map': 'requireBuilderPage',
51
54
  '/builders/roadmap': 'requireBuilderPage',
52
55
  // Gated at this tier all along, but with no row until task 1003448 walked the
53
56
  // module's pages: copy-desk (task 1003113) and ideas (task 1002928) are open to
@@ -17,7 +17,11 @@ import assert from 'node:assert/strict';
17
17
  import { createRequire } from 'node:module';
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
- const { checkDevsecopsOwnsItsChecks, siblingRequires } = require('../scripts/gds/scope-map-guard.js');
20
+ const { checkDevsecopsOwnsItsChecks, siblingRequires, checkEveryModuleDirIsScoped, moduleDirsOnDisk,
21
+ checkNoNewUnownedRoot, trackedRoots, rootIsTouched } = require('../scripts/gds/scope-map-guard.js');
22
+ const fs = require('node:fs');
23
+ const os = require('node:os');
24
+ const path = require('node:path');
21
25
 
22
26
  test('every file fitness.js requires is owned by some MODULE_SCOPE_MAP key', () => {
23
27
  const r = checkDevsecopsOwnsItsChecks();
@@ -80,3 +84,99 @@ test('the PASS note carries the count, so the check cannot look green while doin
80
84
  const n = Number(r.note.match(/^(\d+) file/)[1]);
81
85
  assert.ok(n > 14, `expected more than the fourteen the filing task listed, saw ${n}`);
82
86
  });
87
+
88
+ // ── the same defect one level up: a carved module nobody can scope to (task 1004132) ──
89
+ //
90
+ // modules/specialities/ shipped a whole module — manifest, db slice, routes, two
91
+ // migrations — with no MODULE_SCOPE_MAP key, so a goal scoped to `specialities` was
92
+ // refused out_of_scope for touching the module it is named after. 21 of the 22 modules
93
+ // were fine, which is exactly why it went unseen.
94
+
95
+ test('every carved module on disk is claimed by some MODULE_SCOPE_MAP key', () => {
96
+ const r = checkEveryModuleDirIsScoped();
97
+ assert.equal(r.ok, true, r.violations.join('\n'));
98
+ assert.equal(r.hardFail, false);
99
+ });
100
+
101
+ test('specialities specifically — the module this check was written for', () => {
102
+ const { MODULE_SCOPE_MAP } = require('../src/bongos/module-scope-map.js');
103
+ const globsOf = (v) => (Array.isArray(v) ? v : (v && v.globs) || []);
104
+ const all = Object.values(MODULE_SCOPE_MAP).flatMap(globsOf);
105
+ assert.ok(all.includes('modules/specialities/'),
106
+ 'modules/specialities/ lost its owner again — a goal scoped to specialities cannot touch it');
107
+ });
108
+
109
+ test('moduleDirsOnDisk counts a manifest, not a directory', () => {
110
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-modules-'));
111
+ fs.mkdirSync(path.join(root, 'real'));
112
+ fs.writeFileSync(path.join(root, 'real', 'module.json'), '{"key":"real"}');
113
+ fs.mkdirSync(path.join(root, 'not-a-module')); // no manifest
114
+ fs.writeFileSync(path.join(root, 'stray.txt'), 'x'); // not a directory
115
+ assert.deepEqual(moduleDirsOnDisk(root), ['real']);
116
+ fs.rmSync(root, { recursive: true, force: true });
117
+ });
118
+
119
+ test('the scan cannot pass vacuously — an empty modules/ is a BROKEN scan, not a clean one', () => {
120
+ // The failure this mirrors is the one the sibling check above documents: a scanner
121
+ // that matches nothing reports a healthy repo, and healthy is what broken looks like.
122
+ const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-empty-'));
123
+ assert.deepEqual(moduleDirsOnDisk(empty), []);
124
+ assert.deepEqual(moduleDirsOnDisk(path.join(empty, 'does-not-exist')), [],
125
+ 'a missing modules/ must return empty rather than throw — the check turns that into a hard fail');
126
+ fs.rmSync(empty, { recursive: true, force: true });
127
+ });
128
+
129
+ // ── the record of what is deliberately unowned, and that it cannot rot (task 1004132) ──
130
+ //
131
+ // The decision "the core is owned by nobody" is DATA (UNOWNED_ROOTS), not prose, so
132
+ // that it can be enforced. A one-directional check would let the list become a
133
+ // rubber stamp, so both directions are pinned here.
134
+
135
+ test('the repo has no unowned root that UNOWNED_ROOTS does not account for', () => {
136
+ const r = checkNoNewUnownedRoot();
137
+ assert.equal(r.ok, true, r.violations.join('\n'));
138
+ });
139
+
140
+ test('a NEW unowned root fails — the growth this exists to stop', () => {
141
+ const files = require('node:child_process')
142
+ .execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
143
+ const r = checkNoNewUnownedRoot([...files, 'vendor/thing.js']);
144
+ assert.equal(r.ok, false);
145
+ assert.equal(r.hardFail, true);
146
+ assert.match(r.violations.join('\n'), /'vendor\/' is tracked but NO module glob/);
147
+ });
148
+
149
+ test('a STALE entry fails too — the list may shrink, never lie', () => {
150
+ const files = require('node:child_process')
151
+ .execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
152
+ const r = checkNoNewUnownedRoot(files.filter((f) => f !== 'LICENSE'));
153
+ assert.equal(r.ok, false);
154
+ assert.match(r.violations.join('\n'), /lists 'LICENSE', which is not a tracked root any more/);
155
+ });
156
+
157
+ test('the scan cannot pass vacuously', () => {
158
+ const r = checkNoNewUnownedRoot([]);
159
+ assert.equal(r.ok, false);
160
+ assert.equal(r.hardFail, true);
161
+ assert.match(r.violations.join('\n'), /broken scan, not a clean result/);
162
+ });
163
+
164
+ test('trackedRoots collapses paths to roots; rootIsTouched sees a PARTIAL carve', () => {
165
+ assert.deepEqual(trackedRoots(['src/a/b.js', 'src/c.js', 'LICENSE', 'bin/x']),
166
+ ['LICENSE', 'bin/', 'src/']);
167
+ // src/ is partially owned (one file claimed) — that is the normal in-progress
168
+ // state of ADR 0083's carve, and must NOT read as unowned territory.
169
+ assert.equal(rootIsTouched('src/', ['src/bongos/llm-pricing.js']), true);
170
+ assert.equal(rootIsTouched('vendor/', ['src/bongos/llm-pricing.js']), false);
171
+ // A directory glob covering the whole root counts.
172
+ assert.equal(rootIsTouched('modules/', ['modules/ideas/']), true);
173
+ });
174
+
175
+ test('every UNOWNED_ROOTS entry carries a reason', () => {
176
+ const { UNOWNED_ROOTS } = require('../src/bongos/module-scope-map.js');
177
+ for (const [root, reason] of Object.entries(UNOWNED_ROOTS)) {
178
+ assert.equal(typeof reason, 'string', `${root} has no reason`);
179
+ assert.ok(reason.trim().length > 8,
180
+ `${root}'s reason is too thin to justify an unowned root: ${JSON.stringify(reason)}`);
181
+ }
182
+ });