@bongos/core 1.19.1014 → 1.19.1016

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 (104) hide show
  1. package/.bongos-core.json +145 -140
  2. package/.claude/skills/planning-session/SKILL.md +1 -1
  3. package/clients/bongos-client/index.d.ts +1 -1
  4. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +4 -1
  5. package/docs/adr/0331-the-sky-is-the-ideators-home.md +18 -1
  6. package/docs/adr/0335-an-invite-is-a-pre-approved-row-on-the-projects-own-instance.md +113 -0
  7. package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +201 -0
  8. package/docs/adr/README.md +2 -0
  9. package/docs/api/openapi.json +4 -0
  10. package/docs/api-reference.md +1 -1
  11. package/docs/architecture.md +4 -1
  12. package/docs/copy-inventory.md +139 -110
  13. package/docs/copy-registry.json +872 -296
  14. package/docs/file-map.md +0 -1
  15. package/docs/module-api-changelog.md +4 -0
  16. package/migrations/core_253_idea_filed_via.sql +59 -0
  17. package/modules/agents/pollers/event-router.js +2 -0
  18. package/modules/discord/discord-bugs.js +2 -0
  19. package/modules/discord/discord-inbound.js +3 -0
  20. package/modules/hall-ui/public/atlas.html +1 -1
  21. package/modules/hall-ui/public/blockers.html +1 -1
  22. package/modules/hall-ui/public/board-room.html +1 -1
  23. package/modules/hall-ui/public/collab.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/deploy.html +1 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +1 -1
  28. package/modules/hall-ui/public/fleet.html +1 -1
  29. package/modules/hall-ui/public/gate.html +1 -1
  30. package/modules/hall-ui/public/goal-map.html +125 -52
  31. package/modules/hall-ui/public/goal-map.states.json +29 -5
  32. package/modules/hall-ui/public/goals.html +1 -1
  33. package/modules/hall-ui/public/government.html +1 -1
  34. package/modules/hall-ui/public/idea.html +1 -1
  35. package/modules/hall-ui/public/ideas.html +1 -1
  36. package/modules/hall-ui/public/index.html +1 -1
  37. package/modules/hall-ui/public/modules.html +1 -1
  38. package/modules/hall-ui/public/people.html +1 -1
  39. package/modules/hall-ui/public/primer.html +1 -1
  40. package/modules/hall-ui/public/profile.html +1 -1
  41. package/modules/hall-ui/public/project-settings.html +1 -1
  42. package/modules/hall-ui/public/ranks.html +1 -1
  43. package/modules/hall-ui/public/roadmap.html +1 -1
  44. package/modules/hall-ui/public/roster.html +1 -1
  45. package/modules/hall-ui/public/sessions.html +1 -1
  46. package/modules/hall-ui/public/settings.html +1 -1
  47. package/modules/hall-ui/public/shell.js +11 -0
  48. package/modules/hall-ui/public/sky-data.js +41 -9
  49. package/modules/hall-ui/public/sky-draw.js +3 -2
  50. package/modules/hall-ui/public/sky-interaction.js +14 -7
  51. package/modules/hall-ui/public/sky-layout.js +2 -1
  52. package/modules/hall-ui/public/sky-panel.js +1 -1
  53. package/modules/hall-ui/public/sky-tour.js +10 -6
  54. package/modules/hall-ui/public/studio.html +1 -1
  55. package/modules/hall-ui/public/task.html +1 -1
  56. package/modules/hall-ui/public/thinking.css +6 -3
  57. package/modules/hall-ui/public/thinking.html +16 -10
  58. package/modules/hall-ui/public/watch.html +1 -1
  59. package/modules/hall-ui/public/work.html +1 -1
  60. package/modules/ideas/born-of.js +55 -0
  61. package/modules/ideas/inbox.js +23 -8
  62. package/modules/ideas/projection.js +48 -0
  63. package/modules/ideas/routes/inbox.js +20 -0
  64. package/modules/lifecycle/sky.js +25 -4
  65. package/modules/platform-identity/activity-snapshots.js +47 -0
  66. package/modules/platform-identity/migrations/platform_identity_022_activity_snapshots.sql +46 -0
  67. package/modules/platform-identity/platform-identity.js +13 -0
  68. package/modules/platform-identity/tests/activity-snapshots.mjs +68 -0
  69. package/modules/platform-identity/tests/platform-identity.mjs +21 -0
  70. package/modules/ui-design/skills/impeccable/SKILL.md +1 -1
  71. package/package-lock.json +2 -2
  72. package/package.json +1 -1
  73. package/scripts/gds/capture.js +11 -0
  74. package/scripts/gds/run-unit-tests.js +4 -0
  75. package/src/bongos/serve-internal.js +4 -0
  76. package/src/module-api.js +1 -1
  77. package/tests/activity_snapshots_db.mjs +97 -0
  78. package/tests/discord_bugs.mjs +1 -0
  79. package/tests/discord_inbound.mjs +1 -0
  80. package/tests/goal_map_page.mjs +360 -211
  81. package/tests/guild_namespace_reserved.mjs +76 -0
  82. package/tests/hall_audit.mjs +1 -0
  83. package/tests/hall_nav.mjs +6 -1
  84. package/tests/hall_page_gate_map.mjs +3 -2
  85. package/tests/hall_sky.mjs +33 -16
  86. package/tests/helpers.mjs +21 -5
  87. package/tests/idea_filed_via.mjs +185 -0
  88. package/tests/landing_page.mjs +8 -0
  89. package/tests/nav_permission_atoms.mjs +1 -1
  90. package/tests/profile_rollup_consent.mjs +5 -1
  91. package/tests/project_admin_console_model.mjs +105 -0
  92. package/tests/render_check.mjs +6 -4
  93. package/tests/sky_read.mjs +19 -2
  94. package/tests/ui_design_skills.mjs +9 -0
  95. package/modules/hall-ui/public/goal-map-lib.js +0 -349
  96. package/modules/hall-ui/public/goal-map.css +0 -117
  97. package/modules/hall-ui/public/goal-map.js +0 -232
  98. package/modules/public-landing/public/world/assets/clouds/cloud-island-large.png +0 -0
  99. package/modules/public-landing/public/world/assets/clouds/cloud-puff.png +0 -0
  100. package/modules/public-landing/public/world/assets/creatures/creature-whale-cargo.png +0 -0
  101. package/modules/public-landing/public/world/assets/creatures/creature-whale-glide.png +0 -0
  102. package/modules/public-landing/public/world/assets/plates/plate-clouds-near.png +0 -0
  103. package/modules/public-landing/public/world/assets/plates/scene-establishing.png +0 -0
  104. package/tests/goal_map_lib.mjs +0 -214
