@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.
- package/.bongos-core.json +61 -81
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
- package/docs/copy-inventory.md +139 -110
- package/docs/copy-registry.json +872 -296
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +125 -52
- package/modules/hall-ui/public/goal-map.states.json +29 -5
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +11 -0
- package/modules/hall-ui/public/sky-data.js +19 -1
- package/modules/hall-ui/public/sky-interaction.js +14 -7
- package/modules/hall-ui/public/sky-layout.js +2 -1
- package/modules/hall-ui/public/sky-tour.js +10 -6
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +16 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/serve-internal.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/goal_map_page.mjs +346 -211
- package/tests/hall_audit.mjs +1 -0
- package/tests/hall_nav.mjs +6 -1
- package/tests/hall_page_gate_map.mjs +3 -2
- package/tests/helpers.mjs +21 -5
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/render_check.mjs +6 -4
- package/modules/hall-ui/public/goal-map-lib.js +0 -349
- package/modules/hall-ui/public/goal-map.css +0 -117
- package/modules/hall-ui/public/goal-map.js +0 -232
- package/tests/goal_map_lib.mjs +0 -214
package/tests/goal_map_page.mjs
CHANGED
|
@@ -1,267 +1,402 @@
|
|
|
1
|
-
// tests/goal_map_page.mjs —
|
|
2
|
-
//
|
|
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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
// -
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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 {
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
-
//
|
|
86
|
+
// ═══ the lens is home ════════════════════════════════════════════════════════
|
|
94
87
|
|
|
95
|
-
await test('
|
|
96
|
-
const
|
|
97
|
-
assert.
|
|
98
|
-
const
|
|
99
|
-
assert.equal(
|
|
100
|
-
assert.
|
|
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
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
assert.
|
|
107
|
-
assert.equal(
|
|
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('
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
assert.equal(
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
//
|
|
129
|
+
// ═══ the wells ═══════════════════════════════════════════════════════════════
|
|
128
130
|
|
|
129
|
-
await test('
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
assert.
|
|
134
|
-
assert.
|
|
135
|
-
assert.ok(
|
|
136
|
-
assert.ok(
|
|
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('
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
assert.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
assert.ok(
|
|
156
|
-
|
|
157
|
-
assert.ok(
|
|
158
|
-
assert.ok(
|
|
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
|
-
//
|
|
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
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
assert.equal(
|
|
172
|
-
|
|
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
|
-
//
|
|
214
|
+
// ═══ explore ═════════════════════════════════════════════════════════════════
|
|
176
215
|
|
|
177
|
-
await test('
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
assert.equal(
|
|
182
|
-
assert.equal(
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
assert.equal(
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
await test('
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
assert.equal(
|
|
211
|
-
assert.
|
|
212
|
-
|
|
213
|
-
assert.equal(
|
|
214
|
-
assert.
|
|
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
|
-
//
|
|
267
|
+
// ═══ the ledger drawer — the accessible path ═════════════════════════════════
|
|
218
268
|
|
|
219
|
-
await test('
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
assert.
|
|
223
|
-
assert.equal(
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
assert.equal(
|
|
244
|
-
assert.
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
//
|
|
357
|
+
// ═══ the words are never markup ══════════════════════════════════════════════
|
|
253
358
|
|
|
254
|
-
await test('
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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('<img src=x onerror=alert(1)> & "quotes"'), `${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"><img src=x') && !p.panel().includes('<img'), 'the named dependency is escaped too');
|
|
259
372
|
});
|
|
260
373
|
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
assert.
|
|
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();
|