@bongos/core 1.19.468 → 1.19.470

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.
Files changed (41) hide show
  1. package/.bongos-core.json +94 -44
  2. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +169 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +42 -50
  5. package/docs/copy-registry.json +167 -271
  6. package/docs/file-map.md +5 -1
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/research/task-network-map/a-every-task-p2.png +0 -0
  9. package/docs/research/task-network-map/a-every-task-p40.png +0 -0
  10. package/docs/research/task-network-map/b-dependency-forest-p2.png +0 -0
  11. package/docs/research/task-network-map/b-dependency-forest-p40.png +0 -0
  12. package/docs/research/task-network-map/c-goals-p2.png +0 -0
  13. package/docs/research/task-network-map/c-goals-p40.png +0 -0
  14. package/docs/research/task-network-map/d-joined-by-goal.png +0 -0
  15. package/docs/research/task-network-map-readability.md +191 -0
  16. package/modules/hall-ui/public/atlas.css +96 -88
  17. package/modules/hall-ui/public/atlas.html +43 -20
  18. package/modules/hall-ui/public/atlas.js +118 -20
  19. package/modules/hall-ui/public/atlas.states.json +52 -1
  20. package/modules/hall-ui/public/modules.css +67 -86
  21. package/modules/hall-ui/public/modules.html +46 -35
  22. package/modules/hall-ui/public/modules.js +195 -103
  23. package/modules/hall-ui/public/modules.states.json +48 -1
  24. package/modules/hall-ui/public/people.css +3 -2
  25. package/modules/hall-ui/public/people.html +2 -2
  26. package/modules/hall-ui/public/people.js +5 -4
  27. package/modules/hall-ui/public/people.states.json +1 -1
  28. package/modules/hall-ui/public/settings.js +1 -1
  29. package/modules/hall-ui/public/style.css +12 -11
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/scripts/gds/spike-task-map.js +730 -0
  33. package/src/bongos/serve-internal.js +20 -8
  34. package/src/module-api.js +1 -1
  35. package/tests/hall_catalog_world.mjs +181 -0
  36. package/tests/hall_page_gate_map.mjs +4 -2
  37. package/tests/hall_reading_world.mjs +2 -2
  38. package/tests/hall_roster_world.mjs +40 -12
  39. package/tests/spike_task_map.mjs +0 -0
  40. package/modules/hall-ui/public/scouting.html +0 -69
  41. package/modules/hall-ui/public/scouting.states.json +0 -59