@@ -47,7 +47,7 @@ await test('the shape: wells carry their criteria and their compact tasks; edges
47
47
  criteria: [cr(10, 1, 100, 'shipped'), cr(10, 1, 101, 'ready'), cr(11, 2, null)],
48
48
  tasks: [t(100, 1, { status: 'shipped', idea_id: '555' }), t(101, 1), t(200, 2)],
49
49
  });
50
- assert.deepEqual(Object.keys(out), ['goals', 'edges', 'counts']);
50
+ assert.deepEqual(Object.keys(out), ['goals', 'edges', 'born', 'counts']);
51
51
  assert.deepEqual(out.edges, [{ from: '1', to: '2', count: 3 }]);
52
52
  assert.deepEqual(out.counts, { goals: 2, edges: 1, criteria: 2, tasks: 3 });
53
53
  const w1 = out.goals[0];
@@ -108,7 +108,7 @@ await test('a PRIVATE goal the viewer cannot see stays a well with its counts, r
108
108
  });
109
109
 
110
110
  await test('an empty corpus is a valid, empty sky', () => {
111
- assert.deepEqual(shapeSky({}), { goals: [], edges: [], counts: { goals: 0, edges: 0, criteria: 0, tasks: 0 } });
111
+ assert.deepEqual(shapeSky({}), { goals: [], edges: [], born: { goals: {}, criteria: {} }, counts: { goals: 0, edges: 0, criteria: 0, tasks: 0 } });
112
112
  });
113
113
 
114
114
  // ---- the criterion→task link agrees with the progress rollup ------------------------
@@ -198,6 +198,23 @@ await test('the payload is the shaped sky, categories included, from one fake re
198
198
  assert.equal(out.goals[0].tasks[0].title, 'Task 100');
199
199
  });
200
200
 
