@bongos/core 1.19.1014 → 1.19.1015

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 (59) hide show
  1. package/.bongos-core.json +61 -81
  2. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
  3. package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
  4. package/docs/copy-inventory.md +139 -110
  5. package/docs/copy-registry.json +872 -296
  6. package/docs/module-api-changelog.md +2 -0
  7. package/modules/hall-ui/public/atlas.html +1 -1
  8. package/modules/hall-ui/public/blockers.html +1 -1
  9. package/modules/hall-ui/public/board-room.html +1 -1
  10. package/modules/hall-ui/public/collab.html +1 -1
  11. package/modules/hall-ui/public/copy-desk.html +1 -1
  12. package/modules/hall-ui/public/deploy.html +1 -1
  13. package/modules/hall-ui/public/diagrams.html +1 -1
  14. package/modules/hall-ui/public/drachmae.html +1 -1
  15. package/modules/hall-ui/public/fleet.html +1 -1
  16. package/modules/hall-ui/public/gate.html +1 -1
  17. package/modules/hall-ui/public/goal-map.html +125 -52
  18. package/modules/hall-ui/public/goal-map.states.json +29 -5
  19. package/modules/hall-ui/public/goals.html +1 -1
  20. package/modules/hall-ui/public/government.html +1 -1
  21. package/modules/hall-ui/public/idea.html +1 -1
  22. package/modules/hall-ui/public/ideas.html +1 -1
  23. package/modules/hall-ui/public/index.html +1 -1
  24. package/modules/hall-ui/public/modules.html +1 -1
  25. package/modules/hall-ui/public/people.html +1 -1
  26. package/modules/hall-ui/public/primer.html +1 -1
  27. package/modules/hall-ui/public/profile.html +1 -1
  28. package/modules/hall-ui/public/project-settings.html +1 -1
  29. package/modules/hall-ui/public/ranks.html +1 -1
  30. package/modules/hall-ui/public/roadmap.html +1 -1
  31. package/modules/hall-ui/public/roster.html +1 -1
  32. package/modules/hall-ui/public/sessions.html +1 -1
  33. package/modules/hall-ui/public/settings.html +1 -1
  34. package/modules/hall-ui/public/shell.js +11 -0
  35. package/modules/hall-ui/public/sky-data.js +19 -1
  36. package/modules/hall-ui/public/sky-interaction.js +14 -7
  37. package/modules/hall-ui/public/sky-layout.js +2 -1
  38. package/modules/hall-ui/public/sky-tour.js +10 -6
  39. package/modules/hall-ui/public/studio.html +1 -1
  40. package/modules/hall-ui/public/task.html +1 -1
  41. package/modules/hall-ui/public/thinking.css +6 -3
  42. package/modules/hall-ui/public/thinking.html +16 -10
  43. package/modules/hall-ui/public/watch.html +1 -1
  44. package/modules/hall-ui/public/work.html +1 -1
  45. package/package-lock.json +2 -2
  46. package/package.json +1 -1
  47. package/src/bongos/serve-internal.js +4 -0
  48. package/src/module-api.js +1 -1
  49. package/tests/goal_map_page.mjs +346 -211
  50. package/tests/hall_audit.mjs +1 -0
  51. package/tests/hall_nav.mjs +6 -1
  52. package/tests/hall_page_gate_map.mjs +3 -2
  53. package/tests/helpers.mjs +21 -5
  54. package/tests/nav_permission_atoms.mjs +1 -1
  55. package/tests/render_check.mjs +6 -4
  56. package/modules/hall-ui/public/goal-map-lib.js +0 -349
  57. package/modules/hall-ui/public/goal-map.css +0 -117
  58. package/modules/hall-ui/public/goal-map.js +0 -232
  59. package/tests/goal_map_lib.mjs +0 -214
@@ -1,267 +1,402 @@
1
- // tests/goal_map_page.mjs — the goal map page, EXECUTED (task 1003479, goal
2
- // 1000089; ADR 0227 §5 and §7).
1
+ // tests/goal_map_page.mjs — THE PROJECT SKY at /goal-map, EXECUTED (task
2
+ // 1004232 / sky part 4 of 5, goal 1000095; ADR 0331, ADR 0227 §11).
3
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.
4
+ // The page that held this URL was the goal map (task 1003479): a layered
5
+ // node-link picture with a table under it, no control, no nav slot. This
6
+ // suite replaced the one that pinned it. The sky the owner adopted as the
7
+ // design of record (docs/design/mocks/sky/) took the URL, in its "the whole
8
+ // project" lens, and the nav slot the map was denied (ADR 0227 §11). What the
9
+ // old page promised and the new one must keep is asserted here as behaviour:
10
+ // - the accessible path — every goal is a row in the ledger drawer, with
11
+ // both directions of each dependency by name (the old table's whole job);
12
+ // - ADR 0227 §1's refusal — tasks resolve only inside ONE well past the zoom
13
+ // threshold, never for every goal at once, and no control asks for them;
14
+ // - the nav slot, granted this time, directly under Goals.
15
+ // And what the template adds: wells sized by credits with settled and
16
+ // collapsed mass, alive as motion, routes charted by a shuttle that pulls the
17
+ // camera back, the shuttle's five-line menu, explore on by default with
18
+ // "where do you begin", "my ideas" off by default, the clock, band click.
19
+ //
20
+ // The REAL goal-map.html markup and the REAL seven sky-*.js files boot in a vm
21
+ // through bootSkyPage (tests/helpers.mjs) — the same helper tests/hall_sky.mjs
22
+ // uses for /thinking, pointed at this page's own file, so a button lost from
23
+ // either door fails its own suite.
16
24
  //
17
25
  // Run: node tests/goal_map_page.mjs
18
26
  import assert from 'node:assert/strict';
19
27
  import fs from 'node:fs';
20
28
  import path from 'node:path';
21
- import vm from 'node:vm';
22
29
  import { fileURLToPath } from 'node:url';
23
- import { createRequire } from 'node:module';
24
- import { makeRunner, makeEl, walk } from './helpers.mjs';
30
+ import { makeRunner, settle, bootSkyPage } from './helpers.mjs';
25
31
 
26
32
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
33
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
28
34
  const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
