@bongos/core 1.19.1012 → 1.19.1014
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 +113 -78
- package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
- package/docs/copy-inventory.md +120 -66
- package/docs/copy-registry.json +984 -498
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -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-costs.js +80 -0
- package/modules/hall-ui/public/fleet.html +64 -0
- package/modules/hall-ui/public/fleet.js +45 -0
- package/modules/hall-ui/public/fleet.states.json +14 -0
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea-objects.js +4 -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 +4 -0
- package/modules/hall-ui/public/sky-composer.js +155 -0
- package/modules/hall-ui/public/sky-data.js +287 -0
- package/modules/hall-ui/public/sky-draw.js +244 -0
- package/modules/hall-ui/public/sky-interaction.js +137 -0
- package/modules/hall-ui/public/sky-layout.js +81 -0
- package/modules/hall-ui/public/sky-panel.js +141 -0
- package/modules/hall-ui/public/sky-tour.js +80 -0
- 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 +203 -267
- package/modules/hall-ui/public/thinking.html +119 -58
- package/modules/hall-ui/public/thinking.states.json +33 -9
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/provisioning/routes/provisioning.js +4 -4
- package/modules/public-landing/public/projects.html +7 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/auth.js +8 -0
- package/src/bongos/routes/me.js +4 -1
- package/src/bongos/serve-internal.js +25 -6
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +3 -2
- package/tests/fleet_cost_view.mjs +189 -0
- package/tests/hall_audit.mjs +27 -0
- package/tests/hall_nav.mjs +3 -1
- package/tests/hall_page_gate_map.mjs +13 -5
- package/tests/hall_sky.mjs +393 -405
- package/tests/hall_thinking_room.mjs +274 -296
- package/tests/helpers.mjs +185 -217
- package/tests/ideator_large_dead_thought_proof.mjs +33 -25
- package/tests/ideator_traceable_proof.mjs +61 -39
- package/tests/nav_permission_atoms.mjs +1 -1
- package/modules/hall-ui/public/sky.css +0 -365
- package/modules/hall-ui/public/sky.html +0 -79
- package/modules/hall-ui/public/sky.js +0 -580
- package/modules/hall-ui/public/sky.states.json +0 -20
- package/modules/hall-ui/public/thinking.js +0 -418
package/tests/hall_sky.mjs
CHANGED
|
@@ -1,77 +1,53 @@
|
|
|
1
|
-
// tests/hall_sky.mjs — THE SKY, at the rendered layer
|
|
2
|
-
//
|
|
1
|
+
// tests/hall_sky.mjs — THE SKY, at the rendered layer (task 1004231 / sky part
|
|
2
|
+
// 3 of 5, goal 1000095; ADR 0331, ADR 0227 §11; criterion
|
|
3
|
+
// `ideator-sky-tells-the-truth`).
|
|
3
4
|
//
|
|
4
|
-
// This suite boots the REAL
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// green over a page that reads a field the
|
|
5
|
+
// This suite boots the REAL page — thinking.html's markup, the REAL
|
|
6
|
+
// idea-objects.js and the REAL seven sky-*.js files — in a vm through
|
|
7
|
+
// bootSkyPage and reads what it wrote. Every trail row goes in shaped the way
|
|
8
|
+
// GET /sky/mine emits one and every well the way GET /sky emits one, so a case
|
|
9
|
+
// cannot stay green over a page that reads a field the routes do not send.
|
|
9
10
|
//
|
|
10
|
-
// WHAT IT
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// - a remnant drawn small, which destroys the entire two-channel model — the
|
|
21
|
-
// criterion's own proof (BV2.R27) is "large and dead", and if the glyph
|
|
22
|
-
// shrinks with brightness there is nothing left to prove;
|
|
23
|
-
// - an unreadable brightness drawn as a dim star, which states that work was
|
|
24
|
-
// removed when the only thing that happened is that nobody could look;
|
|
25
|
-
// - a picture with no words, which is a page a screen reader cannot use;
|
|
26
|
-
// - a well drawn around a goal that never caught the thought, or rings
|
|
27
|
-
// claiming a goal has no criteria when the truth is nobody asked.
|
|
28
|
-
//
|
|
29
|
-
// It also holds the one structural promise of spec decision 11: the sky is
|
|
30
|
-
// never the only path to anything it shows.
|
|
11
|
+
// WHAT IT GUARDS. The page is the pixel contract of the prototype the owner
|
|
12
|
+
// adopted (docs/design/mocks/sky/), built over the two part-2 reads. What can
|
|
13
|
+
// go wrong is the wiring, not the drawing: a thought drawn in a well the ledger
|
|
14
|
+
// never put it in, a diameter computed from a curve of its own, a remnant
|
|
15
|
+
// shrunk for being dead, an unreadable brightness drawn as a dim star, a
|
|
16
|
+
// heuristic layer (the claude layer, born-of marks) shipped as if it were a
|
|
17
|
+
// ledger fact, a title that became markup, a band that names this instance's
|
|
18
|
+
// versions by hand, and — the structural promise of spec decision 11 — a drawn
|
|
19
|
+
// thought with no row in the drawer. The composer's side is
|
|
20
|
+
// tests/hall_thinking_room.mjs.
|
|
31
21
|
|
|
32
22
|
import assert from 'node:assert/strict';
|
|
33
|
-
import fs from 'node:fs/promises';
|
|
34
|
-
import path from 'node:path';
|
|
35
|
-
import { fileURLToPath } from 'node:url';
|
|
36
23
|
import { makeRunner, settle, bootSkyPage } from './helpers.mjs';
|
|
37
24
|
|
|
38
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
-
const readFile = (rel) => fs.readFile(path.join(ROOT, rel), 'utf8');
|
|
40
|
-
|
|
41
25
|
const { test, summary } = makeRunner();
|
|
26
|
+
// Values born inside the vm carry that realm's prototypes, so a strict deepEqual
|
|
27
|
+
// reports "same structure but not reference-equal"; compare them as plain data.
|
|
28
|
+
const same = (actual, expected, msg) => assert.deepEqual(JSON.parse(JSON.stringify(actual)), expected, msg);
|
|
42
29
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
const drain = async (n =
|
|
30
|
+
// Five reads in parallel, then the world is built, then the boot line runs —
|
|
31
|
+
// several awaits deep, so one microtask turn is far short.
|
|
32
|
+
const drain = async (n = 30) => { for (let i = 0; i < n; i += 1) await settle(); };
|
|
33
|
+
const boot = async (opts = {}) => { const p = bootSkyPage(opts); await drain(); return p; };
|
|
46
34
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
//
|
|
35
|
+
// ── the rows, in the routes' own shapes ─────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
// A trail row as GET /sky/mine emits one (the trail's own projection, plus the
|
|
38
|
+
// three attachers). Defaults are the dullest possible thought — filed, nothing
|
|
39
|
+
// came of it, floating — so every case states only the facts it is about.
|
|
50
40
|
function idea(over = {}) {
|
|
51
41
|
return {
|
|
52
|
-
id: 1,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
captured_at: '2026-09-01T00:00:00.000Z',
|
|
57
|
-
triage_note: null,
|
|
58
|
-
promoted_to_task_id: null,
|
|
59
|
-
promoted_task_title: null,
|
|
60
|
-
promoted_task_status: null,
|
|
61
|
-
merged_into_idea_id: null,
|
|
62
|
-
merged_into_idea_title: null,
|
|
63
|
-
lineage_tasks: [],
|
|
64
|
-
descent_tasks: [],
|
|
42
|
+
id: 1, title: 'A thought', kind: 'feature', status: 'open', captured_at: '2026-09-01T00:00:00.000Z',
|
|
43
|
+
triage_note: null, promoted_to_task_id: null, promoted_task_title: null, promoted_task_status: null,
|
|
44
|
+
merged_into_idea_id: null, merged_into_idea_title: null, grade: 'quick', field_values: null,
|
|
45
|
+
lineage_tasks: [], descent_tasks: [],
|
|
65
46
|
size: { credits: 0, payments: 0, scale: 0, historical: false },
|
|
66
|
-
state: {
|
|
67
|
-
state: 'protostar', position: 'open-space', wellGoalId: null,
|
|
68
|
-
awaitingNod: false, brightness: null, hasWork: false, attributable: true,
|
|
69
|
-
},
|
|
47
|
+
state: { state: 'protostar', position: 'open-space', wellGoalId: null, awaitingNod: false, brightness: null, hasWork: false, attributable: true },
|
|
70
48
|
...over,
|
|
71
49
|
};
|
|
72
50
|
}
|
|
73
|
-
const page = (ideas, over = {}) => ({ ideas, total: ideas.length, offset: 0, limit: 100, ...over });
|
|
74
|
-
|
|
75
51
|
const star = (over = {}) => idea({
|
|
76
52
|
id: 10, title: 'It ignited', status: 'promoted',
|
|
77
53
|
lineage_tasks: [{ task_id: 500, origin: 'promote', title: 'The task', status: 'shipped', goal_id: 7 }],
|
|
@@ -85,374 +61,386 @@ const remnant = (over = {}) => star({
|
|
|
85
61
|
state: { state: 'remnant', position: 'well', wellGoalId: '7', awaitingNod: false, brightness: 0, hasWork: true, attributable: true },
|
|
86
62
|
...over,
|
|
87
63
|
});
|
|
64
|
+
const satellite = (over = {}) => idea({
|
|
65
|
+
id: 13, title: 'Folded', status: 'merged', merged_into_idea_id: 10, merged_into_idea_title: 'It ignited',
|
|
66
|
+
state: { state: 'satellite', position: 'well', wellGoalId: '7', awaitingNod: false, brightness: 1, hasWork: true, attributable: true },
|
|
67
|
+
...over,
|
|
68
|
+
});
|
|
88
69
|
const coldBody = (over = {}) => idea({
|
|
89
70
|
id: 12, title: 'It never ignited', status: 'discarded',
|
|
90
|
-
size: { credits: 0, payments: 0, scale: 0, historical: false },
|
|
91
71
|
state: { state: 'cold-body', position: 'open-space', wellGoalId: null, awaitingNod: false, brightness: null, hasWork: false, attributable: true },
|
|
92
72
|
...over,
|
|
93
73
|
});
|
|
74
|
+
const waiting = (over = {}) => idea({
|
|
75
|
+
id: 14, title: 'Caught, waiting for a nod', status: 'promoted',
|
|
76
|
+
lineage_tasks: [{ task_id: 9001, origin: 'route', title: 'Caught, waiting for a nod', status: 'backlog', goal_id: 7 }],
|
|
77
|
+
state: { state: 'protostar', position: 'well', wellGoalId: '7', awaitingNod: true, brightness: 1, hasWork: false, attributable: true },
|
|
78
|
+
...over,
|
|
79
|
+
});
|
|
80
|
+
const FIVE = () => [star(), idea(), satellite(), remnant(), coldBody()];
|
|
81
|
+
const mine = (ideas, over = {}) => ({ builder_id: 95, ideas, total: ideas.length, cap: 1000, truncated: false, ...over });
|
|
94
82
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
function glyphOf(html, ideaId) {
|
|
99
|
-
const re = new RegExp(`data-idea="${ideaId}"[\\s\\S]*?<span class="sky-glyph"([^>]*)>`);
|
|
100
|
-
const m = re.exec(html);
|
|
101
|
-
if (!m) return null;
|
|
102
|
-
const attrs = m[1];
|
|
83
|
+
// A well as GET /sky emits one: the graph's node plus its criteria and its
|
|
84
|
+
// compact tasks.
|
|
85
|
+
function well(over = {}) {
|
|
103
86
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
87
|
+
goal_id: '7', title: 'The well — a goal that holds work', subtitle: null, status: 'open', version_id: 'V2', category: null,
|
|
88
|
+
task_total: 3, task_done: 1, created_at: '2026-06-01T00:00:00.000Z',
|
|
89
|
+
criteria: [
|
|
90
|
+
{ id: '70', criterion_id: 'c1', criterion_md: 'The first ring is met', satisfied: true, pending_review: false, task_ids: ['500'] },
|
|
91
|
+
{ id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
|
|
92
|
+
],
|
|
93
|
+
tasks: [
|
|
94
|
+
{ id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
|
|
95
|
+
{ id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
|
|
96
|
+
{ id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
|
|
97
|
+
],
|
|
98
|
+
...over,
|
|
108
99
|
};
|
|
109
100
|
}
|
|
110
|
-
const
|
|
101
|
+
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' } });
|
|
102
|
+
const versions = () => ({ versions: [
|
|
103
|
+
{ id: 'V2', status: 'building', shipped_at: null, started_at: '2026-08-01T00:00:00.000Z' },
|
|
104
|
+
{ id: 'V1', status: 'shipped', shipped_at: '2026-05-12T00:00:00.000Z', started_at: '2026-05-01T00:00:00.000Z' },
|
|
105
|
+
] });
|
|
106
|
+
|
|
107
|
+
const glyphWidth = (rows, id) => Number((new RegExp(`data-id="${id}"[\\s\\S]*?<span class="mk [^"]*" style="width:(\\d+)px`).exec(rows) || [])[1]);
|
|
108
|
+
const glyphClass = (rows, id) => (new RegExp(`data-id="${id}"[\\s\\S]*?<span class="mk ([^"]*)"`).exec(rows) || [])[1] || '';
|
|
109
|
+
const findIdea = (p, id) => p.S().ideas.find((i) => i.id === String(id));
|
|
110
|
+
|
|
111
|
+
// ═══ the reads ═══════════════════════════════════════════════════════════════
|
|
112
|
+
|
|
113
|
+
await test('it reads the whole sky in five requests, and never the old trail read', async () => {
|
|
114
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
115
|
+
const urls = p.requests.map((r) => r.url);
|
|
116
|
+
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}`);
|
|
117
|
+
assert.ok(urls.some((u) => u.startsWith('/api/bongos/versions')), 'reads the versions for the bands');
|
|
118
|
+
assert.ok(!urls.some((u) => /by-builder|\/goals\/\d+$|\/goals$/.test(u)), 'the room\'s paged trail read and the per-goal reads are gone — one read each (ADR 0331)');
|
|
119
|
+
assert.equal(urls.filter((u) => u.endsWith('/sky/mine')).length, 1, 'the trail is one request, whole — the route pages it server-side');
|
|
120
|
+
assert.ok(p.ready(), 'the page booted');
|
|
121
|
+
});
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
await test('the trail is the VIEWER\'s — /sky/mine carries no id, and nothing in the URL could name another builder', async () => {
|
|
124
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
125
|
+
assert.ok(p.requests.some((r) => r.url === '/api/bongos/sky/mine'), 'the exact path, no id');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ═══ the five objects ════════════════════════════════════════════════════════
|
|
113
129
|
|
|
114
130
|
await test('every one of the five draws its own object, and the word comes from the shared table', async () => {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
await test('
|
|
134
|
-
const p =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
assert.
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// ═══ size
|
|
141
|
-
|
|
142
|
-
await test('the diameter
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
assert.
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await test('A LARGE DEAD THOUGHT RENDERS LARGE AND DEAD — it keeps its diameter and loses its fill', async () => {
|
|
163
|
-
// The criterion's own picture, and the thing BV2.R27 goes on to prove live.
|
|
164
|
-
// The two rows differ ONLY in what happened to the work: same scale on the
|
|
165
|
-
// remnant as on the star, nothing at all on the cold body.
|
|
166
|
-
const p = bootSkyPage({ pages: [page([star(), remnant(), coldBody()])] });
|
|
167
|
-
await drain();
|
|
168
|
-
const html = p.field();
|
|
169
|
-
const alive = glyphOf(html, 10);
|
|
170
|
-
const dead = glyphOf(html, 11);
|
|
171
|
-
const never = glyphOf(html, 12);
|
|
172
|
-
assert.equal(dead.d, alive.d, 'size never falls: the remnant is exactly as big as the star');
|
|
173
|
-
assert.ok(dead.d > never.d, 'and it is visibly bigger than a thought that never mattered');
|
|
174
|
-
assert.equal(dead.object, 'remnant');
|
|
175
|
-
assert.equal(never.object, 'cold-body');
|
|
176
|
-
// Distinguishable in WORDS too, not only in shape — the picture is not the
|
|
177
|
-
// only channel a reader has.
|
|
178
|
-
assert.match(labelOf(html, 11), /burned out/);
|
|
179
|
-
assert.match(labelOf(html, 11), /none of it still alive/);
|
|
180
|
-
assert.match(labelOf(html, 12), /declined/);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
await test('a size that could not be read draws at the floor and says so, never as a measured zero', async () => {
|
|
184
|
-
const unreadable = star({ id: 30, size: null });
|
|
185
|
-
const zero = coldBody({ id: 31 });
|
|
186
|
-
const p = bootSkyPage({ pages: [page([unreadable, zero])] });
|
|
187
|
-
await drain();
|
|
188
|
-
const a = glyphOf(p.field(), 30);
|
|
189
|
-
const b = glyphOf(p.field(), 31);
|
|
190
|
-
assert.equal(a.unknownSize, true, 'the unreadable one is marked');
|
|
191
|
-
assert.equal(b.unknownSize, false, 'a real zero is not');
|
|
192
|
-
assert.match(labelOf(p.field(), 30), /size unknown/);
|
|
193
|
-
p.pick(30);
|
|
194
|
-
assert.match(p.panel(), /unknown/, 'the panel says unknown rather than a number');
|
|
195
|
-
assert.ok(!/size — the work it caused<\/span><span class="sky-fact__v">0/.test(p.panel()));
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
await test('a historical size carries its marker wherever the number is shown', async () => {
|
|
199
|
-
const p = bootSkyPage({ pages: [page([star({ size: { credits: 96, payments: 3, scale: 0.71, historical: true } })])] });
|
|
200
|
-
await drain();
|
|
201
|
-
p.pick(10);
|
|
202
|
-
assert.match(p.panel(), /part historical/);
|
|
203
|
-
assert.match(p.panel(), /not payouts that were made/);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// ═══ brightness: the three answers stay three ═══════════════════════════════
|
|
207
|
-
|
|
208
|
-
await test('brightness rides the glyph, and a dimmer star is drawn dimmer', async () => {
|
|
209
|
-
const full = star({ id: 40 });
|
|
210
|
-
const half = star({ id: 41, state: { ...star().state, brightness: 0.5 } });
|
|
211
|
-
const p = bootSkyPage({ pages: [page([full, half])] });
|
|
212
|
-
await drain();
|
|
213
|
-
assert.equal(glyphOf(p.field(), 40).lit, 1);
|
|
214
|
-
assert.equal(glyphOf(p.field(), 41).lit, 0.5);
|
|
215
|
-
assert.match(labelOf(p.field(), 41), /50% still alive/);
|
|
131
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
132
|
+
p.key('l');
|
|
133
|
+
const rows = p.rows(); const filters = p.filters(); const objects = p.objects();
|
|
134
|
+
for (const key of objects.ORDER) {
|
|
135
|
+
const cls = key === 'cold-body' ? 'cold' : key;
|
|
136
|
+
assert.ok(new RegExp(`class="mk ${cls}(?: |")`).test(rows), `a row draws the ${key} mark`);
|
|
137
|
+
assert.ok(filters.includes(objects.objectFor(key).label), `the drawer's filter says "${objects.objectFor(key).label}"`);
|
|
138
|
+
}
|
|
139
|
+
const order = objects.ORDER.map((k) => filters.indexOf(`data-f="${k}"`));
|
|
140
|
+
same(order, [...order].sort((a, b) => a - b), 'the filters sit in the shared ORDER');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await test('the state wins over the row\'s status — the page re-derives nothing', async () => {
|
|
144
|
+
const p = await boot({ mine: mine([star({ status: 'discarded' })]) });
|
|
145
|
+
p.key('l');
|
|
146
|
+
assert.equal(glyphClass(p.rows(), 10).split(' ')[0], 'star', 'a discarded row the resolver called a star is drawn as a star');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await test('a state the page has never heard of is drawn through, not swallowed', async () => {
|
|
150
|
+
const p = await boot({ mine: mine([idea({ id: 99, state: { state: 'nebula', position: 'open-space', wellGoalId: null, awaitingNod: false, brightness: null, hasWork: false, attributable: true } })]) });
|
|
151
|
+
p.key('l');
|
|
152
|
+
assert.equal(glyphClass(p.rows(), 99), 'nebula');
|
|
153
|
+
assert.equal(findIdea(p, 99).st, 'nebula');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ═══ size and brightness — the two channels ══════════════════════════════════
|
|
157
|
+
|
|
158
|
+
await test('the diameter is the server scale and nothing else — one curve, drawn by no page', async () => {
|
|
159
|
+
const p = await boot({ mine: mine([star({ size: { credits: 240, payments: 4, scale: 0.5, historical: false } }), star({ id: 20, size: { credits: 9000, payments: 40, scale: 0.5, historical: false } })]) });
|
|
160
|
+
const SKY = p.SKY();
|
|
161
|
+
assert.equal(SKY.diameter(findIdea(p, 10)), SKY.diameter(findIdea(p, 20)), 'same scale, same diameter — the credits do not get a second curve here');
|
|
162
|
+
assert.equal(SKY.diameter(findIdea(p, 10)), 6 + 22 * 0.5, 'affine in scale (the prototype\'s 6 + 22·scale)');
|
|
163
|
+
assert.equal(SKY.diameter({ size: null }), 6, 'no size draws at the floor');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await test('A LARGE DEAD THOUGHT IS LARGE AND DEAD — the remnant keeps its diameter and loses its light', async () => {
|
|
167
|
+
const p = await boot({ mine: mine([star(), remnant()]), sky: sky([well()]) });
|
|
168
|
+
p.key('l');
|
|
169
|
+
const rows = p.rows();
|
|
170
|
+
assert.equal(glyphWidth(rows, 11), glyphWidth(rows, 10), 'the dead one is drawn exactly as wide as the live one it was paid like');
|
|
171
|
+
assert.equal(glyphClass(rows, 11), 'remnant');
|
|
172
|
+
assert.equal(findIdea(p, 11).lit, 0);
|
|
173
|
+
p.SKY().pick({ kind: 'idea', ref: findIdea(p, 11) }, false);
|
|
174
|
+
assert.match(p.panel(), /none of it still alive/, 'and the words say so');
|
|
175
|
+
assert.match(p.panel(), /180/, 'while the size is spoken whole');
|
|
216
176
|
});
|
|
217
177
|
|
|
218
178
|
await test('AN UNREADABLE BRIGHTNESS LEAVES THE STAR FULLY LIT and puts the doubt in words', async () => {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const out = star({ id: 51, state: { ...star().state, brightness: 0, attributable: true } });
|
|
225
|
-
const p = bootSkyPage({ pages: [page([unknown, out])] });
|
|
226
|
-
await drain();
|
|
227
|
-
assert.equal(glyphOf(p.field(), 50).lit, 1, 'unreadable is drawn at full');
|
|
228
|
-
assert.equal(glyphOf(p.field(), 51).lit, 0, 'a confirmed-dead descent is not');
|
|
229
|
-
assert.match(labelOf(p.field(), 50), /still alive — unknown/);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await test('the panel draws no brightness BAR for a share nobody could measure', async () => {
|
|
233
|
-
// A bar at zero and a bar that could not be measured look identical, and one
|
|
234
|
-
// of them is a claim. The words carry it instead.
|
|
235
|
-
const unknown = star({ id: 60, state: { ...star().state, brightness: null, attributable: false } });
|
|
236
|
-
const p = bootSkyPage({ pages: [page([unknown])] });
|
|
237
|
-
await drain();
|
|
238
|
-
p.pick(60);
|
|
239
|
-
assert.ok(!/sky-bar/.test(p.panel()), 'no bar on an unreadable share');
|
|
179
|
+
const p = await boot({ mine: mine([star({ state: { state: 'star', position: 'well', wellGoalId: '7', awaitingNod: false, brightness: null, hasWork: true, attributable: false } })]) });
|
|
180
|
+
const i = findIdea(p, 10);
|
|
181
|
+
assert.equal(i.lit, 1, 'nobody could look, so nothing is drawn as lost');
|
|
182
|
+
assert.equal(p.SKY().brightnessWord(i), 'still alive — unknown');
|
|
183
|
+
p.SKY().pick({ kind: 'idea', ref: i }, false);
|
|
240
184
|
assert.match(p.panel(), /still alive — unknown/);
|
|
185
|
+
});
|
|
241
186
|
|
|
242
|
-
|
|
243
|
-
await
|
|
244
|
-
|
|
245
|
-
assert.
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
assert.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
await
|
|
283
|
-
const
|
|
284
|
-
assert.
|
|
285
|
-
assert.
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
await test('a
|
|
289
|
-
const p =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
id
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const html = p.field();
|
|
309
|
-
// Both in the HOST's well, not one in open space — the satellite's own
|
|
310
|
-
// wellGoalId is null, so grouping by that alone would separate them.
|
|
311
|
-
assert.ok(!/Open space/.test(html), 'the satellite did not float off on its own');
|
|
312
|
-
const pos = (id) => {
|
|
313
|
-
const m = new RegExp(`data-idea="${id}"[\\s\\S]*?left:([\\d.-]+)%;top:([\\d.-]+)%`).exec(html);
|
|
314
|
-
return { left: Number(m[1]), top: Number(m[2]) };
|
|
315
|
-
};
|
|
316
|
-
const a = pos(90);
|
|
317
|
-
const b = pos(91);
|
|
318
|
-
const gap = Math.hypot(a.left - b.left, a.top - b.top);
|
|
319
|
-
assert.ok(gap > 0 && gap < 12, `it orbits the host rather than taking a slot of its own (gap ${gap.toFixed(1)}%)`);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
await test('a protostar in a well says a nod is waiting; one in open space says nobody claimed it', async () => {
|
|
323
|
-
const waiting = idea({
|
|
324
|
-
id: 100, title: 'Caught, not lit', status: 'promoted',
|
|
325
|
-
lineage_tasks: [{ task_id: 7, origin: 'promote', title: 'T', status: 'backlog', goal_id: 7 }],
|
|
326
|
-
state: { state: 'protostar', position: 'well', wellGoalId: '7', awaitingNod: true, brightness: null, hasWork: false, attributable: true },
|
|
327
|
-
});
|
|
328
|
-
const floating = idea({ id: 101, title: 'Nobody caught it' });
|
|
329
|
-
const p = bootSkyPage({ pages: [page([waiting, floating])], goals: [{ id: '7', title: 'A goal' }] });
|
|
330
|
-
await drain();
|
|
331
|
-
assert.match(labelOf(p.field(), 100), /waiting for a nod/);
|
|
332
|
-
assert.match(labelOf(p.field(), 101), /nothing has claimed it/);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// ═══ never the only path ════════════════════════════════════════════════════
|
|
336
|
-
|
|
337
|
-
await test('every thought carries its whole sentence, so the picture is readable without seeing it', async () => {
|
|
338
|
-
const p = bootSkyPage({ pages: [page([star({ id: 110, title: 'A named thought' })])] });
|
|
339
|
-
await drain();
|
|
340
|
-
const label = labelOf(p.field(), 110);
|
|
341
|
-
for (const bit of ['A named thought', 'burning', 'size 240', 'all of it still alive']) {
|
|
342
|
-
assert.ok(label.includes(bit), `the aria-label says "${bit}" — got "${label}"`);
|
|
187
|
+
await test('a thought with no work yet says nothing about what survived', async () => {
|
|
188
|
+
const p = await boot({ mine: mine([idea()]) });
|
|
189
|
+
const i = findIdea(p, 1);
|
|
190
|
+
assert.equal(p.SKY().brightnessWord(i), null);
|
|
191
|
+
p.SKY().pick({ kind: 'idea', ref: i }, false);
|
|
192
|
+
assert.doesNotMatch(p.panel(), /still alive/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ═══ the wells ═══════════════════════════════════════════════════════════════
|
|
196
|
+
|
|
197
|
+
await test('a well is the goal that holds the work, from GET /sky, and a caught thought sits inside it', async () => {
|
|
198
|
+
const p = await boot({ mine: mine([star()]), sky: sky([well()]), versions: versions() });
|
|
199
|
+
const S = p.S(); const g = S.goalById.get('7'); const i = findIdea(p, 10);
|
|
200
|
+
assert.ok(g, 'the goal is a well');
|
|
201
|
+
assert.equal(g.title, 'The well — a goal that holds work');
|
|
202
|
+
assert.ok(Math.hypot(i.x - g.x, i.y - g.y) <= g.r, 'the thought is placed inside the well that caught it');
|
|
203
|
+
assert.ok(g.thoughts.includes(i));
|
|
204
|
+
p.key('l');
|
|
205
|
+
assert.match(p.rows(), /· in The well/, 'and the row names the well');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await test('the rings are the criteria, closing as each is satisfied and thick with the credits on them', async () => {
|
|
209
|
+
const p = await boot({ sky: sky([well()]) });
|
|
210
|
+
const g = p.S().goalById.get('7');
|
|
211
|
+
assert.equal(g.crit.length, 2);
|
|
212
|
+
same(g.crit.map((c) => c.satisfied), [true, false]);
|
|
213
|
+
same(g.crit.map((c) => c.credits), [60, 70], 'a ring\'s credits are the credits of the tasks on it');
|
|
214
|
+
assert.equal(g.tasks.find((t) => t.id === '500').ci, 0, 'a task knows which ring it gates');
|
|
215
|
+
assert.equal(g.tasks.find((t) => t.id === '500').sourceRef, '10', 'and which thought it was born of');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await test('a private goal keeps its structure and none of its prose', async () => {
|
|
219
|
+
const p = await boot({ sky: sky([well({ goal_id: '8', title: null, subtitle: null, criteria: [{ id: '80', criterion_id: 'c1', criterion_md: null, satisfied: false, pending_review: false, task_ids: [] }], tasks: [{ id: '800', title: null, status: 'ready', credits_reward: 10, created_on: '2026-08-01', idea_id: null }] })]) });
|
|
220
|
+
const g = p.S().goalById.get('8');
|
|
221
|
+
assert.equal(g.title, 'A private goal');
|
|
222
|
+
assert.equal(g.crit.length, 1, 'the ring count is structure');
|
|
223
|
+
assert.equal(g.tasks.length, 1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await test('a satellite is drawn on the thought it was folded into', async () => {
|
|
227
|
+
const p = await boot({ mine: mine([star(), satellite()]), sky: sky([well()]) });
|
|
228
|
+
const host = findIdea(p, 10); const sat = findIdea(p, 13);
|
|
229
|
+
assert.equal(sat.host, host);
|
|
230
|
+
assert.ok(Math.abs(Math.hypot(sat.x - host.x, sat.y - host.y) - 14) < 0.001, 'a fixed step off its host, not a slot of its own');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await test('a protostar in a well says a nod is waiting; one in open space says nothing claimed it', async () => {
|
|
234
|
+
const p = await boot({ mine: mine([waiting(), idea()]), sky: sky([well()]) });
|
|
235
|
+
const SKY = p.SKY();
|
|
236
|
+
assert.equal(SKY.positionWord(findIdea(p, 14)), 'waiting for a nod');
|
|
237
|
+
assert.equal(SKY.positionWord(findIdea(p, 1)), 'nothing has claimed it');
|
|
238
|
+
assert.equal(SKY.positionWord(findIdea(p, 14)), 'waiting for a nod');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ═══ the drawer — never the only path ════════════════════════════════════════
|
|
242
|
+
|
|
243
|
+
await test('EVERY DRAWN THOUGHT IS A ROW IN THE DRAWER with its whole sentence', async () => {
|
|
244
|
+
const p = await boot({ mine: mine([...FIVE(), waiting()]), sky: sky([well()]) });
|
|
245
|
+
p.key('l');
|
|
246
|
+
const rows = p.rows(); const SKY = p.SKY();
|
|
247
|
+
for (const i of p.S().ideas) {
|
|
248
|
+
assert.ok(rows.includes(`data-id="${i.id}"`), `${i.title} has a row`);
|
|
249
|
+
assert.ok(rows.includes(SKY.esc(i.title)), `${i.title} is named whole`);
|
|
250
|
+
assert.ok(rows.includes(SKY.esc(SKY.became(i))), `${i.title} says what it became`);
|
|
251
|
+
assert.ok(rows.includes(i.well ? '· in The well' : '· open space'), `${i.title} says where it sits`);
|
|
252
|
+
if (i.size && i.size.credits) assert.ok(rows.includes(`size ${i.size.credits}`), `${i.title} says its size`);
|
|
343
253
|
}
|
|
344
|
-
|
|
345
|
-
assert.match(p.field(), /<button type="button" class="sky-body"/);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
await test('the page points at the list before anything is picked, and the panel links out after', async () => {
|
|
349
|
-
const p = bootSkyPage({ pages: [page([star()])], goals: [{ id: '7', title: 'A goal' }] });
|
|
350
|
-
await drain();
|
|
351
|
-
assert.match(p.panel(), /href="\/thinking"/, 'the unpicked panel names the room');
|
|
352
|
-
p.pick(10);
|
|
353
|
-
const html = p.panel();
|
|
354
|
-
assert.match(html, /href="\/idea\/10"/, 'the record');
|
|
355
|
-
assert.match(html, /href="\/goals#\/goal\/7"/, 'the well');
|
|
356
|
-
assert.match(html, /href="\/task\/500"/, 'and the work it became');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
await test('the shell markup itself carries the way back to the list', async () => {
|
|
360
|
-
const html = await readFile('modules/hall-ui/public/sky.html');
|
|
361
|
-
assert.match(html, /href="\/builders\/thinking"/);
|
|
362
|
-
// …and the room carries the way here, so the pair is reachable both ways.
|
|
363
|
-
const room = await readFile('modules/hall-ui/public/thinking.html');
|
|
364
|
-
assert.match(room, /href="\/builders\/sky"/);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// ═══ the read ═══════════════════════════════════════════════════════════════
|
|
368
|
-
|
|
369
|
-
await test('it reads the VIEWER own trail — the id comes from /me, never from the URL', async () => {
|
|
370
|
-
const p = bootSkyPage({ pages: [page([star()])], viewer: { id: 4242 } });
|
|
371
|
-
await drain();
|
|
372
|
-
assert.ok(p.requests.some((u) => u.includes('/inbox/by-builder/4242')));
|
|
373
|
-
assert.ok(!p.requests.some((u) => /builder=/.test(u)), 'no builder query parameter anywhere');
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await test('the whole trail is read in the background, and the figures count what arrived', async () => {
|
|
377
|
-
const first = page([star({ id: 200 }), star({ id: 201 })], { total: 3, offset: 0 });
|
|
378
|
-
const second = page([remnant({ id: 202 })], { total: 3, offset: 2 });
|
|
379
|
-
const p = bootSkyPage({ pages: [first, second] });
|
|
380
|
-
await drain(40);
|
|
381
|
-
assert.equal(p.requests.filter((u) => u.includes('/inbox/by-builder/')).length, 2);
|
|
382
|
-
assert.match(p.sub(), /2 burning, 1 burned out, 3 in all/);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
await test('the wells are read AFTER the field is on screen, so the picture never waits on them', async () => {
|
|
386
|
-
const p = bootSkyPage({ pages: [page([star()])], goals: [{ id: '7', title: 'A goal' }] });
|
|
387
|
-
await drain();
|
|
388
|
-
const trailAt = p.requests.findIndex((u) => u.includes('/inbox/by-builder/'));
|
|
389
|
-
const goalsAt = p.requests.findIndex((u) => /\/goals$/.test(u));
|
|
390
|
-
assert.ok(trailAt >= 0 && goalsAt > trailAt, 'the trail is asked for first');
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
await test('an empty trail says so, and points at the one gesture that fixes it', async () => {
|
|
394
|
-
const p = bootSkyPage({ pages: [page([])] });
|
|
395
|
-
await drain();
|
|
396
|
-
assert.match(p.field(), /nothing in your sky/);
|
|
397
|
-
assert.match(p.field(), /href="\/ideas"/);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
await test('a trail that could not be read says so rather than drawing an empty sky', async () => {
|
|
401
|
-
const p = bootSkyPage({ pages: [page([star()])], trailStatus: 500 });
|
|
402
|
-
await drain();
|
|
403
|
-
assert.match(p.field(), /Could not read your idea trail/);
|
|
404
|
-
assert.ok(!/sky-body/.test(p.field()));
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// ═══ it derives nothing the server already answered ═════════════════════════
|
|
408
|
-
|
|
409
|
-
await test('when the status and the resolved state disagree, the page draws the STATE', async () => {
|
|
410
|
-
// The room's rule, and it matters more here: a state decided twice is two
|
|
411
|
-
// states, and the sky is the surface most tempted to guess because it draws
|
|
412
|
-
// rather than lists. `status` may only be read for what a thought BECAME.
|
|
413
|
-
//
|
|
414
|
-
// A GREP CANNOT ASK THIS. `headline()` legitimately counts `state === 'star'`
|
|
415
|
-
// — the server's own vocabulary, used as the server emitted it — so banning
|
|
416
|
-
// the five words from the source would fail a correct page while still
|
|
417
|
-
// passing one that re-derived the object from something else entirely. So the
|
|
418
|
-
// question is put as a disagreement the page has to resolve: a row whose
|
|
419
|
-
// `status` says it was discarded and whose resolver says it is a burning
|
|
420
|
-
// star. Anything reading the status draws a cold body.
|
|
421
|
-
const liar = star({
|
|
422
|
-
id: 300, title: 'Discarded, and still burning', status: 'discarded', triage_note: 'a duplicate',
|
|
423
|
-
});
|
|
424
|
-
const p = bootSkyPage({ pages: [page([liar])] });
|
|
425
|
-
await drain();
|
|
426
|
-
assert.equal(glyphOf(p.field(), 300).object, 'star', 'the resolver wins');
|
|
427
|
-
assert.match(labelOf(p.field(), 300), /burning/);
|
|
428
|
-
assert.ok(!/declined/.test(labelOf(p.field(), 300)));
|
|
429
|
-
// …and the other direction: a row the resolver calls a cold body draws one
|
|
430
|
-
// however alive its own columns look.
|
|
431
|
-
const other = star({
|
|
432
|
-
id: 301, title: 'Promoted, and never lit', status: 'promoted',
|
|
433
|
-
state: { state: 'cold-body', position: 'open-space', wellGoalId: null, awaitingNod: false, brightness: null, hasWork: false, attributable: true },
|
|
434
|
-
});
|
|
435
|
-
const q = bootSkyPage({ pages: [page([other])] });
|
|
436
|
-
await drain();
|
|
437
|
-
assert.equal(glyphOf(q.field(), 301).object, 'cold-body');
|
|
254
|
+
assert.equal(p.el('roomCount').textContent, '6 of 6');
|
|
438
255
|
});
|
|
439
256
|
|
|
440
|
-
await test('
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
assert.
|
|
448
|
-
|
|
257
|
+
await test('the drawer is one key away — L opens it, L closes it, and the button agrees', async () => {
|
|
258
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
259
|
+
const room = p.el('room');
|
|
260
|
+
assert.equal(room.hidden, true, 'closed at first');
|
|
261
|
+
p.key('l');
|
|
262
|
+
assert.equal(room.hidden, false);
|
|
263
|
+
assert.equal(p.el('roomBtn').getAttribute('aria-pressed'), 'true');
|
|
264
|
+
assert.match(p.rows(), /class="row"/);
|
|
265
|
+
p.key('l');
|
|
266
|
+
assert.equal(room.hidden, true);
|
|
267
|
+
assert.equal(p.el('roomBtn').getAttribute('aria-pressed'), 'false');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await test('a key pressed inside the composer is the composer\'s, not the sky\'s', async () => {
|
|
271
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
272
|
+
p.key('l', { tagName: 'INPUT', blur() {} });
|
|
273
|
+
assert.equal(p.el('room').hidden, true, 'typing an l into the line does not open the drawer');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ═══ empty and failed ════════════════════════════════════════════════════════
|
|
277
|
+
|
|
278
|
+
await test('THE PAGE RENDERS WITH AN EMPTY TRAIL — an empty sky is a state, not an error', async () => {
|
|
279
|
+
const p = await boot({ mine: mine([]), sky: sky([]), versions: versions() });
|
|
280
|
+
assert.ok(p.ready());
|
|
281
|
+
assert.match(p.stats(), /<b>0<\/b> thoughts/);
|
|
282
|
+
p.key('l');
|
|
283
|
+
assert.match(p.rows(), /Nothing of that kind in your sky\./);
|
|
284
|
+
assert.doesNotThrow(() => p.frame(), 'a frame draws with nothing in it');
|
|
285
|
+
assert.equal(p.S().BANDS.length, 2, 'the bands are still the versions\'');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await test('reads that fail still leave a sky that draws', async () => {
|
|
289
|
+
const p = await boot({ status: { sky: 500, mine: 500 } });
|
|
290
|
+
assert.ok(p.ready());
|
|
291
|
+
assert.match(p.stats(), /<b>0<\/b> thoughts/);
|
|
292
|
+
assert.doesNotThrow(() => p.frame());
|
|
293
|
+
assert.equal(p.S().BANDS[0].r0, 0, 'with no versions read there is still a present band to draw in');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ═══ the two heuristics that must not ship (part 5's facts) ══════════════════
|
|
297
|
+
|
|
298
|
+
await test('the claude layer is HIDDEN until filed_via exists, and every thought shows', async () => {
|
|
299
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
300
|
+
assert.equal(p.S().hasFiledVia, false);
|
|
301
|
+
assert.equal(p.el('claudeBtn').hidden, true, 'the pill is not offered');
|
|
302
|
+
assert.equal(p.S().ideas.filter(p.SKY().inLayer).length, 5, 'nothing is guessed to be a session\'s');
|
|
303
|
+
assert.ok(p.S().ideas.every((i) => i.origin === 'human'));
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await test('once filed_via is on a row the pill appears, that thought waits behind it, and historic rows still show', async () => {
|
|
307
|
+
const p = await boot({ mine: mine([star(), idea({ id: 2, title: 'A session filed this', filed_via: 'session' }), idea({ id: 3, title: 'A person filed this', filed_via: 'person' })]) });
|
|
308
|
+
assert.equal(p.S().hasFiledVia, true);
|
|
309
|
+
assert.equal(p.el('claudeBtn').hidden, false);
|
|
310
|
+
const SKY = p.SKY();
|
|
311
|
+
assert.equal(SKY.inLayer(findIdea(p, 2)), false, 'off by default: only what a person filed');
|
|
312
|
+
assert.equal(SKY.inLayer(findIdea(p, 10)), true, 'a historic row with no filed_via shows (the owner\'s rule)');
|
|
313
|
+
assert.equal(SKY.inLayer(findIdea(p, 3)), true);
|
|
314
|
+
p.click('claudeBtn');
|
|
315
|
+
assert.equal(SKY.inLayer(findIdea(p, 2)), true, 'the pill reveals it');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await test('NO BORN-OF MARK IS DRAWN — the read does not exist yet, so nothing is guessed', async () => {
|
|
319
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]) });
|
|
320
|
+
const S = p.S();
|
|
321
|
+
assert.equal(S.becameGoal.size, 0);
|
|
322
|
+
assert.equal(S.becameCrit.size, 0);
|
|
323
|
+
assert.ok(S.ideas.every((i) => i.shaped === null));
|
|
324
|
+
assert.ok(S.goals.every((g) => g.bornOf === null && g.crit.every((c) => c.bornOf === null)));
|
|
325
|
+
p.key('l');
|
|
326
|
+
assert.doesNotMatch(p.rows(), /born of a thought/);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ═══ the bands — derived, never named by hand ════════════════════════════════
|
|
330
|
+
|
|
331
|
+
await test('the bands come from the versions read: building at the centre, shipped outward, small ones sharing a band', async () => {
|
|
332
|
+
const p = await boot({});
|
|
333
|
+
const goalsIn = (v, n) => Array.from({ length: n }, (_, i) => ({ version: v, id: `${v}-${i}` }));
|
|
334
|
+
const bands = p.SKY().bandsFor([
|
|
335
|
+
{ id: 'NEW', status: 'building', shipped_at: null },
|
|
336
|
+
{ id: 'OLD-1', status: 'shipped', shipped_at: '2026-06-01T00:00:00.000Z' },
|
|
337
|
+
{ id: 'OLD-2', status: 'shipped', shipped_at: '2026-04-01T00:00:00.000Z' },
|
|
338
|
+
{ id: 'OLD-3', status: 'shipped', shipped_at: '2026-03-01T00:00:00.000Z' },
|
|
339
|
+
{ id: 'PLAN', status: 'planning', shipped_at: null },
|
|
340
|
+
], [...goalsIn('NEW', 4), ...goalsIn('OLD-1', 12), ...goalsIn('OLD-2', 1), ...goalsIn('OLD-3', 2)]);
|
|
341
|
+
same(bands[0].versions, ['NEW', 'PLAN'], 'the present holds whatever is not shipped');
|
|
342
|
+
assert.equal(bands[0].r0, 0);
|
|
343
|
+
same(bands[1].versions, ['OLD-1'], 'the newest shipped version is the first ring out');
|
|
344
|
+
same(bands[2].versions, ['OLD-2', 'OLD-3'], 'two small versions share a band so no ring is razor-thin');
|
|
345
|
+
assert.equal(bands[2].state, 'the beginning');
|
|
346
|
+
assert.equal(bands[1].label, 'old-1');
|
|
347
|
+
assert.ok(bands[1].r0 > bands[0].r1 && bands[2].r0 > bands[1].r1, 'rings never overlap');
|
|
348
|
+
const src = ['sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js'];
|
|
349
|
+
const fs = await import('node:fs'); const path = await import('node:path');
|
|
350
|
+
for (const f of src) assert.doesNotMatch(fs.readFileSync(path.join('modules/hall-ui/public', f), 'utf8'), /BONGOS-V\d|GDS-V\d|PMS-V\d|CB-V\d/, `${f} names no instance version`);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await test('"the whole project" leaves for the project sky at /goal-map', async () => {
|
|
354
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
355
|
+
p.click('lensAll');
|
|
356
|
+
assert.equal(p.navigated, '/goal-map');
|
|
357
|
+
assert.equal(p.S().lens, 'mine', 'this page does not switch — part 4 owns that view');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ═══ the words are never markup ══════════════════════════════════════════════
|
|
361
|
+
|
|
362
|
+
await test('a title never becomes markup — in the drawer, the panel and the hover card', async () => {
|
|
363
|
+
const evil = '<img src=x onerror=alert(1)> & "quotes"';
|
|
364
|
+
const p = await boot({ mine: mine([star({ title: evil })]), sky: sky([well({ title: '<b>bold goal</b>' })]) });
|
|
365
|
+
p.key('l');
|
|
366
|
+
const i = findIdea(p, 10);
|
|
367
|
+
p.SKY().pick({ kind: 'idea', ref: i }, false);
|
|
368
|
+
p.SKY().showCard({ kind: 'idea', ref: i }, 10, 10);
|
|
369
|
+
for (const [where, html] of [['rows', p.rows()], ['panel', p.panel()], ['card', p.card()]]) {
|
|
370
|
+
assert.ok(html.includes('<img src=x onerror=alert(1)> & "quotes"'), `${where} escapes the title`);
|
|
371
|
+
assert.ok(!html.includes('<img'), `${where} carries no element from the title`);
|
|
372
|
+
}
|
|
373
|
+
assert.ok(p.rows().includes('<b>bold goal') && !p.rows().includes('<b>bold goal'), 'a goal title is escaped too');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ═══ the tour, the lenses and the keys ═══════════════════════════════════════
|
|
377
|
+
|
|
378
|
+
await test('the tour is the template\'s thirteen steps — ? opens it, Escape closes it', async () => {
|
|
379
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
380
|
+
const STEPS = p.SKY().STEPS;
|
|
381
|
+
assert.equal(STEPS.length, 13);
|
|
382
|
+
same(STEPS.map((s) => s.h), [
|
|
383
|
+
'This is your sky.', 'A goal is a gravity well.', 'A ring is a criterion.', 'A burning thought is a star.', 'Some thoughts became the structure.',
|
|
384
|
+
'Unclaimed thoughts drift.', 'An alive well glows.', 'Routes run between goals that wait on each other.', 'A settled system is a goal that finished.',
|
|
385
|
+
'The sky has a clock.', 'By default, you explore.', 'Release a thought here.', 'When it is too much, go simple.',
|
|
386
|
+
]);
|
|
387
|
+
assert.equal(p.el('tour').hidden, true);
|
|
388
|
+
p.key('?');
|
|
389
|
+
assert.equal(p.el('tour').hidden, false);
|
|
390
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 1 of 13');
|
|
391
|
+
for (let n = 0; n < 12; n++) p.click('tourNext');
|
|
392
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 13 of 13');
|
|
393
|
+
assert.equal(p.el('tourNext').textContent, 'start reading');
|
|
394
|
+
p.key('Escape');
|
|
395
|
+
assert.equal(p.el('tour').hidden, true);
|
|
396
|
+
assert.equal(p.S().lens, 'mine', 'the tour hands the sky back to your thinking');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await test('the tour opens by itself on a first visit, and only then', async () => {
|
|
400
|
+
const first = await boot({ mine: mine(FIVE()), tourSeen: false });
|
|
401
|
+
assert.equal(first.timers.pending(), 1, 'a first visit schedules the tour');
|
|
402
|
+
await first.timers.flush();
|
|
403
|
+
assert.equal(first.el('tour').hidden, false);
|
|
404
|
+
const again = await boot({ mine: mine(FIVE()), tourSeen: true });
|
|
405
|
+
assert.equal(again.timers.pending(), 0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await test('the simple lens: no drawer, no jump, no legend, just the thoughts in their wells', async () => {
|
|
409
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]) });
|
|
410
|
+
p.key('l');
|
|
411
|
+
p.click('lensSimple');
|
|
412
|
+
assert.equal(p.S().lens, 'simple');
|
|
413
|
+
assert.equal(p.el('room').hidden, true, 'the drawer closes');
|
|
414
|
+
assert.equal(p.el('roomBtn').hidden, true);
|
|
415
|
+
assert.equal(p.el('legend').hidden, true);
|
|
416
|
+
assert.equal(p.el('recentBtn').hidden, false, 'recent / all is the one control');
|
|
417
|
+
assert.equal(p.el('lensWord').textContent, '— simply');
|
|
418
|
+
p.click('recentBtn');
|
|
419
|
+
assert.equal(p.S().recentOnly, true);
|
|
420
|
+
assert.match(p.stats(), /the last thirty days/);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await test('keys: 0 fits the sky and / puts the cursor in the composer', async () => {
|
|
424
|
+
const p = await boot({ mine: mine(FIVE()) });
|
|
425
|
+
p.S().cam.k = 5;
|
|
426
|
+
p.key('0');
|
|
427
|
+
assert.notEqual(p.S().cam.k, 5, 'the camera moved to the fit');
|
|
428
|
+
p.key('/');
|
|
429
|
+
assert.equal(p.focused(), p.el('thought'));
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await test('"my influence" fades every well your thoughts never touched', async () => {
|
|
433
|
+
const p = await boot({ mine: mine([star()]), sky: sky([well(), well({ goal_id: '9', title: 'Untouched', tasks: [{ id: '900', title: 'x', status: 'ready', credits_reward: 10, created_on: '2026-08-01', idea_id: null }] })]) });
|
|
434
|
+
p.click('influenceBtn');
|
|
435
|
+
const SKY = p.SKY(); const now = Date.now();
|
|
436
|
+
assert.equal(SKY.goalAlpha(p.S().goalById.get('7'), now), 1);
|
|
437
|
+
assert.ok(SKY.goalAlpha(p.S().goalById.get('9'), now) <= 0.12);
|
|
449
438
|
});
|
|
450
439
|
|
|
451
|
-
await test('
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
assert.ok(!/Math\.log/.test(code), 'no second compression curve');
|
|
455
|
-
assert.ok(!/descent_tasks\s*\.\s*length\s*\/\s*/.test(code), 'no brightness computed from a descent length');
|
|
440
|
+
await test('a capped trail is spoken, never a silent slice', async () => {
|
|
441
|
+
const p = await boot({ mine: mine(FIVE(), { total: 1400, truncated: true }) });
|
|
442
|
+
assert.match(p.stats(), /the newest 5 only/);
|
|
456
443
|
});
|
|
457
444
|
|
|
445
|
+
console.log('');
|
|
458
446
|
summary();
|