201
+ // ---- born of a thought (task 1004233, sky part 5) ---------------------------------------
202
+
203
+ await test('born rides on the sky from the ideas port, normalised to string ids; an absent or failing port is an EMPTY map — never a guess', async () => {
204
+ const pool = fakePool({ goalRows: [{ goal_id: '1', title: 'A', status: 'open', version_id: 'V1', category_id: null, visible: true, task_total: 1, task_done: 0 }], extraRows: [x(1)] });
205
+ const out = await sky({ viewerId: 1 }, { pool, categoryIndex: async () => CATS, readBornOf: async () => ({ goals: { 1: 55 }, criteria: { 10: '56', 11: null } }) });
206
+ assert.deepEqual(out.born, { goals: { 1: '55' }, criteria: { 10: '56' } });
207
+ assert.equal(pool.calls.length, 5, 'the born read is the ideas module\'s own — not a sixth query here');
208
+ const none = await sky({ viewerId: 1 }, { pool: fakePool(), categoryIndex: async () => CATS, readBornOf: null });
209
+ assert.deepEqual(none.born, { goals: {}, criteria: {} });
210
+ const broken = await sky({ viewerId: 1 }, { pool: fakePool(), categoryIndex: async () => CATS, readBornOf: async () => { throw new Error('port down'); } });
211
+ assert.deepEqual(broken.born, { goals: {}, criteria: {} }, 'a failing port empties the map and the sky still draws');
212
+ assert.deepEqual(skyLib.normalizeBorn({ goals: 'nope' }), { goals: {}, criteria: {} });
213
+ const SKY_SRC = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/sky.js'), 'utf8');
214
+ assert.match(SKY_SRC, /resolveOptional\('ideas\.bornOf'\)/, 'production resolves the port');
215
+ assert.equal(/ideas_goal_ratifications|criterion_proposal_audit/.test(SKY_SRC), false, 'lifecycle reads no ideas table itself');
216
+ });
217
+
201
218
  // ---- the cache and the handler -------------------------------------------------------
202
219
 
203
220
  function run(handler, req) {
@@ -145,6 +145,15 @@ test('impeccable is one in-house skill whose sixteen sub-commands each own exact
145
145
  }
146
146
  });
147
147
 