@@ -837,13 +837,25 @@ function mountInternalSurfaces(app) {
837
837
  // exact permission its data route (GET /scouting) checks — same posture as the
838
838
  // /government tab above (the page can never drift wider than the API it fronts).
839
839
  const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
840
- // /scouting is the LEGACY twin of /people — the same /api/gds/scouting directory,
841
- // left behind when task 1002577 graduated it into the hall. It has no nav entry
842
- // and its own header claims "Archon-only", but it was never page-gated: the page
843
- // rendered for anyone and leaned entirely on the data route's 403. Gated on the
844
- // same atom as /people so the two can't drift apart (task 1002710). Deleting it
845
- // in favour of /people is a separate call — it is walled, not removed, here.
846
- const SCOUTING_PAGE_RE = /^\/builders\/scouting(?:\.html)?\/?$/;
840
+ // /scouting WAS the legacy twin of /people — the same /api/gds/scouting
841
+ // directory, left behind when task 1002577 graduated it into the hall. Task
842
+ // 1003470 had already made the two one page (scouting.html rendered people's
843
+ // body byte-for-byte and scouting.js was deleted); task 1003440 removes the
844
+ // door itself, because a second URL onto one directory is a surface to keep in
845
+ // sync for no reader. The URL survives as a 301 so an old bookmark lands on the
846
+ // directory instead of a 404. It carries no page gate: the redirect discloses
847
+ // nothing, and /people's wall applies the instant the browser follows it.
848
+ const SCOUTING_LEGACY_RE = /^\/builders\/scouting(?:\.html)?\/?$/;
849
+ app.use((req, res, next) => {
850
+ if (isModuleSurfaceHost(req)) return next();
851
+ if (req.method !== 'GET' && req.method !== 'HEAD') return next();
852
+ if (!SCOUTING_LEGACY_RE.test(path.posix.normalize(req.path))) return next();
853
+ const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
854
+ // On the builders subdomain the /builders prefix is redundant (the canonical
855
+ // redirect above strips it), so emit the clean path and save a second hop.
856
+ return res.redirect(301, `${isBuildersHost(req) ? '' : '/builders'}/people${qs}`);
857
+ });
858
+
847
859
  app.use((req, res, next) => {
848
860
  if (isModuleSurfaceHost(req)) return next();
849
861
  if (!req.path.startsWith('/builders')) return next();
@@ -862,7 +874,7 @@ function mountInternalSurfaces(app) {
862
874
  if (GOVERNMENT_PAGE_RE.test(norm)) {
863
875
  return gdsAuth.requireGovernmentPage(req, res, next);
864
876
  }
865
- if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
877
+ if (PEOPLE_PAGE_RE.test(norm)) {
866
878
  return gdsAuth.requireProjectCuratePage(req, res, next);
867
879
  }
868
880
  next();
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.468'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.470'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,181 @@
1
+ // tests/hall_catalog_world.mjs — the catalog pages in the chrome world (task
2
+ // 1003472, goal 1000081): modules and the repo atlas, REDESIGNED rather than
3
+ // re-skinned. Static pins over the shipped source: the module catalog is ONE
4
+ // panel of the oversight family's rows (the card grid, the boxed search and
5
+ // the .mod-chip alias are gone), the description is clamped with a details
6
+ // disclosure under it, the right column is the family's grant toggle or a
7
+ // state pill and never an accent fill, the submit door sits on the row, and
8
+ // the figure band carries the numbers the page head used to say as prose; the
9
+ // atlas keeps its nine semantic colours and puts every piece of chrome around
10
+ // the scene in the shell's dialect — the panel is a .scroll, the view switch
11
+ // the tab strip, the detail card a hairline panel with no shadow — and gains
12
+ // an island index a keyboard or a phone can open.
13
+ import test from 'node:test';
14
+ import assert from 'node:assert/strict';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
21
+ const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8').replace(/\r\n/g, '\n');
22
+ // Negative pins read the code, not its comments (a comment that says what left
23
+ // would otherwise fail the pin that says it left).
24
+ const decomment = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/^\s*\/\/.*$/gm, '');
25
+ const STYLE = read('style.css');
26
+ const OVERSIGHT = read('oversight.css');
27
+ const tokensDefined = (css, where) => {
28
+ for (const t of new Set([...css.matchAll(/var\((--[a-z0-9-]+)\)/g)].map((m) => m[1]))) {
29
+ const declared = (src) => new RegExp(`(^\\s*|[{;]\\s*)${t}:`, 'm').test(src);
30
+ assert.ok(declared(STYLE) || declared(OVERSIGHT) || declared(css),
31
+ `${where} spends ${t}, which neither style.css, oversight.css nor the sheet itself defines`);
32
+ }
33
+ };
34
+ const noAlias = (rawCss, where) => {
35
+ const css = decomment(rawCss);
36
+ const alias = css.match(/--(surface|surface-2|border|border-strong|accent-weak|accent-text)\b/g) || [];
37
+ assert.deepEqual(alias, [], `${where} spends a compat alias: ${alias.join(', ')}`);
38
+ };
39
+
40
+ // ---- modules ----------------------------------------------------------------
41
+
42
+ test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll', () => {
43
+ const html = read('modules.html');
44
+ assert.match(html, /<link rel="stylesheet" href="\/builders\/oversight\.css">\s*<link rel="stylesheet" href="\/builders\/modules\.css">/, 'the family sheet loads before the page sheet');
45
+ assert.equal((html.match(/class="scroll[ "]/g) || []).length, 1, 'one panel, not three');
46
+ assert.match(html, /<div id="mod-filters"><\/div>/, 'the kit filter bar has a mount');
47
+ assert.match(html, /class="ov-figs" id="mod-figs"/, 'the figure band is the family\'s');
48
+ assert.match(html, /<ul class="ov-rows" id="core-grid">/, 'the core catalog is a run of rows');
49
+ assert.match(html, /<ul class="ov-rows" id="own-grid">/, 'the project\'s own modules are a run of rows');
50
+ assert.equal((html.match(/class="ov-group__head"/g) || []).length, 2, 'two group heads');
51
+ assert.doesNotMatch(decomment(html), /mod-grid|mod-card|mod-browse|submit-scroll|class="settings-status"/, 'the card grid, the boxed search and the third panel are gone');
52
+ assert.doesNotMatch(html, /\d+ module\(s\)/, 'the page head no longer says the numbers as prose');
53
+ });
54
+
55
+ test('modules: the row is the oversight row — title with the mono key, clamped description, facts, one control on the right', () => {
56
+ const js = read('modules.js');
57
+ assert.match(js, /<li class="ov-row mod-row" data-key=/, 'each module is an .ov-row');
58
+ assert.match(js, /<span class="ov-row__title">\$\{escapeHtml\(m\.title \|\| m\.key\)\}<span class="ov-mono">\$\{escapeHtml\(m\.key\)\}<\/span>/, 'the key rides inside the title in mono');
59
+ assert.match(js, /class="ov-row__sub mod-row__desc"/, 'the description is the sub line');
60
+ assert.match(js, /<details class="mod-more"/, 'a details disclosure opens the rest');
61
+ assert.match(js, /<div class="ov-row__facts">\$\{provenancePill\(m\)\}\$\{attributionFacts\(m\)\}\$\{upkeepFact\(m\)\}<\/div>/, 'provenance, credit and upkeep are the facts line');
62
+ assert.match(js, /<div class="ov-row__right mod-row__right">\$\{rightHtml\(m\)\}<\/div>/, 'the right column is one function');
63
+ const css = read('modules.css');
64
+ assert.match(css, /\.mod-row__desc \{[^}]*-webkit-line-clamp: 2/, 'the description clamps to two lines');
65
+ assert.match(css, /\.mod-row:has\(\.mod-more\[open\]\) \.mod-row__desc/, 'and lifts the clamp while the details are open');
66
+ assert.doesNotMatch(decomment(js), /class="mod-chip|innerHTML = .*style=|<span style=/, 'no .mod-chip, no inline style');
67
+ });
68
+
69
+ test('modules: the switch is the family grant toggle or a state pill — never an accent fill, never a dead control', () => {
70
+ const js = read('modules.js');
71
+ assert.match(js, /class="ov-toggle mod-toggle" aria-pressed=/, 'the toggle is .ov-toggle with aria-pressed');
72
+ assert.match(js, /: `<span class="fact-pill \$\{on \? 'fact-pill--ok' : ''\}"/, 'a viewer below the floor sees the state as a pill');
73
+ assert.match(js, /rankLabel\('metic'\)/, 'the floor is written from the pack');
74
+ assert.doesNotMatch(js, /Metic\+/, 'and never as a hardcoded rank');
75
+ const css = decomment(read('modules.css'));
76
+ assert.doesNotMatch(css, /background:\s*var\(--accent\)/, 'no accent fill on a state');
77
+ assert.doesNotMatch(css, /button\.mod-toggle|\[data-on=/, 'the old accent-filled toggle is gone');
78
+ assert.match(js, /class="btn-ghost btn--sm mod-submit" data-submit-key=/, 'the submit door is a ghost pill on the row');
79
+ });
80
+
81
+ test('modules: upkeep colours the exception — undeclared is the warn pill, maintained a plain fact', () => {
82
+ const js = read('modules.js');
83
+ assert.match(js, /fact-pill fact-pill--warn"[^>]*>upkeep undeclared</, 'undeclared upkeep is the warn pill');
84
+ assert.match(js, /<span class="ov-fact">upkeep <b>\$\{escapeHtml\(status\)\}<\/b><\/span>/, 'a maintained module reads as a plain fact');
85
+ assert.match(js, /fact-pill fact-pill--info">core-loaded</, 'core-loaded is the info pill');
86
+ assert.match(js, /<span class="fact-chip">\$\{escapeHtml\(m\.license\)\}<\/span>/, 'the licence is a chip');
87
+ });
88
+
89
+ test('modules: the kit filter bar carries the search and a state lens, and the empty state is the kit\'s', () => {
90
+ const js = read('modules.js');
91
+ assert.match(js, /kit\.makeFilterBar\(\{/, 'the bar is the kit\'s');
92
+ assert.match(js, /search: \{ id: 'mod-search'/, 'the search keeps its id (the states file fills it)');
93
+ assert.match(js, /\{ id: 'mod-state', label: 'State'/, 'a state lens');
94
+ assert.match(js, /kit\.emptyStateHtml\(/, 'empties are the kit\'s');
95
+ assert.doesNotMatch(decomment(js), /mod-empty/, '.mod-empty is gone');
96
+ const html = read('modules.html');
97
+ assert.match(html, /<script src="\/builders\/hall-kit\.js"><\/script>/, 'and the page loads the kit');
98
+ });
99
+
100
+ test('modules: the figure band carries modules, enabled, built here and undeclared upkeep', () => {
101
+ const js = read('modules.js');
102
+ for (const label of ['modules', 'enabled', 'built here', 'no upkeep']) {
103
+ assert.match(js, new RegExp(`fig\\('${label}'`), `figure: ${label}`);
104
+ }
105
+ });
106
+
107
+ test('modules.css spends only the fifteen, and every token it names exists', () => {
108
+ const css = read('modules.css');
109
+ noAlias(css, 'modules.css');
110
+ tokensDefined(css, 'modules.css');
111
+ assert.doesNotMatch(decomment(css), /#[0-9a-f]{3,8}\b/i, 'no literal colours');
112
+ });
113
+
114
+ test('the shell no longer aliases .mod-chip onto the pill', () => {
115
+ const alias = STYLE.slice(STYLE.indexOf('.fact-pill,\n'), STYLE.indexOf('{', STYLE.indexOf('.fact-pill,\n')));
116
+ assert.doesNotMatch(alias, /mod-chip/, 'modules takes .fact-pill by name');
117
+ });
118
+
119
+ // ---- atlas ------------------------------------------------------------------
120
+
121
+ test('atlas: the nine semantic colours stay fixed literals, and nothing else in the sheet is one', () => {
122
+ const css = read('atlas.css');
123
+ for (const t of ['--ab-sun', '--ab-sky', '--ab-coral', '--ab-paper', '--ab-good', '--ab-core', '--ab-db', '--ab-ghost', '--ab-danger']) {
124
+ assert.match(css, new RegExp(`${t}:#[0-9a-f]{6}`), `${t} is a fixed literal (ADR 0094)`);
125
+ }
126
+ const rest = decomment(css).replace(/--ab-(sun|sky|coral|paper|good|core|db|ghost|danger):#[0-9a-f]{6}/g, '');
127
+ assert.deepEqual(rest.match(/#[0-9a-f]{3,8}\b/gi) || [], [], 'no other literal colour');
128
+ noAlias(css, 'atlas.css');
129
+ tokensDefined(css, 'atlas.css');
130
+ assert.doesNotMatch(rest, /box-shadow/, 'no box-shadow: the world draws depth with hairlines');
131
+ });
132
+
133
+ test('atlas: the panel is the shell\'s .scroll and the view switch is the shell\'s tab strip', () => {
134
+ const html = read('atlas.html');
135
+ assert.match(html, /<section class="scroll atlas-page"/, 'the map sits on a .scroll');
136
+ assert.match(html, /<div class="kit-tabs ab-views" role="tablist"/, 'the view switch is .kit-tabs');
137
+ assert.equal((html.match(/class="setup__tab" role="tab" data-view=/g) || []).length, 2, 'two tabs');
138
+ assert.doesNotMatch(decomment(html), /ab-modes|ab-chip"|class="card"/, 'the segmented box, the live chip and the empty card are gone');
139
+ const css = decomment(read('atlas.css'));
140
+ assert.doesNotMatch(css, /\.ab-modes|\.ab-chip\b|radial-gradient|\.ab-empty \.card/, 'and their rules with them');
141
+ const js = read('atlas.js');
142
+ assert.match(js, /setAttribute\('aria-selected'/, 'the script switches the tabs by aria-selected');
143
+ assert.doesNotMatch(decomment(js), /classList\.toggle\('active'/, 'not by an .active class');
144
+ });
145
+
146
+ test('atlas: the detail card is a hairline panel whose facts are the shell\'s pills and chips, and a neighbour is a door', () => {
147
+ const css = read('atlas.css');
148
+ assert.match(css, /\.ab-popup\{[^}]*border:var\(--line\) solid var\(--rule\)/, 'the card closes on one rule hairline');
149
+ const js = read('atlas.js');
150
+ assert.match(js, /fact-pill fact-pill--' \+ \(PILL\[n\.badge\.k\]/, 'the badge is a status pill');
151
+ assert.match(js, /<span class="fact-chip">' \+ esc\(n\.metric\)/, 'the metric is a chip');
152
+ assert.match(js, /<button type="button" class="fact-chip ab-jump" data-node=/, 'a connected island is a button');
153
+ assert.doesNotMatch(decomment(js), /ab-chip2|ab-badge ab-b-|↔/, 'the glass chips, the private badges and the arrow glyph are gone');
154
+ assert.match(js, /ev\.key === 'Escape'/, 'Escape closes the card');
155
+ });
156
+
157
+ test('atlas: the island index renders every node of the view as a row a keyboard can open', () => {
158
+ const html = read('atlas.html');
159
+ assert.match(html, /<details class="kit-collapse ab-index" id="ab-index">/, 'the index is a collapsible under the map');
160
+ assert.match(html, /<ul class="ov-rows" id="ab-index-rows"><\/ul>/, 'its rows are the family\'s');
161
+ const js = read('atlas.js');
162
+ assert.match(js, /function drawIndex\(d\)/, 'the index is drawn per view');
163
+ assert.match(js, /<li class="ov-row ov-row--dense ab-ix" data-node=/, 'each island is a dense row');
164
+ assert.match(js, /class="ov-row__title ab-ix__t" aria-expanded="false" aria-controls=/, 'the title is a disclosure button');
165
+ assert.match(js, /body\.innerHTML = detailHTML\(nodeById\(id\)\)/, 'and opens the same detail the map\'s card shows');
166
+ assert.match(js, /matchMedia\('\(max-width: 640px\)'\)\.matches\) indexEl\.open = true/, 'open by default on a phone, where the index is the read');
167
+ assert.match(js, /drawIndex\(d\);/, 'drawn on every render');
168
+ });
169
+
170
+ test('atlas: the hub breathes at the world\'s ambient floor, not as a pulse', () => {
171
+ const css = read('atlas.css');
172
+ const m = css.match(/animation:abbreathe (\d+)s/);
173
+ assert.ok(m && Number(m[1]) >= 20, 'the loop is 20s or slower');
174
+ });
175
+
176
+ test('the states files exercise the redesign\'s new states', () => {
177
+ const mod = JSON.parse(read('modules.states.json'));
178
+ for (const s of ['browse', 'search-empty', 'lens', 'details', 'out']) assert.ok(mod.states[s], `modules: ${s}`);
179
+ const atlas = JSON.parse(read('atlas.states.json'));
180
+ for (const s of ['map', 'truth', 'detail', 'index', 'out']) assert.ok(atlas.states[s], `atlas: ${s}`);
181
+ });
@@ -63,9 +63,11 @@ const ORACLE = {
63
63
  '/builders/sessions': 'requireGovernmentPage',
64
64
 
65
65
  // Metic+ via the `project.curate` atom — the exact permission GET /scouting
66
- // checks, so neither page can drift wider than the API it fronts.
66
+ // checks, so the page can never drift wider than the API it fronts. Its
67
+ // legacy twin /builders/scouting is gone (task 1003440): the URL now 301s to
68
+ // /people before this dispatch runs, so it holds no gate and has no row —
69
+ // tests/hall_roster_world.mjs owns the proof that the redirect is there.
67
70
  '/builders/people': 'requireProjectCuratePage',
68
- '/builders/scouting': 'requireProjectCuratePage',
69
71
 
70
72
  // Metic+ since R16 (goal 1000069): the Government page is three rooms —
71
73
  // Permissions · Board Room · Constitution (reordered, task 1003269) — and only
@@ -364,8 +364,8 @@ test('no reading room carries an inline <style>, and none needs an audit excepti
364
364
  assert.ok(budgets, 'hall_audit.mjs still declares the ratchet');
365
365
  assert.doesNotMatch(budgets[1].replace(/\/\/[^\n]*/g, ''), /['"][a-z-]+\.html['"]\s*:/,
366
366
  'no page carries an inline-style exception any more');
367
- // (scouting.html keeps a one-line block, comfortably inside the DEFAULT
368
- // budget — an exception is what this asserts is gone, not every <style>.)
367
+ // (an exception is what this asserts is gone, not every <style>. scouting.html
368
+ // was the last page carrying a one-line block; task 1003440 deleted the page.)
369
369
  });
370
370
 
371
371
  test('roadmap\'s two sub-24px controls clear the floor, on both axes', () => {
@@ -5,8 +5,8 @@
5
5
  // in the open and the rest behind the kit's disclosure, the readiness is the
6
6
  // family's figure band in the same dot language as the roll line, the view
7
7
  // toggle is a view and not a filter, the accent lands on the one live door; the
8
- // two directory pages are ONE page (the legacy door renders the same body from
9
- // the same script) and the directory shows the avatar, the name and the login
8
+ // legacy /scouting door is deleted and its URL redirects onto the one directory
9
+ // page (task 1003440), which shows the avatar, the name and the login
10
10
  // the API returns; the session ledger drops the nowrap + wrapper pair that broke
11
11
  // its sticky header, makes every id a door, and names its floor from the pack.
12
12
  import test from 'node:test';
@@ -165,17 +165,47 @@ test('goals.css and people.css spend only the fifteen, and every token they name
165
165
  });
166
166
 
167
167
  // =========================================================================
168
- // PEOPLE + SCOUTING — one directory, two doors
168
+ // PEOPLE — one directory, one door
169
169
  // =========================================================================
170
170
 
171
- test('scouting is the same page as people: one body, one script, no script of its own', () => {
171
+ // Task 1003470 made /scouting render people.html byte-for-byte; task 1003440
172
+ // removes the door itself. Two URLs onto one directory is a surface to keep in
173
+ // sync that no reader ever asked for — but the URL still has to answer, or a
174
+ // bookmark on the old door 404s.
175
+ test('the legacy /scouting door is deleted, and nothing scouting-shaped is left in the hall', () => {
176
+ for (const f of ['scouting.html', 'scouting.js', 'scouting.states.json', 'scouting.css']) {
177
+ assert.ok(!fs.existsSync(path.join(PUB, f)), f + ' is gone — one directory, one door');
178
+ }
179
+ });
180
+
181
+ test('the /scouting URL redirects to /people, and no dead gate dispatches to it', () => {
182
+ const serve = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'), 'utf8');
183
+ const i = serve.indexOf('const SCOUTING_LEGACY_RE');
184
+ assert.ok(i > 0, 'the retired URL keeps a matcher, so the bookmark still lands somewhere');
185
+ const block = serve.slice(i, i + 1200);
186
+ assert.ok(block.includes('res.redirect(301,'), 'and answers with a permanent redirect');
187
+ assert.ok(block.includes("/people"), 'pointing at the directory that replaced it');
188
+ // RUN the shipped matcher, rather than trusting that it reads right. Anchoring
189
+ // is the half that goes wrong: an unanchored twin would swallow the API route.
190
+ const decl = 'const SCOUTING_LEGACY_RE = ';
191
+ const j = serve.indexOf(decl);
192
+ const rx = eval(serve.slice(j + decl.length, serve.indexOf(';', j))); // eslint-disable-line no-eval
193
+ for (const p of ['/builders/scouting', '/builders/scouting/', '/builders/scouting.html']) {
194
+ assert.ok(rx.test(p), p + ' must reach the redirect');
195
+ }
196
+ for (const p of ['/api/gds/scouting', '/api/bongos/scouting', '/builders/people',
197
+ '/builders/scoutingfoo', '/builders/scouting/extra']) {
198
+ assert.ok(!rx.test(p), p + ' must NOT be redirected — the data route and the directory are not the door');
199
+ }
200
+ // The gate is the half that would rot silently: a page-gate branch still
201
+ // naming a deleted page is a wall guarding nothing.
202
+ assert.ok(!decomment(serve).includes('SCOUTING_PAGE_RE'),
203
+ 'no page gate is left dispatching to a page that no longer exists');
204
+ });
205
+
206
+ test('the directory page loads its own script, sheet and toolkit', () => {
172
207
  const people = read('people.html');
173
- const scouting = read('scouting.html');
174
- const bodyOf = (html) => html.slice(html.indexOf('<body'));
175
- assert.equal(bodyOf(scouting), bodyOf(people), 'the two bodies are byte-identical');
176
- assert.ok(!fs.existsSync(path.join(PUB, 'scouting.js')), 'scouting.js is gone — the third rendering of one endpoint');
177
- assert.doesNotMatch(scouting, /<style[^>]*>/, 'the legacy door carries no inline style');
178
- for (const html of [people, scouting]) {
208
+ for (const html of [people]) {
179
209
  assert.match(html, /\/builders\/people\.js/);
180
210
  assert.match(html, /\/builders\/people\.css/);
181
211
  assert.match(html, /\/builders\/dom-utils\.js/, 'the toolkit the rank label and the number formats come from');
@@ -280,8 +310,6 @@ test('the states files exercise the redesign\'s new states', () => {
280
310
  assert.deepEqual(goals.states.cards.expect.value, [['#f-goal-view', 'cards']]);
281
311
  const people = JSON.parse(read('people.states.json'));
282
312
  assert.ok(people.states.search, 'people: a search is measured');
283
- const scouting = JSON.parse(read('scouting.states.json'));
284
- assert.deepEqual(scouting.states.board.expect.visible, ['#people-scroll'], 'scouting expects the People panel');
285
313
  const sessions = JSON.parse(read('sessions.states.json'));
286
314
  assert.deepEqual(sessions.states.ledger.expect.visible, ['#stats-scroll', '#sessions-scroll']);
287
315
  });
Binary file
@@ -1,69 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>People — {{worldName}}</title>
7
- <meta name="description" content="The builder directory: everyone building across {{worldName}} projects, searchable and ranked by track record.">
8
- <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <!-- /scouting is the LEGACY DOOR to the People directory (task 1002577
13
- graduated it into the hall; task 1002710 walled it on the same
14
- project.curate gate). Task 1003470 made it the SAME PAGE: the body below
15
- is byte-identical to people.html and runs people.js — the one
16
- /api/gds/scouting rollup used to be drawn three different ways, and a
17
- door that renders a different page from the same data is the drift.
18
- tests/hall_roster_world.mjs holds the two bodies equal. -->
19
- <link rel="stylesheet" href="/builders/people.css">
20
- </head>
21
- <body data-page="people" data-page-title="People">
22
-
23
- <div class="app">
24
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
25
- <div class="app-scrim" id="app-scrim" hidden></div>
26
- <div class="app-body">
27
- <header class="app-topbar" id="app-topbar"></header>
28
- <div class="app-content">
29
- <!-- The board page head (task 1003307): the label voice, because on a
30
- working surface the page's own name is the least interesting thing on
31
- it. The sub is rewritten by people.js once the directory has loaded. -->
32
- <div class="page-head page-head--board">
33
- <h1 class="page-head__h">People</h1>
34
- <p class="page-head__sub" id="people-sub">Everyone building across hosted projects, ranked by track record. Descriptive only — cross-project numbers never carry authority here.</p>
35
- </div>
36
-
37
- <main id="hall-main">
38
- <!-- task 1003470: ONE directory. /people and its legacy door /scouting
39
- render this same page from the same script — the same
40
- /api/gds/scouting rollup used to be drawn three different ways. -->
41
- <section class="scroll" id="people-scroll" aria-labelledby="people-h">
42
- <h2 id="people-h" class="scroll__h">Builder directory<span class="count-pill" id="people-count" hidden></span></h2>
43
- <div id="people-filters"></div>
44
- <div id="people-body">
45
- <div class="profile__placeholder">Loading the directory…</div>
46
- </div>
47
- </section>
48
- </main>
49
-
50
- <footer class="app-foot" id="app-foot"></footer>
51
- </div>
52
- </div>
53
- </div>
54
-
55
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
-
57
- <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
58
- chrome exists before the page script runs. -->
59
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
60
- <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
- <script src="/builders/palette.js" defer></script>
62
- <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
63
- branding pack's currency label, loaded BEFORE people.js. -->
64
- <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
65
- <script src="/builders/brand-apply.js"></script>
66
- <script src="/builders/hall-kit.js"></script>
67
- <script src="/builders/people.js"></script>
68
- </body>
69
- </html>
@@ -1,59 +0,0 @@
1
- {
2
- "_": "Scouting — the LEGACY DOOR to the People directory, on the same project.curate gate. Since task 1003470 it renders the People page itself (the same body, the same people.js), so this file measures the door, not a second design: the same floors at the same widths, with the People panel expected. The three-line inline <style> it used to carry went with the page it belonged to.",
3
- "page": "/builders/scouting",
4
- "surface": "hall-ui",
5
- "stub": {
6
- "prefix": "/builders/"
7
- },
8
- "modeQuery": false,
9
- "_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
10
- "ignoreRequests": [
11
- "/api/(gds|bongos)/me$",
12
- "/api/(gds|bongos)/me/",
13
- "/api/(gds|bongos)/goals",
14
- "/api/(gds|bongos)/inbox/",
15
- "/api/(gds|bongos)/versions",
16
- "/api/(gds|bongos)/scouting",
17
- "/api/(gds|bongos)/builders"
18
- ],
19
- "ignoreConsole": [
20
- "/api/(gds|bongos)/",
21
- "\\b401\\b",
22
- "\\b403\\b",
23
- "\\b404\\b",
24
- "\\b501\\b",
25
- "Unauthorized",
26
- "Forbidden",
27
- "Not Found",
28
- "Not Implemented",
29
- "load failed"
30
- ],
31
- "states": {
32
- "board": {
33
- "auth": true,
34
- "actions": [
35
- [
36
- "wait",
37
- 700
38
- ]
39
- ],
40
- "expect": {
41
- "visible": [
42
- "#people-scroll"
43
- ]
44
- }
45
- },
46
- "out": {
47
- "auth": false
48
- }
49
- },
50
- "reducedMotion": {
51
- "auth": true,
52
- "actions": [
53
- [
54
- "wait",
55
- 600
56
- ]
57
- ]
58
- }
59
- }