29
- const KIT_JS = read('hall-kit.js');
30
- const LIB_JS = read('goal-map-lib.js');
31
- const FIT_JS = read('text-fit.js');
32
- const PAGE_JS = read('goal-map.js');
33
- const PAGE_HTML = read('goal-map.html');
34
- const PAGE_CSS = read('goal-map.css');
35
- const FIXTURE = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/hall-preview/fixtures/goals__graph.json'), 'utf8'));
36
35
 
37
36
  const { test, summary } = makeRunner();
38
- const tick = () => new Promise((r) => setTimeout(r, 0));
39
- const settle = async () => { for (let i = 0; i < 20; i++) await tick(); };
40
-
41
- 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 });
42
-
43
- async function boot(payload, { status = 200, fail = false } = {}) {
44
- const root = makeEl('div');
45
- const reg = (id, hidden = false) => { const el = makeEl('div', { id }); if (hidden) el.hidden = true; root.appendChild(el); return el; };
46
- ['gm-sub', 'gm-graph', 'gm-legend', 'gm-alone', 'gm-alone-lede', 'gm-table'].forEach((id) => reg(id));
47
- ['gm-alone-scroll', 'gm-table-scroll'].forEach((id) => reg(id, true));
48
-
49
- const documentObj = {
50
- getElementById: (id) => walk(root, (el) => (el.id === id ? el : null)),
51
- // SVG elements: the stub's own class/id only follow attrs at construction, so
52
- // mirror what a browser does when setAttribute writes them.
53
- createElementNS(_ns, tag) {
54
- const el = makeEl(tag);
55
- const orig = el.setAttribute.bind(el);
56
- el.setAttribute = (k, v) => { orig(k, v); if (k === 'class') el.className = String(v); if (k === 'id') el.id = String(v); };
57
- // A proportional fake of SVGTextElement's measurement: a W is twice a letter.
58
- if (tag === 'text') el.getComputedTextLength = () => Array.from(el.textContent).reduce((w, c) => w + (c === 'W' ? 12 : 6), 0);
59
- return el;
60
- },
61
- };
62
- const requests = [];
63
- const navigations = [];
64
- const locationObj = { hostname: 'builders.example.test', assign: (u) => navigations.push(u) };
65
- const windowObj = {
66
- OTB: { escapeHtml: (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])) },
67
- localStorage: { getItem: () => null, setItem() {} },
68
- location: locationObj,
69
- addEventListener() {},
70
- };
71
- const sandbox = {
72
- window: windowObj, self: windowObj, document: documentObj, location: locationObj, console: { error() {}, log() {}, warn() {} },
73
- setTimeout, clearTimeout, Promise, Date, Math, JSON, Error, Number, String, Object, Array, Boolean, Set, Map, RegExp, encodeURIComponent,
74
- fetch: async (url) => {
75
- requests.push(url);
76
- if (fail) throw new Error('offline');
77
- return { ok: status === 200, status, json: async () => payload };
78
- },
37
+ const drain = async (n = 30) => { for (let i = 0; i < n; i += 1) await settle(); };
38
+ // Explore is ON by default (the template's rule) and remembers what you
39
+ // reached; a case about the drawn sky rather than about explore turns it off,
40
+ // the way a reader who pressed "show me everything" has.
41
+ const boot = async (opts = {}) => { const p = bootSkyPage({ page: 'goal-map', storage: { 'sky-explore': 'off' }, ...opts }); await drain(); return p; };
42
+ const fire = (el) => el.fire('click', { type: 'click', preventDefault() {} });
43
+
44
+ // ── the rows, in the routes' own shapes ─────────────────────────────────────
45
+
46
+ // A well as GET /sky emits one. Defaults: an open goal in the present, three
47
+ // tasks on two rings, one held now.
48
+ function well(over = {}) {
49
+ return {
50
+ goal_id: '7', title: 'The well — a goal that holds work', subtitle: null, status: 'open', version_id: 'V2', category: null,
51
+ task_total: 3, task_done: 1, created_at: '2026-06-01T00:00:00.000Z',
52
+ criteria: [
53
+ { id: '70', criterion_id: 'c1', criterion_md: 'The first ring is met', satisfied: true, pending_review: false, task_ids: ['500'] },
54
+ { id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
55
+ ],
56
+ tasks: [
57
+ { id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
58
+ { id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
59
+ { id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
60
+ ],
61
+ ...over,
79
62
  };
80
- sandbox.globalThis = sandbox;
81
- vm.createContext(sandbox);
82
- vm.runInContext(KIT_JS, sandbox, { filename: 'hall-kit.js' });
83
- vm.runInContext(LIB_JS, sandbox, { filename: 'goal-map-lib.js' });
84
- vm.runInContext(FIT_JS, sandbox, { filename: 'text-fit.js' });
85
- vm.runInContext(PAGE_JS, sandbox, { filename: 'goal-map.js' });
86
- await settle();
87
- const $ = (id) => documentObj.getElementById(id);
88
- // The stub matches one compound selector at a time, so `tbody tr` is two steps.
89
- const rows = () => { const tb = $('gm-table').querySelector('tbody'); return tb ? tb.querySelectorAll('tr') : []; };
90
- return { $, requests, navigations, rows, svg: () => $('gm-graph').querySelector('svg') };
91
63
  }
64
+ const tasksOf = (prefix, n, status, created = '2026-05-01') => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, title: `task ${prefix}${i}`, status, credits_reward: 20, created_on: created, idea_id: null }));
65
+ // A quiet well: nothing held, nothing landed this week.
66
+ const quiet = (over = {}) => well({ goal_id: '9', title: 'A quiet well — nothing moves', task_total: 4, task_done: 4, criteria: [], tasks: tasksOf('9-', 4, 'shipped'), ...over });
67
+ const settled = (over = {}) => well({ goal_id: '8', title: 'A settled system — it finished', status: 'achieved', task_total: 3, task_done: 3, tasks: tasksOf('8-', 3, 'shipped'), created_at: '2026-03-01T00:00:00.000Z', ...over });
68
+ const collapsed = (over = {}) => well({ goal_id: '6', title: 'A collapsed goal — archived', status: 'archived', task_total: 3, task_done: 1, criteria: [], tasks: tasksOf('6-', 3, 'abandoned'), created_at: '2026-03-01T00:00:00.000Z', ...over });
69
+ const sky = (goals, edges = []) => ({ goals, edges, counts: { goals: goals.length, edges: edges.length, criteria: 0, tasks: 0 }, cache: { hit: false, ttl_seconds: 60, taken_at: '2026-09-25T00:00:00.000Z' } });
70
+ const versions = () => ({ versions: [
71
+ { id: 'V2', status: 'building', shipped_at: null, started_at: '2026-08-01T00:00:00.000Z' },
72
+ { id: 'V1', status: 'shipped', shipped_at: '2026-05-12T00:00:00.000Z', started_at: '2026-05-01T00:00:00.000Z' },
73
+ ] });
74
+ const star = () => ({
75
+ id: 10, title: 'It ignited', kind: 'feature', status: 'promoted', captured_at: '2026-09-01T00:00:00.000Z',
76
+ triage_note: null, promoted_to_task_id: null, promoted_task_title: null, promoted_task_status: null, merged_into_idea_id: null, merged_into_idea_title: null, grade: 'quick', field_values: null,
77
+ lineage_tasks: [{ task_id: 500, origin: 'promote', title: 'The task', status: 'shipped', goal_id: 7 }], descent_tasks: [],
78
+ size: { credits: 240, payments: 4, scale: 0.86, historical: false },
79
+ state: { state: 'star', position: 'well', wellGoalId: '7', awaitingNod: false, brightness: 1, hasWork: true, attributable: true },
80
+ });
81
+ const mine = (ideas) => ({ builder_id: 95, ideas, total: ideas.length, cap: 1000, truncated: false });
82
+ // 7 waits on 9 (a route), 8 and 6 stand alone.
83
+ const FOUR = () => sky([well(), quiet(), settled(), collapsed()], [{ from: '7', to: '9', count: 2 }]);
84
+ const goal = (p, id) => p.S().goalById.get(String(id));
92
85
 
93
- // ---- the table is the page ---------------------------------------------------------
86
+ // ═══ the lens is home ════════════════════════════════════════════════════════
94
87
 
95
- await test('the TABLE is on the page by default — every goal is a row, and nothing hides it behind a toggle', async () => {
96
- const h = await boot(FIXTURE);
97
- assert.equal(h.$('gm-table-scroll').hidden, false);
98
- const rows = h.rows();
99
- assert.equal(rows.length, FIXTURE.nodes.length, 'stand-alone goals are rows too');
100
- assert.ok(h.$('gm-table').querySelector('table').getAttribute('aria-label'), 'the table is named for a screen reader');
88
+ await test('it opens in "the whole project": the brand line, the switcher, the tools and the figures are the project sky\'s', async () => {
89
+ const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
90
+ assert.ok(p.ready(), 'the page booted');
91
+ const S = p.S();
92
+ assert.equal(S.home, 'all', 'the body says which lens is home');
93
+ assert.equal(S.lens, 'all');
94
+ assert.equal(p.el('lensAll').getAttribute('aria-pressed'), 'true');
95
+ assert.equal(p.el('lensMine').getAttribute('aria-pressed'), 'false');
96
+ assert.equal(p.el('lensWord').textContent, '— the whole project');
97
+ assert.equal(p.el('lensWord').parentNode.textContent, 'the sky — the whole project', 'the h1 is the project sky\'s');
98
+ assert.equal(p.el('roomTitle').textContent, 'the ledger');
99
+ assert.equal(p.el('exploreBtn').hidden, false, 'explore is a tool here');
100
+ assert.equal(p.el('ideasBtn').hidden, false, 'so is "my ideas"');
101
+ assert.equal(p.el('claudeBtn').hidden, true, 'the ideator\'s pills are not');
102
+ assert.equal(p.el('influenceBtn').hidden, true);
103
+ assert.match(p.stats(), /<b>4<\/b> goals · <b>1<\/b> alive · <b>2<\/b> wells · <b>1<\/b> settled · <b>1<\/b> collapsed · <b>1<\/b> routes · <b>13<\/b> tasks/, 'the figures line is the goals\' line');
104
+ const urls = p.requests.map((r) => r.url);
105
+ for (const want of ['/api/bongos/sky', '/api/bongos/sky/mine', '/api/bongos/me', '/api/bongos/inbox/template']) assert.ok(urls.includes(want), `reads ${want}`);
106
+ assert.ok(!urls.some((u) => /goals\/graph/.test(u)), 'the goal map\'s aggregate read is gone — the sky\'s own reads carry the routes');
101
107
  });
102
108
 
103
- await test('the picture is the enhancement, and it is a role=img figure: no focus stops, no links inside the drawing', async () => {
104
- const h = await boot(FIXTURE);
105
- const svg = h.svg();
106
- assert.ok(svg, 'the map is drawn');
107
- assert.equal(svg.getAttribute('role'), 'img');
108
- assert.equal(svg.getAttribute('aria-labelledby'), 'gm-cap');
109
- assert.equal(svg.querySelectorAll('a').length, 0, 'thirty-six hidden interactive elements from a drawing is the wrong trade');
110
- assert.equal(svg.querySelectorAll('[tabindex]').length, 0);
111
- assert.match(h.$('gm-graph').querySelector('figcaption').textContent, /also in the table below/);
109
+ await test('the same seven files open /thinking in "your thinking" — one page, two doors', async () => {
110
+ const p = bootSkyPage({ page: 'thinking', sky: FOUR(), versions: versions() }); await drain();
111
+ assert.equal(p.S().home, 'mine');
112
+ assert.equal(p.S().lens, 'mine');
113
+ assert.equal(p.el('lensWord').parentNode.textContent, 'your sky — your thinking');
112
114
  });
113
115
 
114
- await test('every goal in the map is a table row too, and every drawn dependency is named in the table', async () => {
115
- const h = await boot(FIXTURE);
116
- const drawn = h.svg().querySelectorAll('.gm-node').length;
117
- assert.equal(drawn, FIXTURE.counts.connected);
118
- const tableText = h.$('gm-table').textContent;
119
- for (const e of FIXTURE.edges) {
120
- const dependent = FIXTURE.nodes.find((n) => n.goal_id === e.from);
121
- const dependency = FIXTURE.nodes.find((n) => n.goal_id === e.to);
122
- assert.ok(tableText.includes(dependency.title || 'A private goal'), `the table names ${dependency.title}`);
123
- assert.ok(tableText.includes(dependent.title || 'A private goal'));
124
- }
116
+ await test('the switcher is two doors: "simple" and "your thinking" leave for /thinking, and "the whole project" stays', async () => {
117
+ const a = await boot({ sky: FOUR() });
118
+ a.click('lensSimple');
119
+ assert.equal(a.navigated, '/thinking');
120
+ const b = await boot({ sky: FOUR() });
121
+ b.click('lensMine');
122
+ assert.equal(b.navigated, '/thinking');
123
+ const c = await boot({ sky: FOUR() });
124
+ c.click('lensAll');
125
+ assert.equal(c.navigated, null, 'already here');
126
+ assert.equal(c.S().lens, 'all');
125
127
  });
126
128
 
127
- // ---- category is text, not only colour ------------------------------------------------
129
+ // ═══ the wells ═══════════════════════════════════════════════════════════════
128
130
 
129
- await test('CATEGORY IS A WORD on every drawn node, not only a colour — and "No category" is said too', async () => {
130
- const h = await boot(FIXTURE);
131
- const cats = h.svg().querySelectorAll('.gm-cat').map((t) => t.textContent);
132
- assert.equal(cats.length, FIXTURE.counts.connected);
133
- assert.ok(cats.every((c) => c.length > 0));
134
- assert.ok(cats.includes('No category'), 'an uncategorised goal is legal and named as such (ADR 0192)');
135
- assert.ok(cats.some((c) => /Modules|Security|Builders Hall|Economics/.test(c)));
136
- assert.ok(h.$('gm-legend').textContent.includes('Modules'), 'and the legend spells each category out');
131
+ await test('a goal is a well sized by its credits; settled and collapsed goals keep their mass at 0.8× and 0.5×', async () => {
132
+ const same = (over) => ({ ...over, task_total: 3, task_done: 3, criteria: [], tasks: tasksOf(over.goal_id + '-', 3, 'shipped') });
133
+ const p = await boot({ sky: sky([well(same({ goal_id: '1', title: 'open' })), well(same({ goal_id: '2', title: 'done', status: 'achieved' })), well(same({ goal_id: '3', title: 'gone', status: 'archived' }))]), versions: versions() });
134
+ const [o, a, c] = ['1', '2', '3'].map((id) => goal(p, id));
135
+ assert.equal(o.credits, 60, 'mass is the credits across the goal\'s tasks');
136
+ assert.equal(o.mass, 60);
137
+ assert.ok(Math.abs(a.r - o.r * 0.8) < 1e-9, 'a settled system is 0.8× the well it would have been');
138
+ assert.ok(Math.abs(c.r - o.r * 0.5) < 1e-9, 'a collapsed goal is half');
139
+ const big = await boot({ sky: sky([well(same({ goal_id: '1' })), well({ ...same({ goal_id: '4' }), tasks: tasksOf('4-', 3, 'shipped').map((t) => ({ ...t, credits_reward: 400 })) })]) });
140
+ assert.ok(goal(big, '4').r > goal(big, '1').r, 'more credits, a bigger well');
137
141
  });
138
142
 
139
- await test('node text is fitted to its card by MEASURED width, clipped as a backstop, and kept whole in the tooltip and table (task 1004204)', async () => {
140
- const wide = 'W'.repeat(60);
141
- const long = 'Agents module — framework, runtime, and the whole event router rewrite';
142
- const h = await boot({
143
- nodes: [node(1, { title: wide, depended_on_by: 1 }), node(2, { title: 'Fix', depends_on: 1, category: { slug: 'x', label: long } })],
144
- edges: [{ from: '2', to: '1', count: 1 }],
145
- counts: {},
146
- });
147
- const lib = createRequire(import.meta.url)(path.join(HALL, 'goal-map-lib.js'));
148
- const inner = lib.layout(lib.analyse({ nodes: [node(1), node(2)], edges: [] })).nodeW - 32;
149
- const texts = h.svg().querySelectorAll('.gm-node').flatMap((g) => g.querySelectorAll('text'));
150
- assert.equal(texts.length, 6, 'name, category and task count on each of two nodes');
151
- for (const t of texts) {
152
- assert.ok(t.getComputedTextLength() <= inner, `"${t.textContent}" measures ${t.getComputedTextLength()}px, past the card's ${inner}px`);
153
- assert.equal(t.getAttribute('clip-path'), 'url(#gm-text-clip)');
154
- }
155
- assert.ok(h.svg().querySelector('#gm-text-clip'), 'the clip the lines point at exists');
156
- const names = h.svg().querySelectorAll('.gm-name').map((t) => t.textContent);
157
- assert.ok(names.includes('Fix'), 'a title that fits is left whole');
158
- assert.ok(names.some((n) => /^W+…$/.test(n)), 'a wide title is cut by width and says so');
159
- assert.ok(h.svg().querySelectorAll('.gm-cat').some((t) => t.textContent.endsWith('…')), 'the category line is fitted too');
160
- assert.ok(h.svg().querySelectorAll('title').some((x) => x.textContent.includes(wide)), 'the full title is still readable on hover');
161
- assert.ok(h.$('gm-table').textContent.includes(wide), 'and in the table');
143
+ await test('ALIVE IS MOTION, NOT STATUS: a builder holding a task now, or more than three landing this week', async () => {
144
+ const thisWeek = new Date(Date.now() - 2 * 86400000).toISOString().slice(0, 10);
145
+ const p = await boot({ sky: sky([
146
+ well(),
147
+ quiet(),
148
+ well({ goal_id: '11', title: 'Busy — four landed this week', criteria: [], tasks: tasksOf('11-', 4, 'shipped', thisWeek) }),
149
+ well({ goal_id: '12', title: 'Three landed — not yet alive', criteria: [], tasks: tasksOf('12-', 3, 'shipped', thisWeek) }),
150
+ settled({ tasks: [{ id: '8-0', title: 'held in a settled system', status: 'active', credits_reward: 20, created_on: thisWeek, idea_id: null }] }),
151
+ ]), versions: versions() });
152
+ assert.ok(goal(p, '7').live > 0, 'a held task lights the well');
153
+ assert.equal(goal(p, '9').live, 0, 'a quiet well stays white');
154
+ assert.ok(goal(p, '11').live > 0, 'four landings this week light it');
155
+ assert.equal(goal(p, '12').live, 0, 'three do not — the rule is MORE than three');
156
+ assert.equal(goal(p, '8').live, 0, 'a settled system is never alive, whatever its tasks say');
157
+ p.key('l');
158
+ const alive = p.el('filters').querySelectorAll('button').find((b) => b.dataset.f === 'alive');
159
+ assert.ok(alive, 'the ledger has an alive filter');
160
+ fire(alive);
161
+ assert.ok(p.rows().includes('data-id="7"') && p.rows().includes('data-id="11"'));
162
+ assert.ok(!p.rows().includes('data-id="9"') && !p.rows().includes('data-id="8"'));
162
163
  });
163
164
 
164
- // ---- the split is stated, the alone goals are a list ----------------------------------
165
+ // ═══ the routes and the shuttle ══════════════════════════════════════════════
166
+
167
+ await test('routes are the graph\'s edges; charting one pulls the camera back to hold both ends', async () => {
168
+ const p = await boot({ sky: FOUR(), versions: versions() });
169
+ const S = p.S(); const SKY = p.SKY();
170
+ assert.equal(S.edges.length, 1);
171
+ const e = S.edges[0];
172
+ assert.equal(e.a, goal(p, '7')); assert.equal(e.b, goal(p, '9')); assert.equal(e.count, 2);
173
+ assert.equal(S.charted.size, 0, 'a whisper until asked');
174
+ // Zoomed into the waiting goal, the far end is off screen.
175
+ S.cam.x = e.a.x; S.cam.y = e.a.y; S.cam.k = 5; S.flight = null;
176
+ SKY.chartRoute(e, e.a);
177
+ assert.ok(S.charted.has(e.id), 'the route is charted');
178
+ assert.equal(S.charted.get(e.id).done, true, 'under reduced motion the shuttle arrives at once');
179
+ assert.ok(S.cam.k < 5, 'the camera pulled back');
180
+ assert.doesNotThrow(() => p.frame(), 'and the charted route draws');
181
+ });
165
182
 
166
- await test('the stand-alone goals are a LIST, never dots, and the split is stated in the page head', async () => {
167
- const h = await boot(FIXTURE);
168
- const drawnIds = new Set(h.svg().querySelectorAll('.gm-node').map((g) => g.attrs.transform));
169
- assert.equal(drawnIds.size, FIXTURE.counts.connected);
170
- assert.equal(h.$('gm-alone-scroll').hidden, false);
171
- assert.equal(h.$('gm-alone').querySelectorAll('li').length, FIXTURE.counts.isolated);
172
- assert.match(h.$('gm-sub').textContent, new RegExp(`the other ${FIXTURE.counts.isolated} stand alone`));
183
+ await test('the shuttle\'s menu offers the five lines, and each does what it says', async () => {
184
+ const p = await boot({ sky: sky([well(), quiet(), settled(), collapsed()], [{ from: '7', to: '9', count: 2 }, { from: '9', to: '8', count: 1 }]), versions: versions() });
185
+ const S = p.S(); const SKY = p.SKY();
186
+ SKY.showMenu(S.edges[0], 10, 10);
187
+ const menu = p.el('menu');
188
+ assert.equal(menu.hidden, false);
189
+ const acts = menu.querySelectorAll('button').map((b) => [b.dataset.act, b.textContent]);
190
+ assert.deepEqual(acts.map((a) => a[0]), ['goal', 'all', 'latest', 'hide', 'clear']);
191
+ assert.match(acts[0][1], /^chart every route from /);
192
+ assert.equal(acts[1][1], 'map every route in the sky');
193
+ assert.equal(acts[2][1], 'trace only the latest');
194
+ assert.equal(acts[3][1], 'hide unrelated systems');
195
+ assert.equal(acts[4][1], 'land every shuttle');
196
+ const act = (k) => { SKY.showMenu(S.edges[0], 10, 10); fire(menu.querySelectorAll('button').find((b) => b.dataset.act === k)); };
197
+ act('all');
198
+ assert.equal(S.charted.size, 2, 'every route in the sky');
199
+ assert.equal(menu.hidden, true, 'the menu closes on a choice');
200
+ act('hide');
201
+ assert.ok(S.focusSet && S.focusSet.has('7') && S.focusSet.has('9') && S.focusSet.has('8') && !S.focusSet.has('6'), 'the unrelated system is faded');
202
+ assert.ok(SKY.goalAlpha(goal(p, '6'), Date.now()) <= 0.1);
203
+ act('hide');
204
+ assert.equal(S.focusSet, null, 'and shown again');
205
+ act('clear');
206
+ assert.equal(S.charted.size, 0, 'every shuttle landed');
207
+ act('goal');
208
+ assert.equal(S.charted.size, 1, 'every route from this goal — 7 has one');
209
+ SKY.clearCharts();
210
+ act('latest');
211
+ assert.equal(S.charted.size, 1, 'only the latest: both ends in the present band');
173
212
  });
174
213
 
175
- // ---- no view A, no view D -------------------------------------------------------------------
214
+ // ═══ explore ═════════════════════════════════════════════════════════════════
176
215
 
177
- 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 () => {
178
- const h = await boot(FIXTURE);
179
- assert.deepEqual(h.requests, ['/api/bongos/goals/graph'], 'the only read is the aggregate — it carries no task identity');
180
- const controls = /<(?:button|select|input|textarea)\b/i;
181
- assert.equal(controls.test(PAGE_HTML), false, 'the page markup has no control at all');
182
- assert.equal(h.$('gm-graph').querySelectorAll('button, select, input').length, 0);
183
- assert.equal(/include=|\/tasks|task_id|densif/i.test(PAGE_JS.replace(/^\s*\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')), false,
184
- 'no path in the page script can ask for a task');
216
+ await test('EXPLORE IS ON BY DEFAULT: the sky asks where you begin, and the ledger knows only what you have reached', async () => {
217
+ const p = await boot({ sky: FOUR(), versions: versions(), storage: {} });
218
+ const S = p.S(); const SKY = p.SKY();
219
+ assert.equal(S.explore.on, true);
220
+ assert.equal(p.el('exploreBtn').getAttribute('aria-pressed'), 'true');
221
+ assert.equal(p.el('start').hidden, false, '"where do you begin" is up');
222
+ const opts = p.el('startOpts').querySelectorAll('button');
223
+ assert.equal(opts[0].textContent, 'where my thoughts are');
224
+ assert.deepEqual(opts.slice(1, -1).map((b) => b.dataset.start), ['7', '9'], 'one door per open well, biggest first');
225
+ assert.equal(opts.at(-1).textContent, 'show me everything');
226
+ assert.equal(SKY.seen(goal(p, '7')), false, 'nothing is reached yet');
227
+ p.key('l');
228
+ assert.match(p.rows(), /Nothing explored yet\. Pick a starting point in the sky\./);
229
+ fire(opts.find((b) => b.dataset.start === '7'));
230
+ assert.equal(p.el('start').hidden, true);
231
+ assert.equal(SKY.seen(goal(p, '7')), true, 'the well you began in is revealed');
232
+ assert.equal(SKY.seen(goal(p, '9')), false, 'the one at the far end of its route is not');
233
+ assert.ok(p.rows().includes('data-id="7"') && !p.rows().includes('data-id="9"'), 'the ledger lists what you reached');
234
+ assert.equal(p.el('roomCount').textContent, '1 of 1 · 3 unexplored');
235
+ SKY.chartRoute(S.edges[0], goal(p, '7'));
236
+ assert.equal(SKY.seen(goal(p, '9')), true, 'charting the route brings the goal at its far end into view');
237
+ assert.ok(p.rows().includes('data-id="9"'));
238
+ assert.match(p.stats(), /explored 2 of 4/);
185
239
  });
186
240
 
187
- // ---- scrolling ----------------------------------------------------------------------------
188
-
189
- await test('the picture scrolls INSIDE its own region and names both overflow axes (the page never scrolls sideways)', async () => {
190
- const h = await boot(FIXTURE);
191
- const region = h.$('gm-graph').querySelector('.gm-scroll');
192
- assert.ok(region && region.contains(h.svg()));
193
- assert.equal(region.getAttribute('tabindex'), '0', 'a keyboard reader can scroll it');
194
- assert.equal(region.getAttribute('role'), 'region');
195
- const rule = PAGE_CSS.match(/\.gm-scroll\s*\{[^}]*\}/)[0];
196
- assert.match(rule, /overflow-x:\s*auto/);
197
- assert.match(rule, /overflow-y:\s*hidden/, 'one axis named alone makes the other auto (task 1004130)');
198
- assert.match(rule, /max-width:\s*100%/);
241
+ await test('"show me everything" turns explore off, and every goal is then a row', async () => {
242
+ const p = await boot({ sky: FOUR(), versions: versions(), storage: {} });
243
+ fire(p.el('startOpts').querySelectorAll('button').find((b) => b.dataset.start === 'all'));
244
+ assert.equal(p.S().explore.on, false);
245
+ assert.equal(p.el('exploreBtn').getAttribute('aria-pressed'), 'false');
246
+ p.key('l');
247
+ for (const id of ['7', '9', '8', '6']) assert.ok(p.rows().includes(`data-id="${id}"`), `goal ${id} is a row`);
248
+ assert.equal(p.el('roomCount').textContent, '4 of 4');
199
249
  });
200
250
 
201
- // ---- text is never markup -------------------------------------------------------------------
202
-
203
- await test('a goal title is builder-authored text: it never becomes markup, in the picture or the table or the chips', async () => {
204
- const evil = '<img src=x onerror=alert(1)>';
205
- const h = await boot({
206
- nodes: [node(1, { title: evil, depends_on: 1 }), node(2, { depended_on_by: 1 }), node(3, { title: evil })],
207
- edges: [{ from: '1', to: '2', count: 1 }],
208
- counts: {},
209
- });
210
- assert.equal(h.svg().querySelectorAll('img').length, 0);
211
- assert.ok(h.svg().querySelectorAll('.gm-name').some((t) => t.textContent.includes('<img')), 'as literal text in the drawing');
212
- assert.equal(h.$('gm-table').querySelectorAll('img').length, 0);
213
- assert.equal(h.$('gm-alone').querySelectorAll('img').length, 0);
214
- assert.ok(h.$('gm-table').textContent.includes(evil), 'shown as the characters the author typed');
251
+ // ═══ "my ideas" ══════════════════════════════════════════════════════════════
252
+
253
+ await test('"my ideas" is OFF by default: no thought is drawn over the project until the pill is pressed', async () => {
254
+ const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
255
+ const S = p.S(); const SKY = p.SKY();
256
+ const i = S.ideas.find((x) => x.id === '10');
257
+ assert.ok(i, 'the thought was read');
258
+ assert.equal(S.ideasOnMap, false);
259
+ assert.equal(p.el('ideasBtn').getAttribute('aria-pressed'), 'false');
260
+ assert.equal(SKY.shown(i), false, 'not drawn');
261
+ assert.equal(p.el('rehearse').hidden, true, 'and "what if" waits behind it');
262
+ p.click('ideasBtn');
263
+ assert.equal(SKY.shown(i), true, 'laid over the map');
264
+ assert.equal(p.el('rehearse').hidden, false);
215
265
  });
216
266
 
217
- // ---- honest states ----------------------------------------------------------------------------
267
+ // ═══ the ledger drawer — the accessible path ═════════════════════════════════
218
268
 
219
- await test('a private goal the viewer cannot read is named as one and is not a link', async () => {
220
- 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: {} });
221
- const row = h.rows().find((r) => r.textContent.includes('A private goal'));
222
- assert.ok(row, 'the row exists');
223
- assert.equal(row.querySelectorAll('a').length, 0, 'nothing to open');
269
+ await test('THE LEDGER IS THE ACCESSIBLE PATH: every goal is a row, both directions of each dependency are counted on it and named in the panel, and the five filters are there', async () => {
270
+ const p = await boot({ sky: FOUR(), versions: versions() });
271
+ p.key('l');
272
+ assert.equal(p.el('room').hidden, false);
273
+ assert.equal(p.el('roomTitle').textContent, 'the ledger');
274
+ const rows = p.rows(); const S = p.S(); const SKY = p.SKY();
275
+ for (const g of S.goals) {
276
+ assert.ok(rows.includes(`data-id="${g.id}"`), `${g.title} has a row`);
277
+ assert.ok(rows.includes(SKY.esc(g.title)), `${g.title} is named whole`);
278
+ }
279
+ const rowOf = (id) => (new RegExp(`data-id="${id}"[\\s\\S]*?</button>`).exec(rows) || [''])[0];
280
+ assert.match(rowOf('7'), /alive · 1\/3 tasks · 130 cr · 1\/2 rings · waits on 1/, 'the waiting goal (alive: a task is held) counts what it waits on');
281
+ assert.match(rowOf('9'), /waited on by 1/, 'the goal waited on counts it the other way');
282
+ assert.match(rowOf('8'), /settled/); assert.match(rowOf('6'), /collapsed/);
283
+ const filters = p.el('filters').querySelectorAll('button').map((b) => [b.dataset.f, b.textContent.replace(/\s*\d+$/, '').trim()]);
284
+ assert.deepEqual(filters, [['all', 'every goal'], ['alive', 'alive'], ['open', 'wells'], ['achieved', 'settled'], ['archived', 'collapsed'], ['routes', 'with routes']]);
285
+ fire(p.el('filters').querySelectorAll('button').find((b) => b.dataset.f === 'routes'));
286
+ assert.ok(p.rows().includes('data-id="7"') && p.rows().includes('data-id="9"') && !p.rows().includes('data-id="8"'), '"with routes" keeps the two ends of the route');
287
+ // The panel names both directions, by name, as doors.
288
+ SKY.pick({ kind: 'goal', ref: goal(p, '7') }, false);
289
+ assert.match(p.panel(), /waits on<\/span><span class="v"><a href="#" data-goal="9">A quiet well<\/a>/);
290
+ SKY.pick({ kind: 'goal', ref: goal(p, '9') }, false);
291
+ assert.match(p.panel(), /waited on by<\/span><span class="v"><a href="#" data-goal="7">The well<\/a>/);
292
+ assert.match(p.panel(), /href="\/goals#\/goal\/9"/, 'and the goal itself is a door');
224
293
  });
225
294
 
226
- await test('no dependency at all says so plainly, draws no picture, and the table is still the whole map', async () => {
227
- const h = await boot({ nodes: [node(1), node(2)], edges: [], counts: {} });
228
- assert.equal(h.svg(), null);
229
- assert.match(h.$('gm-graph').textContent, /no graph to draw — the table below is the whole map/);
230
- assert.equal(h.rows().length, 2);
231
- assert.equal(h.$('gm-alone-scroll').hidden, true, 'nothing to split when nothing is connected');
295
+ // ═══ ADR 0227 §1 stands ══════════════════════════════════════════════════════
296
+
297
+ await test('TASKS RESOLVE ONLY INSIDE ONE WELL PAST THE ZOOM THRESHOLD — never for every goal at once — and no control asks for them', async () => {
298
+ const p = await boot({ sky: FOUR(), versions: versions() });
299
+ const S = p.S(); const SKY = p.SKY();
300
+ const bodies = () => [...S.taskById.values()].filter((t) => t._sx != null);
301
+ p.frame();
302
+ assert.equal(bodies().length, 0, 'at the whole-sky fit no task is a body — dust on the rings at most');
303
+ SKY.fitGoal(goal(p, '7'));
304
+ p.frame();
305
+ const drawn = bodies();
306
+ assert.ok(drawn.length > 0, 'descended into one well, its tasks resolve onto their rings');
307
+ assert.ok(drawn.every((t) => t.goal.id === '7'), 'and only that well\'s');
308
+ const html = read('goal-map.html');
309
+ assert.equal(/<button[^>]*>[^<]*(every task|all tasks|show tasks|densif)/i.test(html), false, 'no control offers every task');
310
+ assert.equal(/<button[^>]*>[^<]*task/i.test(html), false, 'no button on the page speaks of tasks at all');
311
+ assert.deepEqual([...read('sky-interaction.js').matchAll(/data-act="(\w+)"/g)].map((m) => m[1]), ['goal', 'all', 'latest', 'hide', 'clear'], 'the shuttle menu is the five lines, none of them about tasks');
232
312
  });
233
313
 
234
- await test('an empty corpus renders the empty state and no table', async () => {
235
- const h = await boot({ nodes: [], edges: [], counts: {} });
236
- assert.match(h.$('gm-graph').textContent, /No goal carries any work yet/);
237
- assert.equal(h.$('gm-table-scroll').hidden, true);
314
+ // ═══ the clock and the bands ═════════════════════════════════════════════════
315
+
316
+ await test('the clock draws the sky as it stood on a day: a goal born after it is not there, and the figures say as of', async () => {
317
+ const p = await boot({ sky: sky([well(), well({ goal_id: '13', title: 'Born in September', created_at: '2026-09-01T00:00:00.000Z', criteria: [], tasks: tasksOf('13-', 2, 'ready', '2026-09-02') })]), versions: versions() });
318
+ const SKY = p.SKY();
319
+ assert.match(p.stats(), /<b>2<\/b> goals/);
320
+ SKY.setClock(new Date('2026-08-01T12:00:00.000Z').getTime());
321
+ assert.equal(SKY.present(goal(p, '7')), true);
322
+ assert.equal(SKY.present(goal(p, '13')), false);
323
+ assert.match(p.stats(), /<b>1<\/b> goals/);
324
+ assert.match(p.stats(), /as of 01 aug 2026/);
325
+ assert.match(p.el('when').textContent, /01 aug 2026/);
238
326
  });
239
327
 
240
- await test('a failed read is one honest line — not a blank page, not a stack', async () => {
241
- const h = await boot(null, { fail: true });
242
- assert.match(h.$('gm-sub').textContent, /Couldn.t load the goal map/);
243
- assert.equal(h.svg(), null);
244
- assert.equal(h.$('gm-table-scroll').hidden, true);
328
+ await test('a band is a version, and clicking one fills the screen with it', async () => {
329
+ const p = await boot({ sky: FOUR(), versions: versions() });
330
+ const S = p.S(); const SKY = p.SKY();
331
+ assert.equal(S.BANDS.length, 2);
332
+ assert.deepEqual(JSON.parse(JSON.stringify(S.BANDS[0].versions)), ['V2']);
333
+ const b = S.BANDS[1];
334
+ S.cam.x = 999; S.cam.k = 3;
335
+ SKY.fitBand(b);
336
+ assert.equal(S.cam.x, 0); assert.equal(S.cam.y, 0);
337
+ assert.equal(S.cam.k, SKY.fitRad(b.r1 + 80));
245
338
  });
246
339
 
247
- await test('signed out (401) goes to sign-in like every other hall read', async () => {
248
- const h = await boot(null, { status: 401 });
249
- assert.deepEqual(h.navigations, ['/api/bongos/auth/web/start']);
340
+ // ═══ the tour ════════════════════════════════════════════════════════════════
341
+
342
+ await test('the tour walks all three lenses here too, and hands the sky back to the whole project', async () => {
343
+ const p = await boot({ sky: FOUR(), versions: versions() });
344
+ p.key('?');
345
+ assert.equal(p.el('tour').hidden, false);
346
+ assert.equal(p.S().lens, 'mine', 'step one is "This is your sky"');
347
+ for (let n = 0; n < 6; n++) p.click('tourNext');
348
+ assert.equal(p.S().lens, 'all', 'step seven is the alive well, on the project map');
349
+ p.key('Escape');
350
+ assert.equal(p.el('tour').hidden, true);
351
+ assert.equal(p.S().lens, 'all', 'back to the whole project, not to your thinking');
352
+ assert.equal(p.el('lensAll').getAttribute('aria-pressed'), 'true');
353
+ const first = await boot({ sky: FOUR(), tourSeen: false });
354
+ assert.equal(first.timers.pending(), 1, 'a first visit schedules the tour here as well');
250
355
  });
251
356
 
252
- // ---- the shell ---------------------------------------------------------------------------------
357
+ // ═══ the words are never markup ══════════════════════════════════════════════
253
358
 
254
- await test('the page has its h1 in static markup (so every state has one) and loads the lib before the page script', () => {
255
- assert.match(PAGE_HTML, /<h1 class="page-head__h">Goal map<\/h1>/);
256
- assert.ok(PAGE_HTML.indexOf('goal-map-lib.js') > -1 && PAGE_HTML.indexOf('goal-map-lib.js') < PAGE_HTML.indexOf('goal-map.js'));
257
- assert.ok(PAGE_HTML.indexOf('hall-kit.js') < PAGE_HTML.indexOf('goal-map.js'));
258
- assert.ok(PAGE_HTML.indexOf('text-fit.js') > -1 && PAGE_HTML.indexOf('text-fit.js') < PAGE_HTML.indexOf('goal-map.js'));
359
+ await test('a goal title never becomes markup — in the ledger, the panel or the hover card', async () => {
360
+ const evil = '<img src=x onerror=alert(1)> & "quotes"';
361
+ const p = await boot({ sky: sky([well({ title: evil }), quiet()], [{ from: '7', to: '9', count: 1 }]), versions: versions() });
362
+ const SKY = p.SKY(); const g = goal(p, '7');
363
+ p.key('l');
364
+ SKY.pick({ kind: 'goal', ref: g }, false);
365
+ SKY.showCard({ kind: 'goal', ref: g }, 10, 10);
366
+ for (const [where, html] of [['rows', p.rows()], ['panel', p.panel()], ['card', p.card()]]) {
367
+ assert.ok(html.includes('&lt;img src=x onerror=alert(1)&gt; &amp; &quot;quotes&quot;'), `${where} escapes the title`);
368
+ assert.ok(!html.includes('<img'), `${where} carries no element from the title`);
369
+ }
370
+ SKY.pick({ kind: 'goal', ref: goal(p, '9') }, false);
371
+ assert.ok(p.panel().includes('data-goal="7">&lt;img src=x') && !p.panel().includes('<img'), 'the named dependency is escaped too');
259
372
  });
260
373
 
261
- await test('it takes NO nav slot (the sky’s rule) — the Goals page is its door', () => {
374
+ // ═══ the page and the nav ════════════════════════════════════════════════════
375
+
376
+ await test('Sky sits directly under Goals in the nav, member-gated, at the URL Goals links to', () => {
262
377
  const shell = read('shell.js');
263
- assert.equal(/goal-map/.test(shell), false, 'no nav item');
264
- assert.match(read('goals.html'), /href="\/builders\/goal-map"/);
378
+ const item = shell.indexOf("{ id: 'goal-map', label: 'Sky', icon: 'goals', href: '/goal-map', gate: 'member' }");
379
+ assert.ok(item > -1, 'the nav item, with the slot the map was denied (ADR 0227 §11)');
380
+ const goals = shell.indexOf("{ id: 'goals', label: 'Goals'"); const work = shell.indexOf("{ id: 'work', label: 'Tasks'");
381
+ assert.ok(goals > -1 && goals < item && item < work, 'under Goals, above Tasks');
382
+ assert.match(read('goals.html'), /href="\/builders\/goal-map"/, 'Goals still links to it');
383
+ });
384
+
385
+ await test('the page is the sky\'s own surface — the same sheet and the same seven scripts as /thinking, one cache-bust between them, and the old goal map is gone', () => {
386
+ const html = read('goal-map.html'); const thinking = read('thinking.html');
387
+ assert.match(html, /<body data-page="goal-map" data-page-title="The sky" data-sky-lens="all">/);
388
+ assert.match(html, /<title>The sky — \{\{worldName\}\}<\/title>/);
389
+ assert.match(html, /<h1>the sky <em id="lensWord">— the whole project<\/em><\/h1>/, 'the h1 is in static markup, so every state has one');
390
+ assert.equal(/shell\.js|style\.css/.test(html), false, 'no hall chrome, like /thinking');
391
+ const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
392
+ assert.deepEqual(scripts(html).map((s) => s[0]), ['idea-objects.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
393
+ assert.deepEqual(scripts(html), scripts(thinking), 'the two doors load the same files at the same stamp');
394
+ assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
395
+ assert.match(read('thinking.css'), /body\[data-page="thinking"\], body\[data-page="goal-map"\] \{/, 'the sheet names both bodies');
396
+ for (const f of ['goal-map.css', 'goal-map.js', 'goal-map-lib.js']) assert.equal(fs.existsSync(path.join(HALL, f)), false, `${f} is retired`);
397
+ assert.equal(fs.existsSync(path.join(ROOT, 'tests/goal_map_lib.mjs')), false, 'and so is the lib\'s test');
398
+ assert.equal(/goal-map-lib|goal-map\.js|goal-map\.css/.test(html), false);
265
399
  });
266
400
 
401
+ console.log('');
267
402
  summary();