148
+ test('impeccable menu names the kit-report temp dir OS-generically, not Windows-only %TMP% (task 1003636)', () => {
149
+ const skill = read(path.join(IMPECCABLE, 'SKILL.md'));
150
+ const signalLine = skill.split('\n').find((l) => l.includes('The last kit report'));
151
+ assert.ok(signalLine, 'the kit-report menu signal line exists');
152
+ assert.doesNotMatch(signalLine, /`%TMP%\/ui-design-kit/, 'the signal does not lead with a bare Windows %TMP% path');
153
+ assert.match(signalLine, /os\.tmpdir\(\)/, 'points at os.tmpdir(), which resolves per-OS');
154
+ assert.match(signalLine, /\$TMPDIR/, 'names the mac/linux env var too, not just %TMP%');
155
+ });
156
+
148
157
  test('impeccable carries none of the behaviour that made its origin dangerous: no hooks, no bypass agent, no telemetry, no remote bundle, no live-server token', () => {
149
158
  const files = [path.join(IMPECCABLE, 'SKILL.md'), ...fs.readdirSync(path.join(IMPECCABLE, 'reference')).map((f) => path.join(IMPECCABLE, 'reference', f))];
150
159
  for (const f of files) {
@@ -1,349 +0,0 @@
1
- // goal-map-lib.js — the goal map's model and layout, pure (task 1003479, goal
2
- // 1000089, ADR 0227 §6 item 3 and its §10.4 amendment).
3
- //
4
- // WHAT THE MAP IS. Goals are the nodes; an edge says "some tasks in goal A wait on
5
- // tasks in goal B". ADR 0227 measured the real corpus (70 goals, 66 task edges
6
- // across 33 goal pairs) and changed the shape to build: HALF the goals have no
7
- // edge at all, and even the connected half is not one network but a large cluster
8
- // and a scatter of pairs. So this file does three things the picture needs and a
9
- // force layout would get wrong:
10
- // 1. it SPLITS the goals into connected and stand-alone (§10.4 item 1) — the
11
- // stand-alone ones are a list, never dots on a canvas that has nothing to say
12
- // about them;
13
- // 2. it finds the CLUSTERS (§10.4 item 2), largest first, so the view reads as
14
- // "one cluster and a scatter of pairs" and not as a network;
15
- // 3. it lays each cluster out in LAYERS — a goal sits one column right of the
16
- // furthest goal it waits on — which is deterministic (same data, same
17
- // picture, no random seed) and reads as a sequence rather than as a hairball.
18
- //
19
- // IT DRAWS NOTHING THE TABLE DOES NOT ALSO SAY. Every fact in the picture —
20
- // which goals, how many tasks, which category, which goal waits on which — is a
21
- // row or a cell from tableRows(), and the page ships the table by default, not
22
- // behind a toggle (ADR 0227 §5). Category is a WORD on every node as well as a
23
- // colour: the ADR 0192 set is not colour-distinguishable at this many nodes.
24
- //
25
- // NEVER VIEW A OR D. There is no function here that takes a task, and no mode
26
- // that would densify: the only input is the aggregate GET /goals/graph returns,
27
- // which carries no task identity at all (ADR 0227 §7).
28
- //
29
- // Pure — no window, no DOM, no fetch — and Node-require-safe (the blockers-lib.js
30
- // / goals-lib.js precedent) so tests/goal_map_lib.mjs runs the real layout over
31
- // the real fixture instead of agreeing with a copy of it.
32
- (function (root, factory) {
33
- var mod = factory();
34
- if (typeof module !== 'undefined' && module.exports) module.exports = mod;
35
- else root.OTBGoalMapLib = mod;
36
- }(typeof self !== 'undefined' ? self : this, function () {
37
- 'use strict';
38
-
39
- // Geometry. A node is wide enough for ~24 characters of title at the page's
40
- // label size (ADR 0227 measured a 48px pitch as the floor for a label to fit;
41
- // these are well past it), and the gaps leave room for an edge to be seen.
42
- var NODE_W = 168;
43
- // 68, not 60: the extra 8px hold the weight bar (task 1003479 grader follow-up
44
- // — "node weight = task count" was text-only; the bar makes it a real visual
45
- // size, not just a number to read).
46
- var NODE_H = 68;
47
- var GAP_X = 52;
48
- var GAP_Y = 14;
49
- var PAD = 16;
50
- var BLOCK_GAP = 28;
51
- var MIN_ROW_W = 760;
52
- // The categorical colour channel repeats after this many; the WORD on every
53
- // node is what distinguishes them, so a repeat costs nothing.
54
- var CATEGORY_COLOURS = 8;
55
-
56
- function num(id) { return Number(id); }
57
- function byNumericId(a, b) { return num(a) - num(b); }
58
-
59
- // A private goal the viewer cannot read arrives with title null. It stays a node
60
- // (dropping it would falsely isolate its neighbours) and is named as what it is.
61
- function displayTitle(node) { return node.title ? String(node.title) : 'A private goal'; }
62
-
63
- function plural(n, one, many) { return n + ' ' + (n === 1 ? one : (many || one + 's')); }
64
-
65
- // analyse — the graph's structure, with no geometry: nodes by id, who each goal
66
- // waits on and who needs it, the connected/stand-alone split, and the clusters.
67
- function analyse(payload) {
68
- var rawNodes = (payload && Array.isArray(payload.nodes)) ? payload.nodes : [];
69
- var rawEdges = (payload && Array.isArray(payload.edges)) ? payload.edges : [];
70
- var byId = {};
71
- var order = [];
72
- rawNodes.forEach(function (n) {
73
- var id = String(n.goal_id);
74
- if (byId[id]) return;
75
- byId[id] = Object.assign({}, n, { id: id });
76
- order.push(id);
77
- });
78
-
79
- var waits = {};
80
- var needed = {};
81
- var edges = [];
82
- var seen = {};
83
- rawEdges.forEach(function (e) {
84
- var from = String(e.from);
85
- var to = String(e.to);
86
- if (from === to || !byId[from] || !byId[to] || seen[from + '>' + to]) return;
87
- seen[from + '>' + to] = true;
88
- var count = Number(e.count) || 0;
89
- edges.push({ from: from, to: to, count: count });
90
- (waits[from] = waits[from] || []).push({ id: to, count: count });
91
- (needed[to] = needed[to] || []).push({ id: from, count: count });
92
- });
93
- function bySize(a, b) { return (b.count - a.count) || byNumericId(a.id, b.id); }
94
- Object.keys(waits).forEach(function (k) { waits[k].sort(bySize); });
95
- Object.keys(needed).forEach(function (k) { needed[k].sort(bySize); });
96
-
97
- // Union-find over the connected goals only.
98
- var parent = {};
99
- function find(x) { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }
100
- function union(a, b) { var ra = find(a); var rb = find(b); if (ra !== rb) parent[rb] = ra; }
101
- order.forEach(function (id) { if (waits[id] || needed[id]) parent[id] = id; });
102
- edges.forEach(function (e) { union(e.from, e.to); });
103
-
104
- var groups = {};
105
- Object.keys(parent).forEach(function (id) { (groups[find(id)] = groups[find(id)] || []).push(id); });
106
- var clusters = Object.keys(groups).map(function (k) { return groups[k].sort(byNumericId); });
107
- clusters.sort(function (a, b) { return (b.length - a.length) || byNumericId(a[0], b[0]); });
108
- var clusterOf = {};
109
- clusters.forEach(function (ids, i) {
110
- ids.forEach(function (id) { clusterOf[id] = { index: i + 1, size: ids.length }; });
111
- });
112
-
113
- var isolated = order.filter(function (id) { return !parent.hasOwnProperty(id); }).sort(byNumericId);
114
- return {
115
- order: order, byId: byId, edges: edges, waits: waits, needed: needed,
116
- clusters: clusters, clusterOf: clusterOf, isolated: isolated,
117
- connected: order.length - isolated.length,
118
- };
119
- }
120
-
121
- // A goal sits one column right of the furthest goal it waits on. Goal-level
122
- // dependencies CAN loop (task edges never do, but two goals can each hold a task
123
- // the other's waits on), so an edge back onto the current path is ignored for
124
- // layering — it is still drawn, it just does not push anything to the right.
125
- function layerOf(ids, waits) {
126
- var depth = {};
127
- var state = {};
128
- function visit(id) {
129
- if (state[id] === 2) return depth[id];
130
- if (state[id] === 1) return -1;
131
- state[id] = 1;
132
- var d = 0;
133
- (waits[id] || []).forEach(function (dep) {
134
- var dd = visit(dep.id);
135
- if (dd >= 0) d = Math.max(d, dd + 1);
136
- });
137
- state[id] = 2;
138
- depth[id] = d;
139
- return d;
140
- }
141
- ids.forEach(visit);
142
- return depth;
143
- }
144
-
145
- // Order the goals inside each column by the average row of their neighbours in
146
- // the columns already placed, a few alternating sweeps — the standard cheap way
147
- // to cut crossings, and deterministic (ties fall back to goal id).
148
- function orderColumns(columns, neighbours) {
149
- var pos = {};
150
- function reindex() { columns.forEach(function (col) { col.forEach(function (id, i) { pos[id] = i; }); }); }
151
- reindex();
152
- var layerOfId = {};
153
- columns.forEach(function (col, li) { col.forEach(function (id) { layerOfId[id] = li; }); });
154
- for (var sweep = 0; sweep < 4; sweep++) {
155
- var down = sweep % 2 === 0;
156
- var from = down ? 1 : columns.length - 2;
157
- var to = down ? columns.length : -1;
158
- for (var li = from; li !== to; li += down ? 1 : -1) {
159
- var col = columns[li];
160
- var key = {};
161
- col.forEach(function (id) {
162
- var ns = (neighbours[id] || []).filter(function (n) { return down ? layerOfId[n] < li : layerOfId[n] > li; });
163
- key[id] = ns.length ? ns.reduce(function (s, n) { return s + pos[n]; }, 0) / ns.length : pos[id];
164
- });
165
- col.sort(function (a, b) { return (key[a] - key[b]) || byNumericId(a, b); });
166
- col.forEach(function (id, i) { pos[id] = i; });
167
- }
168
- }
169
- }
170
-
171
- // layoutCluster — one cluster's goals in a block of its own: { w, h, at: { id: {x, y} } }.
172
- function layoutCluster(ids, a) {
173
- var depth = layerOf(ids, a.waits);
174
- var neighbours = {};
175
- ids.forEach(function (id) {
176
- neighbours[id] = (a.waits[id] || []).concat(a.needed[id] || []).map(function (n) { return n.id; });
177
- });
178
- var columns = [];
179
- ids.forEach(function (id) { (columns[depth[id]] = columns[depth[id]] || []).push(id); });
180
- columns = columns.map(function (c) { return (c || []).sort(byNumericId); });
181
- orderColumns(columns, neighbours);
182
-
183
- var rows = columns.reduce(function (m, c) { return Math.max(m, c.length); }, 1);
184
- var w = columns.length * NODE_W + (columns.length - 1) * GAP_X;
185
- var h = rows * NODE_H + (rows - 1) * GAP_Y;
186
- var at = {};
187
- columns.forEach(function (col, li) {
188
- var colH = col.length * NODE_H + (col.length - 1) * GAP_Y;
189
- var top = Math.round((h - colH) / 2);
190
- col.forEach(function (id, i) { at[id] = { x: li * (NODE_W + GAP_X), y: top + i * (NODE_H + GAP_Y) }; });
191
- });
192
- return { w: w, h: h, at: at };
193
- }
194
-
195
- function edgePath(dep, dependent) {
196
- // Drawn dependency → dependent, left to right: "this had to ship first".
197
- var x1 = dep.x + NODE_W;
198
- var y1 = dep.y + NODE_H / 2;
199
- var x2 = dependent.x;
200
- var y2 = dependent.y + NODE_H / 2;
201
- var dx = x2 > x1 ? (x2 - x1) / 2 : 44;
202
- return 'M' + x1 + ' ' + y1 + ' C' + (x1 + dx) + ' ' + y1 + ' ' + (x2 - dx) + ' ' + y2 + ' ' + x2 + ' ' + y2;
203
- }
204
-
205
- // layout — every cluster laid out, then packed into rows ("shelves") so the
206
- // big cluster sits on top and the pairs tile beneath it. Returns the SVG's own
207
- // size: the page puts the svg in an overflow-x container and NEVER lets the page
208
- // body scroll sideways (ADR 0227 §5.2).
209
- function layout(a) {
210
- var blocks = a.clusters.map(function (ids) { return layoutCluster(ids, a); });
211
- var rowW = Math.max(MIN_ROW_W, blocks.length ? blocks[0].w : 0);
212
- var rows = [];
213
- var cur = { items: [], w: 0, h: 0 };
214
- blocks.forEach(function (b, i) {
215
- if (cur.items.length && cur.w + BLOCK_GAP + b.w > rowW) { rows.push(cur); cur = { items: [], w: 0, h: 0 }; }
216
- cur.items.push({ i: i, x: cur.items.length ? cur.w + BLOCK_GAP : 0 });
217
- cur.w += (cur.items.length > 1 ? BLOCK_GAP : 0) + b.w;
218
- cur.h = Math.max(cur.h, b.h);
219
- });
220
- if (cur.items.length) rows.push(cur);
221
-
222
- var nodes = [];
223
- var abs = {};
224
- var y = PAD;
225
- var width = 0;
226
- rows.forEach(function (row) {
227
- row.items.forEach(function (it) {
228
- var b = blocks[it.i];
229
- Object.keys(b.at).forEach(function (id) {
230
- var p = { x: PAD + it.x + b.at[id].x, y: y + b.at[id].y };
231
- abs[id] = p;
232
- nodes.push({ id: id, x: p.x, y: p.y, w: NODE_W, h: NODE_H });
233
- });
234
- });
235
- width = Math.max(width, PAD + row.w + PAD);
236
- y += row.h + BLOCK_GAP;
237
- });
238
- var height = rows.length ? y - BLOCK_GAP + PAD : 0;
239
-
240
- var edges = a.edges.map(function (e) {
241
- return {
242
- from: e.from, to: e.to, count: e.count,
243
- d: edgePath(abs[e.to], abs[e.from]),
244
- // 1px for a single task edge, up to 3px — weight is a hint, the table has the count.
245
- width: 1 + Math.min(2, Math.floor(Math.log(Math.max(1, e.count)) / Math.LN2 / 1.5)),
246
- };
247
- });
248
- return { width: width, height: height, nodes: nodes, edges: edges, nodeW: NODE_W, nodeH: NODE_H };
249
- }
250
-
251
- // categories — the legend, and the colour each category wears. Index follows
252
- // the alphabetical order of the labels PRESENT, so a category keeps its colour
253
- // from one visit to the next while the set does not change.
254
- function categories(a) {
255
- var seen = {};
256
- a.order.forEach(function (id) {
257
- var c = a.byId[id].category;
258
- if (c && c.slug && !seen[c.slug]) seen[c.slug] = { slug: c.slug, label: c.label, count: 0 };
259
- if (c && c.slug) seen[c.slug].count++;
260
- });
261
- var list = Object.keys(seen).map(function (k) { return seen[k]; })
262
- .sort(function (x, y) { return String(x.label).localeCompare(String(y.label)) || (x.slug < y.slug ? -1 : 1); });
263
- list.forEach(function (c, i) { c.colour = i % CATEGORY_COLOURS; });
264
- var uncategorised = a.order.filter(function (id) { return !a.byId[id].category; }).length;
265
- return { list: list, uncategorised: uncategorised };
266
- }
267
-
268
- function colourOf(node, cats) {
269
- if (!node.category) return 'none';
270
- for (var i = 0; i < cats.list.length; i++) if (cats.list[i].slug === node.category.slug) return String(cats.list[i].colour);
271
- return 'none';
272
- }
273
-
274
- // weightOf — the node's WEIGHT (ADR 0227 §6 item 3: "node weight = task
275
- // count"), as a 0–1 fraction of the busiest DRAWN goal's task count. Log-scaled
276
- // so one 127-task goal does not flatten every other bar to nothing — the same
277
- // reasoning `edgePath`'s width already uses for edge count. `maxTotal` is the
278
- // caller's (the drawn set, not the whole corpus) so the scale means something
279
- // on THIS map; a maxTotal of 0 (every drawn goal has zero tasks) draws no bar
280
- // rather than dividing by zero into a false full one.
281
- function weightOf(total, maxTotal) {
282
- var t = Math.max(0, Number(total) || 0);
283
- var m = Math.max(0, Number(maxTotal) || 0);
284
- if (!m || !t) return 0;
285
- return Math.min(1, Math.log(1 + t) / Math.log(1 + m));
286
- }
287
-
288
- // tableRows — the primary surface. One row per goal, carrying every fact the
289
- // picture carries plus the two directions of each dependency BY NAME, because
290
- // with this many separate clusters the table holds more of the structure than
291
- // the picture does (§10.4 item 3).
292
- function tableRows(a) {
293
- function named(list) {
294
- return (list || []).map(function (x) { return { id: x.id, title: displayTitle(a.byId[x.id]), count: x.count }; });
295
- }
296
- var rows = a.order.map(function (id) {
297
- var n = a.byId[id];
298
- var cl = a.clusterOf[id] || null;
299
- return {
300
- id: id,
301
- title: displayTitle(n),
302
- isPrivate: !n.title,
303
- version: n.version_id || '',
304
- status: n.status || '',
305
- category: n.category ? n.category.label : '',
306
- done: n.task_done || 0,
307
- total: n.task_total || 0,
308
- waits: named(a.waits[id]),
309
- needed: named(a.needed[id]),
310
- waitsGoals: n.depends_on != null ? n.depends_on : (a.waits[id] || []).length,
311
- neededGoals: n.depended_on_by != null ? n.depended_on_by : (a.needed[id] || []).length,
312
- cluster: cl ? cl.index : 0,
313
- clusterSize: cl ? cl.size : 1,
314
- clusterLabel: cl ? 'Cluster ' + cl.index + ' · ' + plural(cl.size, 'goal') : 'Stands alone',
315
- };
316
- });
317
- // Big cluster first, stand-alone last; within a group the busiest goal first.
318
- rows.sort(function (x, y) {
319
- var cx = x.cluster || 1e9;
320
- var cy = y.cluster || 1e9;
321
- return (cx - cy) || (y.total - x.total) || byNumericId(x.id, y.id);
322
- });
323
- return rows;
324
- }
325
-
326
- // describe — the sentence that states the split out loud (ADR 0227 §10.4 item 1:
327
- // "with the split stated on the page"). Numbers only; no claim the data cannot back.
328
- function describe(a) {
329
- var goals = a.order.length;
330
- if (!goals) return { summary: 'No goal carries any work yet.', graph: '' };
331
- if (!a.connected) {
332
- return {
333
- summary: plural(goals, 'goal') + ' carry work, and no goal waits on another goal’s tasks.',
334
- graph: 'No goal depends on another goal’s work, so there is no graph to draw — the table below is the whole map.',
335
- };
336
- }
337
- var largest = a.clusters[0].length;
338
- var summary = plural(goals, 'goal') + ' carry work. ' + a.connected + ' of them ' + (a.connected === 1 ? 'is' : 'are')
339
- + ' tied to another goal by a dependency; the other ' + a.isolated.length + ' stand alone. '
340
- + 'The ' + a.connected + ' form ' + plural(a.clusters.length, 'cluster') + ', the largest with ' + plural(largest, 'goal') + '.';
341
- return { summary: summary, graph: '' };
342
- }
343
-
344
- return {
345
- analyse: analyse, layout: layout, categories: categories, colourOf: colourOf, weightOf: weightOf,
346
- tableRows: tableRows, describe: describe, displayTitle: displayTitle,
347
- plural: plural, CATEGORY_COLOURS: CATEGORY_COLOURS,
348
- };
349
- }));
@@ -1,117 +0,0 @@
1
- /* goal-map.css — the goal map (task 1003479, goal 1000089; ADR 0227 §6 item 3).
2
- *
3
- * The page is the board archetype: the shell's .scroll panels, the kit's ledger
4
- * for the table, and only the shapes no other page has — the node-link figure, its
5
- * category colour channel and legend, and the stand-alone chips.
6
- *
7
- * COLOUR. Every value below is a var() or a color-mix() of the thirteen contract
8
- * colours (DESIGN.md's Fifteen Rule), so the page inherits dark and light and an
9
- * instance's palette. Category is a colour AND a word on every node — the ADR 0192
10
- * set is not colour-distinguishable at this many nodes — so the eight hues below
11
- * are a redundant channel, and a ninth category repeating the first costs nothing.
12
- * These are page-scoped classes on the figure, never a :root (the pack's :root is
13
- * spliced at (0,1,0); a page's own would neither win reliably nor lose cleanly).
14
- *
15
- * NO SIDEWAYS SCROLL ON THE PAGE. The figure scrolls inside .gm-scroll, which names
16
- * BOTH axes — a lone overflow-x makes the other axis `auto` too and any 1px
17
- * overhang grows a vertical scrollbar nothing can scroll (task 1004130).
18
- */
19
-
20
- .gm-figure { margin: 0; }
21
- .gm-figure .scroll__lede { margin-bottom: var(--sp-md); }
22
-
23
- .gm-scroll {
24
- overflow-x: auto;
25
- overflow-y: hidden;
26
- max-width: 100%;
27
- border: var(--line) solid var(--rule-soft);
28
- border-radius: var(--radius);
29
- background: color-mix(in srgb, var(--ink) 3%, var(--bg-card));
30
- }
31
- .gm-scroll:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
32
- .gm-svg { display: block; }
33
-
34
- /* Eight categorical hues, derived. */
35
- .gm-c0 { --gm-c: var(--sea); }
36
- .gm-c1 { --gm-c: var(--accent-deep); }
37
- .gm-c2 { --gm-c: var(--terracotta); }
38
- .gm-c3 { --gm-c: color-mix(in srgb, var(--sea) 55%, var(--terracotta)); }
39
- .gm-c4 { --gm-c: color-mix(in srgb, var(--sea) 55%, var(--ink)); }
40
- .gm-c5 { --gm-c: color-mix(in srgb, var(--accent-deep) 60%, var(--ink)); }
41
- .gm-c6 { --gm-c: color-mix(in srgb, var(--accent) 55%, var(--sea)); }
42
- .gm-c7 { --gm-c: var(--ink-soft); }
43
-
44
- .gm-edge {
45
- fill: none;
46
- stroke: var(--ink-faint);
47
- opacity: 0.85;
48
- }
49
- .gm-arrowhead { fill: var(--ink-faint); }
50
-
51
- .gm-card {
52
- fill: var(--bg-card);
53
- stroke: var(--rule);
54
- stroke-width: 1;
55
- }
56
- .gm-stripe { fill: var(--gm-c, transparent); }
57
- /* Uncategorised is legal (ADR 0192): a hollow, dashed stripe — present, and plainly not a colour. */
58
- .gm-cnone .gm-stripe { fill: none; stroke: var(--ink-faint); stroke-width: 1; stroke-dasharray: 2 2; }
59
-
60
- .gm-name { font: 600 13px var(--font-body); fill: var(--ink); }
61
- .gm-cat { font: 500 11px var(--font-body); fill: var(--ink-soft); }
62
- .gm-tasks { font: 500 11px var(--font-body); fill: var(--ink-faint); }
63
-
64
- /* The node's WEIGHT (ADR 0227 §6 item 3: "node weight = task count") — a bar
65
- * scaled to the busiest goal drawn on THIS map, so its length is a real second
66
- * channel beside the task-count text, not a decoration repeating it. */
67
- .gm-weight-track { fill: var(--rule-soft); }
68
- .gm-weight-fill { fill: var(--gm-c, var(--ink-soft)); }
69
-
70
- /* The legend is a run of labelled swatches under the figure. */
71
- .gm-legend {
72
- display: flex;
73
- flex-wrap: wrap;
74
- gap: var(--sp-hair) var(--sp-md);
75
- margin: var(--sp-md) 0 0;
76
- padding: 0;
77
- list-style: none;
78
- font-size: 13px;
79
- color: var(--ink-soft);
80
- }
81
- .gm-legend li { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; }
82
- .gm-swatch {
83
- width: 10px;
84
- height: 10px;
85
- border-radius: 2px;
86
- background: var(--gm-c, transparent);
87
- }
88
- .gm-swatch.gm-cnone { background: none; box-shadow: inset 0 0 0 1px var(--ink-faint); }
89
- .gm-n { color: var(--ink-faint); }
90
-
91
- /* The stand-alone goals: chips that wrap, not a grid of cards. */
92
- .gm-alone {
93
- display: flex;
94
- flex-wrap: wrap;
95
- gap: var(--sp-hair);
96
- margin: 0;
97
- padding: 0;
98
- list-style: none;
99
- }
100
- /* A long goal title wraps inside its chip rather than pushing past the panel. */
101
- .gm-chip {
102
- min-height: 24px;
103
- max-width: 100%;
104
- flex-wrap: wrap;
105
- align-items: center;
106
- gap: 0 var(--sp-hair);
107
- white-space: normal;
108
- }
109
-
110
- /* Table cells that list goals. */
111
- /* A floor on the dependency columns: without it a phone squeezes them to a few
112
- characters and every row becomes a tall, empty tower. With it the table is
113
- wider than the card and scrolls inside it (the hall's own ledger rule). */
114
- .gm-deps { margin: 0; padding: 0; list-style: none; min-width: 14rem; }
115
- .gm-deps li { font-size: 13px; line-height: 1.45; }
116
- .gm-none { color: var(--ink-faint); }
117
- .gm-private { color: var(--ink-soft); font-style: italic; }