@bongos/core 1.19.983 ā 1.19.985
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 +18 -18
- package/docs/copy-inventory.md +5 -5
- package/docs/copy-registry.json +5 -5
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/goal-forest-lib.js +50 -5
- package/modules/hall-ui/public/goal-forest-render.js +35 -4
- package/modules/ideas/blockers.js +18 -2
- package/modules/ideas/routes/blockers.js +5 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/blocker_detail_hydration.mjs +14 -0
- package/tests/goal_forest_lib.mjs +61 -0
- package/tests/goal_forest_page.mjs +28 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.985",
|
|
6
|
+
"core_contract": "1.19.985",
|
|
7
|
+
"source_commit": "83b55732b7a45bc4ac27e3f8b4872570c2453a43",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-23T18:23:03.534Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 518,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2933,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "a3e334e9c129ec8270c4874ec1df68f400624469cfb9652714507b7049ec07f5",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2177,12 +2177,12 @@
|
|
|
2177
2177
|
{
|
|
2178
2178
|
"path": "docs/copy-inventory.md",
|
|
2179
2179
|
"mode": "0000644",
|
|
2180
|
-
"sha256": "
|
|
2180
|
+
"sha256": "4c03ef54c19fb90810ecf5fe16be7fa65c0966031fecc8a2496cd4bbcb7bed33"
|
|
2181
2181
|
},
|
|
2182
2182
|
{
|
|
2183
2183
|
"path": "docs/copy-registry.json",
|
|
2184
2184
|
"mode": "0000644",
|
|
2185
|
-
"sha256": "
|
|
2185
|
+
"sha256": "a269cd95254b4e6dace3b2c48becbb1e67d1516bb7387971f760505fee0e4464"
|
|
2186
2186
|
},
|
|
2187
2187
|
{
|
|
2188
2188
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2712,7 +2712,7 @@
|
|
|
2712
2712
|
{
|
|
2713
2713
|
"path": "docs/module-api-changelog.md",
|
|
2714
2714
|
"mode": "0000644",
|
|
2715
|
-
"sha256": "
|
|
2715
|
+
"sha256": "9a567bebe4a58d5b7efeb164ea93f3e134dc7859c12316e2319787ac3e22235c"
|
|
2716
2716
|
},
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/modules-contract.md",
|
|
@@ -4922,12 +4922,12 @@
|
|
|
4922
4922
|
{
|
|
4923
4923
|
"path": "modules/hall-ui/public/goal-forest-lib.js",
|
|
4924
4924
|
"mode": "0000644",
|
|
4925
|
-
"sha256": "
|
|
4925
|
+
"sha256": "a6e105100fcc8d1e6f15510bb44a8c8a86c5a652924629dfa846474d488637fc"
|
|
4926
4926
|
},
|
|
4927
4927
|
{
|
|
4928
4928
|
"path": "modules/hall-ui/public/goal-forest-render.js",
|
|
4929
4929
|
"mode": "0000644",
|
|
4930
|
-
"sha256": "
|
|
4930
|
+
"sha256": "69b010afbc63268d446dd1341261a2a3c611562bd28eea81dfc0505928ec5d8e"
|
|
4931
4931
|
},
|
|
4932
4932
|
{
|
|
4933
4933
|
"path": "modules/hall-ui/public/goal-inbox.js",
|
|
@@ -5517,7 +5517,7 @@
|
|
|
5517
5517
|
{
|
|
5518
5518
|
"path": "modules/ideas/blockers.js",
|
|
5519
5519
|
"mode": "0000644",
|
|
5520
|
-
"sha256": "
|
|
5520
|
+
"sha256": "e372a43f0aeff5a83075d83d2db2733b1be6e7e28dc4631901772af82914c9a6"
|
|
5521
5521
|
},
|
|
5522
5522
|
{
|
|
5523
5523
|
"path": "modules/ideas/completeness.js",
|
|
@@ -5637,7 +5637,7 @@
|
|
|
5637
5637
|
{
|
|
5638
5638
|
"path": "modules/ideas/routes/blockers.js",
|
|
5639
5639
|
"mode": "0000644",
|
|
5640
|
-
"sha256": "
|
|
5640
|
+
"sha256": "551b485b7d7ed9d6e8f28b0baea7154ffe9e9cee093c296a2097d585923340c7"
|
|
5641
5641
|
},
|
|
5642
5642
|
{
|
|
5643
5643
|
"path": "modules/ideas/routes/inbox.js",
|
|
@@ -8157,12 +8157,12 @@
|
|
|
8157
8157
|
{
|
|
8158
8158
|
"path": "package-lock.json",
|
|
8159
8159
|
"mode": "0000644",
|
|
8160
|
-
"sha256": "
|
|
8160
|
+
"sha256": "e56dc24d5e58a71b9768ab10c269e8fb9fd40691fd92e8a205e6e0200204393b"
|
|
8161
8161
|
},
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "package.json",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "e8489984d076fec6055a6c04a71ea53fe859deac7c8bd7c50acb59e45a00c748"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "public-docs/index.html",
|
|
@@ -10187,7 +10187,7 @@
|
|
|
10187
10187
|
{
|
|
10188
10188
|
"path": "src/module-api.js",
|
|
10189
10189
|
"mode": "0000644",
|
|
10190
|
-
"sha256": "
|
|
10190
|
+
"sha256": "ee0aba3198c03fa51608e06ce5d5f483335085a1aded69fef7afd71f600dfea5"
|
|
10191
10191
|
},
|
|
10192
10192
|
{
|
|
10193
10193
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10692,7 +10692,7 @@
|
|
|
10692
10692
|
{
|
|
10693
10693
|
"path": "tests/blocker_detail_hydration.mjs",
|
|
10694
10694
|
"mode": "0000644",
|
|
10695
|
-
"sha256": "
|
|
10695
|
+
"sha256": "23a71cf97346c258941210aab9e18cade21aa43e73f48395d8f656fddbaf2e91"
|
|
10696
10696
|
},
|
|
10697
10697
|
{
|
|
10698
10698
|
"path": "tests/blocker_link.mjs",
|
|
@@ -11672,12 +11672,12 @@
|
|
|
11672
11672
|
{
|
|
11673
11673
|
"path": "tests/goal_forest_lib.mjs",
|
|
11674
11674
|
"mode": "0000644",
|
|
11675
|
-
"sha256": "
|
|
11675
|
+
"sha256": "ecced35bef62920319c038ee7d21f7d0914219c7b8640a39b219990fce3357ca"
|
|
11676
11676
|
},
|
|
11677
11677
|
{
|
|
11678
11678
|
"path": "tests/goal_forest_page.mjs",
|
|
11679
11679
|
"mode": "0000644",
|
|
11680
|
-
"sha256": "
|
|
11680
|
+
"sha256": "f2fa1534f8a0dc3e4e8eee69f95407eea328584f6b25a4d8ca13e2445a42caa8"
|
|
11681
11681
|
},
|
|
11682
11682
|
{
|
|
11683
11683
|
"path": "tests/goal_graph.mjs",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -368,7 +368,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
368
368
|
| `278e8281c82f` | certain | Could not vote: {ā¦} | `modules/hall-ui/public/board-room.js:391` |
|
|
369
369
|
| `a48e9f098392` | certain | Could not {ā¦} just now. | `modules/hall-ui/public/goals-render.js:1348` |
|
|
370
370
|
| `cb09ee26014f` | certain | Could not {ā¦}: {ā¦} | `modules/hall-ui/public/government.js:314` |
|
|
371
|
-
| `f53f7aaa189f` | certain | Couldnāt load this goalās task dependencies right now. | `modules/hall-ui/public/goal-forest-render.js:
|
|
371
|
+
| `f53f7aaa189f` | certain | Couldnāt load this goalās task dependencies right now. | `modules/hall-ui/public/goal-forest-render.js:110` |
|
|
372
372
|
| `737bef533845` | certain | Couldnāt refresh the queue ā try reloading the page. | `modules/hall-ui/public/blockers.js:285` |
|
|
373
373
|
| `28ec06f6060a` | certain | Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text | `modules/hall-ui/public/ideas.html:194` |
|
|
374
374
|
| `38827a954f3b` | certain | Create a custom rank first. | `modules/hall-ui/public/government.js:459` |
|
|
@@ -650,7 +650,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
650
650
|
| `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:761` |
|
|
651
651
|
| `31e91ee6ce03` | certain | Name must be {ā¦} characters or fewer. | `modules/hall-ui/public/settings.js:766` |
|
|
652
652
|
| `5e8199266a8a` | certain | Name unvouched | `modules/hall-ui/public/watch.js:374` |
|
|
653
|
-
| `83197947ee8d` | certain | Needed by | `modules/hall-ui/public/goal-forest-render.js:
|
|
653
|
+
| `83197947ee8d` | certain | Needed by | `modules/hall-ui/public/goal-forest-render.js:145` |
|
|
654
654
|
| `3970d299c95a` | certain | Needs work | `modules/hall-ui/public/studio.js:162` |
|
|
655
655
|
| `7def635421ea` | certain | Needs your attention | `modules/hall-ui/public/goal-inbox.js:559` |
|
|
656
656
|
| `87f09d983f6c` | certain | New category label | `modules/hall-ui/public/project-settings.js:138` |
|
|
@@ -688,7 +688,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
688
688
|
| `bef9fe43ba84` | certain | No sessions recorded yet. They land here as builders ship and end sessions. | `modules/hall-ui/public/sessions.js:146` |
|
|
689
689
|
| `084748fd85b4` | certain | No shipped work is recorded between core {ā¦} and {ā¦}. | `modules/hall-ui/public/deploy.js:340` |
|
|
690
690
|
| `4485497d87bb` | certain | No specialities yet. Create one below ā it takes a name and a craft, and you can train it afterwards. | `modules/hall-ui/public/settings-specialities.js:169` |
|
|
691
|
-
| `545dc44ad427` | certain | No task in this goal depends on another task in this goal. | `modules/hall-ui/public/goal-forest-render.js:
|
|
691
|
+
| `545dc44ad427` | certain | No task in this goal depends on another task in this goal. | `modules/hall-ui/public/goal-forest-render.js:125` |
|
|
692
692
|
| `712b64d8273f` | certain | No tasks match these filters. Try widening them. | `modules/hall-ui/public/work.js:417` |
|
|
693
693
|
| `78f78d1d1e1f` | certain | No version is in flight right now. | `modules/hall-ui/public/roadmap.js:409` |
|
|
694
694
|
| `c9800f8b6bc8` | certain | No versions declared yet. | `modules/hall-ui/public/roadmap.js:427` |
|
|
@@ -975,7 +975,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
975
975
|
| `4894b1d9b0de` | certain | System oversight: people, operations and security. | `modules/hall-ui/public/watch.js:1335` |
|
|
976
976
|
| `e604fc3ace79` | certain | Task # | `modules/hall-ui/public/blockers.js:139` |
|
|
977
977
|
| `a0f384b022fd` | certain | Task dependencies | `modules/hall-ui/public/goals.html:140` |
|
|
978
|
-
| `91576eb7c880` | certain | Task dependency forest ā scrolls sideways inside this box | `modules/hall-ui/public/goal-forest-render.js:
|
|
978
|
+
| `91576eb7c880` | certain | Task dependency forest ā scrolls sideways inside this box | `modules/hall-ui/public/goal-forest-render.js:145` |
|
|
979
979
|
| `dfc967a63d57` | certain | Task lifecycle from backlog to shipped, showing states and transitions and the edge where credits are awarded. | `modules/hall-ui/public/diagrams.html:51` |
|
|
980
980
|
| `10cc12f37636` | certain | Task sections | `modules/hall-ui/public/work.html:53` |
|
|
981
981
|
| `0e636ea41b29` | certain | Task status | `modules/hall-ui/public/goals-render.js:354` |
|
|
@@ -1125,7 +1125,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1125
1125
|
| `b1e63e33f739` | certain | Vulnerability reports, engagement reports, override requests, and the audit log. | `modules/hall-ui/public/watch.html:122` |
|
|
1126
1126
|
| `411b2ff58094` | certain | Wait for task | `modules/hall-ui/public/task.js:611` |
|
|
1127
1127
|
| `f2e019bfdbe8` | certain | Waiting to be developed | `modules/hall-ui/public/ideas.html:41` |
|
|
1128
|
-
| `8e4f0179e1d3` | certain | Waits on | `modules/hall-ui/public/goal-forest-render.js:
|
|
1128
|
+
| `8e4f0179e1d3` | certain | Waits on | `modules/hall-ui/public/goal-forest-render.js:145` |
|
|
1129
1129
|
| `3a9cd9a27a84` | certain | Walk the full ladder ā | `modules/hall-ui/public/hall-render.js:84` |
|
|
1130
1130
|
| `18f2f54de2a8` | certain | Watch sections | `modules/hall-ui/public/watch.html:54` |
|
|
1131
1131
|
| `c1fcfa9b48c2` | certain | Watch ā {{worldName}} | `modules/hall-ui/public/watch.html:6` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -4481,7 +4481,7 @@
|
|
|
4481
4481
|
"surface": "builders-hall",
|
|
4482
4482
|
"text": "Couldnāt load this goalās task dependencies right now.",
|
|
4483
4483
|
"file": "modules/hall-ui/public/goal-forest-render.js",
|
|
4484
|
-
"line":
|
|
4484
|
+
"line": 110,
|
|
4485
4485
|
"origin": "js-text-assign",
|
|
4486
4486
|
"confidence": "certain"
|
|
4487
4487
|
},
|
|
@@ -7019,7 +7019,7 @@
|
|
|
7019
7019
|
"surface": "builders-hall",
|
|
7020
7020
|
"text": "Needed by",
|
|
7021
7021
|
"file": "modules/hall-ui/public/goal-forest-render.js",
|
|
7022
|
-
"line":
|
|
7022
|
+
"line": 145,
|
|
7023
7023
|
"origin": "js-markup",
|
|
7024
7024
|
"confidence": "certain"
|
|
7025
7025
|
},
|
|
@@ -7361,7 +7361,7 @@
|
|
|
7361
7361
|
"surface": "builders-hall",
|
|
7362
7362
|
"text": "No task in this goal depends on another task in this goal.",
|
|
7363
7363
|
"file": "modules/hall-ui/public/goal-forest-render.js",
|
|
7364
|
-
"line":
|
|
7364
|
+
"line": 125,
|
|
7365
7365
|
"origin": "js-text-assign",
|
|
7366
7366
|
"confidence": "certain"
|
|
7367
7367
|
},
|
|
@@ -9944,7 +9944,7 @@
|
|
|
9944
9944
|
"surface": "builders-hall",
|
|
9945
9945
|
"text": "Task dependency forest ā scrolls sideways inside this box",
|
|
9946
9946
|
"file": "modules/hall-ui/public/goal-forest-render.js",
|
|
9947
|
-
"line":
|
|
9947
|
+
"line": 145,
|
|
9948
9948
|
"origin": "js-markup",
|
|
9949
9949
|
"confidence": "certain"
|
|
9950
9950
|
},
|
|
@@ -11294,7 +11294,7 @@
|
|
|
11294
11294
|
"surface": "builders-hall",
|
|
11295
11295
|
"text": "Waits on",
|
|
11296
11296
|
"file": "modules/hall-ui/public/goal-forest-render.js",
|
|
11297
|
-
"line":
|
|
11297
|
+
"line": 145,
|
|
11298
11298
|
"origin": "js-markup",
|
|
11299
11299
|
"confidence": "certain"
|
|
11300
11300
|
},
|
|
@@ -2440,5 +2440,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2440
2440
|
landed since 1.19.981 with no explicit bump. run 35894422097. (task 1002620)
|
|
2441
2441
|
1.19.983 ā CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2442
2442
|
landed since 1.19.982 with no explicit bump. run 35896466235. (task 1002620)
|
|
2443
|
+
1.19.984 ā CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2444
|
+
landed since 1.19.983 with no explicit bump. run 35900593334. (task 1002620)
|
|
2445
|
+
1.19.985 ā CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2446
|
+
landed since 1.19.984 with no explicit bump. run 35902000024. (task 1002620)
|
|
2443
2447
|
---------------------------------------------------------------------------
|
|
2444
2448
|
```
|
|
@@ -31,17 +31,61 @@
|
|
|
31
31
|
'use strict';
|
|
32
32
|
|
|
33
33
|
var NODE_W = 168;
|
|
34
|
-
var NODE_H =
|
|
34
|
+
var NODE_H = 66;
|
|
35
35
|
var GAP_X = 52;
|
|
36
36
|
var GAP_Y = 14;
|
|
37
37
|
var PAD = 16;
|
|
38
|
-
var
|
|
38
|
+
var TEXT_INSET = 16;
|
|
39
|
+
var TITLE_LINES = 2;
|
|
39
40
|
|
|
40
41
|
function num(id) { return Number(id); }
|
|
41
42
|
function byNumericId(a, b) { return num(a) - num(b); }
|
|
42
|
-
|
|
43
|
+
|
|
44
|
+
// Titles are fitted to MEASURED width, not a character budget (task 1004200):
|
|
45
|
+
// proportional glyphs vary too much for any fixed count to fit a fixed box.
|
|
46
|
+
// `measure` is always the caller's, so this file stays DOM-free.
|
|
47
|
+
|
|
48
|
+
// The longest code-point prefix of `chars` that measures within maxWidth with
|
|
49
|
+
// `suffix` appended ('' if none does). Code points, so an emoji is never split.
|
|
50
|
+
function longestPrefix(chars, maxWidth, measure, suffix) {
|
|
51
|
+
var best = '';
|
|
52
|
+
var lo = 1;
|
|
53
|
+
var hi = chars.length;
|
|
54
|
+
while (lo <= hi) {
|
|
55
|
+
var mid = (lo + hi) >> 1;
|
|
56
|
+
var cand = chars.slice(0, mid).join('').replace(/\s+$/, '');
|
|
57
|
+
if (measure(cand + suffix) <= maxWidth) { best = cand; lo = mid + 1; } else { hi = mid - 1; }
|
|
58
|
+
}
|
|
59
|
+
return best;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// fitText ā `text` whole if it fits, else its longest prefix that fits with an ellipsis.
|
|
63
|
+
function fitText(text, maxWidth, measure) {
|
|
43
64
|
var t = String(text == null ? '' : text);
|
|
44
|
-
|
|
65
|
+
if (measure(t) <= maxWidth) return t;
|
|
66
|
+
return longestPrefix(Array.from(t), maxWidth, measure, 'ā¦') + 'ā¦';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// fitLines ā `text` wrapped at spaces into at most `maxLines` lines of maxWidth,
|
|
70
|
+
// the last ellipsised when the rest will not fit. A word wider than a whole
|
|
71
|
+
// line is broken where it runs out of room, the way the browser would.
|
|
72
|
+
function fitLines(text, maxWidth, maxLines, measure) {
|
|
73
|
+
var rest = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
|
|
74
|
+
var lines = [];
|
|
75
|
+
while (rest && lines.length < maxLines - 1 && measure(rest) > maxWidth) {
|
|
76
|
+
var words = rest.split(' ');
|
|
77
|
+
var line = '';
|
|
78
|
+
for (var i = 0; i < words.length; i++) {
|
|
79
|
+
var next = line ? line + ' ' + words[i] : words[i];
|
|
80
|
+
if (measure(next) > maxWidth) break;
|
|
81
|
+
line = next;
|
|
82
|
+
}
|
|
83
|
+
if (!line) line = longestPrefix(Array.from(words[0]), maxWidth, measure, '') || Array.from(words[0])[0];
|
|
84
|
+
lines.push(line);
|
|
85
|
+
rest = rest.slice(line.length).trim();
|
|
86
|
+
}
|
|
87
|
+
if (rest) lines.push(fitText(rest, maxWidth, measure));
|
|
88
|
+
return lines;
|
|
45
89
|
}
|
|
46
90
|
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : (many || one + 's')); }
|
|
47
91
|
|
|
@@ -224,6 +268,7 @@
|
|
|
224
268
|
|
|
225
269
|
return {
|
|
226
270
|
analyse: analyse, layout: layout, tableRows: tableRows, describe: describe,
|
|
227
|
-
|
|
271
|
+
fitText: fitText, fitLines: fitLines, plural: plural, priorityLabel: priorityLabel,
|
|
272
|
+
TEXT_INSET: TEXT_INSET, TITLE_LINES: TITLE_LINES,
|
|
228
273
|
};
|
|
229
274
|
}));
|
|
@@ -48,7 +48,21 @@
|
|
|
48
48
|
t.textContent = content;
|
|
49
49
|
return t;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Node text baselines: the title's lines, then the priority Ā· status line.
|
|
52
|
+
const TITLE_Y = 21;
|
|
53
|
+
const LINE_H = 15;
|
|
54
|
+
const SUB_Y = 52;
|
|
55
|
+
// Titles wrap to the card's measured inner width (task 1004200), which needs
|
|
56
|
+
// the svg in the document ā so drawForest collects each node's line elements
|
|
57
|
+
// into `names` and the caller fits them after appending.
|
|
58
|
+
function fitNames(names, maxWidth) {
|
|
59
|
+
names.forEach(({ els, full }) => {
|
|
60
|
+
const measure = (s) => { els[0].textContent = s; return els[0].getComputedTextLength(); };
|
|
61
|
+
const lines = lib.fitLines(full, maxWidth, els.length, measure);
|
|
62
|
+
els.forEach((el, i) => { el.textContent = lines[i] || ''; });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function drawForest(a, lay, names) {
|
|
52
66
|
const svg = svgEl('svg', {
|
|
53
67
|
class: 'gf-svg', viewBox: `0 0 ${lay.width} ${lay.height}`, width: lay.width, height: lay.height,
|
|
54
68
|
role: 'img', 'aria-labelledby': 'goal-deps-h', focusable: 'false',
|
|
@@ -56,6 +70,10 @@
|
|
|
56
70
|
const defs = svgEl('defs', {}, svg);
|
|
57
71
|
const marker = svgEl('marker', { id: 'gf-arrow', viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 7, markerHeight: 7, orient: 'auto' }, defs);
|
|
58
72
|
svgEl('path', { class: 'gf-arrowhead', d: 'M0 0 L10 5 L0 10 z' }, marker);
|
|
73
|
+
// The hard guarantee for a title that could not be measured (drawn while
|
|
74
|
+
// hidden): it is cut at the card's edge instead of running past it.
|
|
75
|
+
const clip = svgEl('clipPath', { id: 'gf-name-clip' }, defs);
|
|
76
|
+
svgEl('rect', { width: lay.nodeW, height: lay.nodeH }, clip);
|
|
59
77
|
|
|
60
78
|
const edges = svgEl('g', { class: 'gf-edges' }, svg);
|
|
61
79
|
lay.edges.forEach((e) => {
|
|
@@ -71,8 +89,15 @@
|
|
|
71
89
|
const g = svgEl('g', { class: 'gf-node', transform: `translate(${p.x} ${p.y})` }, nodes);
|
|
72
90
|
svgEl('title', {}, g).textContent = `#${p.id} ${n.title || ''} ā ${n.status || ''} ā waits on ${n.depends_on || 0}, needed by ${n.depended_on_by || 0}`;
|
|
73
91
|
svgEl('rect', { class: 'gf-card', width: p.w, height: p.h, rx: 8 }, g);
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
const full = `#${p.id} ${n.title || ''}`;
|
|
93
|
+
const els = [];
|
|
94
|
+
for (let i = 0; i < lib.TITLE_LINES; i++) {
|
|
95
|
+
const line = text(g, 'gf-name', lib.TEXT_INSET, TITLE_Y + i * LINE_H, i ? '' : full);
|
|
96
|
+
line.setAttribute('clip-path', 'url(#gf-name-clip)');
|
|
97
|
+
els.push(line);
|
|
98
|
+
}
|
|
99
|
+
names.push({ els, full });
|
|
100
|
+
text(g, 'gf-sub', lib.TEXT_INSET, SUB_Y, `${lib.priorityLabel(n.priority)} Ā· ${n.status || ''}`);
|
|
76
101
|
});
|
|
77
102
|
return svg;
|
|
78
103
|
}
|
|
@@ -125,7 +150,13 @@
|
|
|
125
150
|
<thead><tr><th>Task</th><th>Status</th><th>Priority</th><th>Waits on</th><th>Needed by</th><th>Chain</th></tr></thead>
|
|
126
151
|
<tbody>${rows.map(rowHtml).join('')}</tbody>
|
|
127
152
|
</table>`;
|
|
128
|
-
|
|
153
|
+
const lay = lib.layout(a);
|
|
154
|
+
const names = [];
|
|
155
|
+
body.querySelector('.gf-scroll').appendChild(drawForest(a, lay, names));
|
|
156
|
+
const fit = () => fitNames(names, lay.nodeW - 2 * lib.TEXT_INSET);
|
|
157
|
+
fit();
|
|
158
|
+
// A web font that lands after the first fit changes every width.
|
|
159
|
+
if (document.fonts && document.fonts.status !== 'loaded') document.fonts.ready.then(fit);
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
async function loadDependencyForest(goalId) {
|
|
@@ -18,18 +18,33 @@ const BLOCKING_TASK_IDS_SQL = `COALESCE(
|
|
|
18
18
|
ARRAY[]::bigint[]
|
|
19
19
|
) AS blocking_task_ids`;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Optional { limit, offset } pushes the page into SQL so a small slice (the Home
|
|
22
|
+
// attention inbox reads limit=5 on every load) never scans the whole open set
|
|
23
|
+
// (task 1003986). No options = the full list, as before.
|
|
24
|
+
async function listOpenBlockers({ limit, offset } = {}) {
|
|
25
|
+
const params = [];
|
|
26
|
+
let page = '';
|
|
27
|
+
if (limit != null) {
|
|
28
|
+
params.push(limit, offset || 0);
|
|
29
|
+
page = ' LIMIT $1 OFFSET $2';
|
|
30
|
+
}
|
|
22
31
|
const { rows } = await pool.query(
|
|
23
32
|
`SELECT b.id, b.title, b.body_md, b.status, b.created_at,
|
|
24
33
|
b.source, b.source_ref,
|
|
25
34
|
${BLOCKING_TASK_IDS_SQL}
|
|
26
35
|
FROM blockers b
|
|
27
36
|
WHERE b.status = 'open'
|
|
28
|
-
ORDER BY b.id
|
|
37
|
+
ORDER BY b.id${page}`,
|
|
38
|
+
params
|
|
29
39
|
);
|
|
30
40
|
return rows;
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
async function countOpenBlockers() {
|
|
44
|
+
const { rows } = await pool.query(`SELECT count(*)::int AS n FROM blockers WHERE status = 'open'`);
|
|
45
|
+
return rows[0].n;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
async function getBlocker(id) {
|
|
34
49
|
const { rows } = await pool.query(
|
|
35
50
|
`SELECT b.id, b.title, b.body_md, b.status, b.created_at, b.resolved_at, b.resolved_by,
|
|
@@ -134,6 +149,7 @@ async function linkBlockerToTask({ blockerId, taskId }) {
|
|
|
134
149
|
|
|
135
150
|
module.exports = {
|
|
136
151
|
listOpenBlockers,
|
|
152
|
+
countOpenBlockers,
|
|
137
153
|
getBlocker,
|
|
138
154
|
createBlocker,
|
|
139
155
|
resolveBlocker,
|
|
@@ -41,9 +41,12 @@ module.exports = function buildBlockersRouter() {
|
|
|
41
41
|
// project ā cap it via the shared helper (was unbounded). `page` is additive
|
|
42
42
|
// (readers of .blockers are unchanged); total lets a caller know to page.
|
|
43
43
|
const { limit, offset } = api.parsePagination(req);
|
|
44
|
-
const
|
|
44
|
+
const [rows, total] = await Promise.all([
|
|
45
|
+
blockers.listOpenBlockers({ limit, offset }),
|
|
46
|
+
blockers.countOpenBlockers(),
|
|
47
|
+
]);
|
|
45
48
|
res.set('Cache-Control', 'public, max-age=60');
|
|
46
|
-
res.json({ blockers:
|
|
49
|
+
res.json({ blockers: rows, page: api.pageMeta(limit, offset, total) });
|
|
47
50
|
} catch (err) {
|
|
48
51
|
console.error('[gds] GET /blockers', err);
|
|
49
52
|
res.fail('list_failed', { status: 500, message: 'internal error' });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.985",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.985",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.985",
|
|
4
4
|
"description": "Cloud Bongos ā the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.985'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -124,5 +124,19 @@ await test('getBlocker still returns null for an id with no row', async () => {
|
|
|
124
124
|
assert.equal(row, null);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
await test('listOpenBlockers({limit, offset}) pushes LIMIT/OFFSET into SQL (task 1003986)', async () => {
|
|
128
|
+
await blockers.listOpenBlockers({ limit: 5, offset: 10 });
|
|
129
|
+
const q = queries[queries.length - 1];
|
|
130
|
+
assert.match(q.sql, /ORDER BY b\.id LIMIT \$1 OFFSET \$2/);
|
|
131
|
+
assert.deepEqual(q.params, [5, 10]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await test('listOpenBlockers() with no options stays unbounded', async () => {
|
|
135
|
+
await blockers.listOpenBlockers();
|
|
136
|
+
const q = queries[queries.length - 1];
|
|
137
|
+
assert.ok(!/LIMIT/.test(q.sql));
|
|
138
|
+
assert.deepEqual(q.params, []);
|
|
139
|
+
});
|
|
140
|
+
|
|
127
141
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
128
142
|
process.exit(failed ? 1 : 0);
|
|
@@ -141,6 +141,67 @@ await test('describe states the numbers plainly, and an empty forest says so', (
|
|
|
141
141
|
assert.equal(L.describe(L.analyse({})), 'No task in this goal depends on another task in this goal.');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
// ---- fitting a title to its card (task 1004200) ------------------------------------
|
|
145
|
+
|
|
146
|
+
// A proportional fake: wide capitals cost three times a narrow letter, so any
|
|
147
|
+
// character budget that fits one title overflows the other.
|
|
148
|
+
const WIDTHS = { W: 12, M: 11, i: 3, 'ā¦': 8, 'š': 14 };
|
|
149
|
+
const measure = (s) => Array.from(s).reduce((w, c) => w + (WIDTHS[c] ?? 6), 0);
|
|
150
|
+
|
|
151
|
+
await test('fitText leaves a title that fits untouched', () => {
|
|
152
|
+
assert.equal(L.fitText('#7 Fix', 136, measure), '#7 Fix');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await test('fitText cuts to the MEASURED width, so wide glyphs keep fewer characters than narrow ones', () => {
|
|
156
|
+
const wide = L.fitText('W'.repeat(40), 136, measure);
|
|
157
|
+
const narrow = L.fitText('i'.repeat(80), 136, measure);
|
|
158
|
+
for (const s of [wide, narrow]) {
|
|
159
|
+
assert.ok(measure(s) <= 136, `${s} measures ${measure(s)}, past the box`);
|
|
160
|
+
assert.ok(s.endsWith('ā¦'), 'a cut title says it was cut');
|
|
161
|
+
}
|
|
162
|
+
assert.ok(Array.from(narrow).length > 3 * Array.from(wide).length, 'no fixed character count could produce both');
|
|
163
|
+
assert.equal(wide, 'W'.repeat(10) + 'ā¦', 'the LONGEST prefix that fits: 10 W + ⦠= 128px, 11 would be 140px');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await test('fitText never ends on a dangling space and never splits an emoji', () => {
|
|
167
|
+
const spaced = L.fitText('ab cd ef gh ij kl mn op qr', 62, measure);
|
|
168
|
+
assert.doesNotMatch(spaced, /\sā¦$/);
|
|
169
|
+
const emoji = L.fitText('š'.repeat(30), 60, measure);
|
|
170
|
+
assert.equal(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(emoji), false, 'no lone high surrogate');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await test('fitText returns a bare ellipsis when not even one character fits', () => {
|
|
174
|
+
assert.equal(L.fitText('WWW', 10, measure), 'ā¦');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await test('fitLines keeps a short title on one line, whole', () => {
|
|
178
|
+
assert.deepEqual(L.fitLines('#7 Fix', 136, 2, measure), ['#7 Fix']);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await test('fitLines wraps at a space, and a second line that fits is left whole', () => {
|
|
182
|
+
assert.deepEqual(L.fitLines('aaaa bbbb cccc dddd', 60, 2, measure), ['aaaa bbbb', 'cccc dddd']);
|
|
183
|
+
assert.deepEqual(L.fitLines('a b\n c', 60, 2, measure), ['a b c'], 'runs of whitespace read as one space');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await test('fitLines ellipsises the LAST line when the title outruns every line, and no line overflows', () => {
|
|
187
|
+
const lines = L.fitLines('BV1.R26 ā Real governance: the constitution becomes enforceable at last', 136, 2, measure);
|
|
188
|
+
assert.equal(lines.length, 2);
|
|
189
|
+
for (const l of lines) assert.ok(measure(l) <= 136, `"${l}" measures ${measure(l)}`);
|
|
190
|
+
assert.ok(lines[1].endsWith('ā¦'));
|
|
191
|
+
assert.ok(!lines[0].endsWith('ā¦'), 'only the last line is cut');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await test('fitLines breaks a word wider than a whole line instead of overflowing, and always finishes', () => {
|
|
195
|
+
const lines = L.fitLines('W'.repeat(40), 136, 2, measure);
|
|
196
|
+
assert.deepEqual(lines, ['W'.repeat(11), 'W'.repeat(10) + 'ā¦']);
|
|
197
|
+
assert.ok(L.fitLines('WWW', 5, 2, measure).length <= 2, 'a box narrower than one glyph still terminates');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await test('fitLines with one line is fitText', () => {
|
|
201
|
+
const t = 'Move the remaining hall pages onto the kit';
|
|
202
|
+
assert.deepEqual(L.fitLines(t, 90, 1, measure), [L.fitText(t, 90, measure)]);
|
|
203
|
+
});
|
|
204
|
+
|
|
144
205
|
// ---- purity -----------------------------------------------------------------------
|
|
145
206
|
|
|
146
207
|
await test('the lib is pure: no DOM, no fetch, no cross-goal input', () => {
|
|
@@ -18,6 +18,7 @@ import fs from 'node:fs';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import vm from 'node:vm';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
21
22
|
import { makeRunner, makeEl, walk } from './helpers.mjs';
|
|
22
23
|
|
|
23
24
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -45,6 +46,8 @@ async function boot({ status = 200, fail = false, payload = { nodes: [], edges:
|
|
|
45
46
|
const el = makeEl(tag);
|
|
46
47
|
const orig = el.setAttribute.bind(el);
|
|
47
48
|
el.setAttribute = (k, v) => { orig(k, v); if (k === 'class') el.className = String(v); if (k === 'id') el.id = String(v); };
|
|
49
|
+
// A proportional fake of SVGTextElement's measurement: a W is twice a letter.
|
|
50
|
+
if (tag === 'text') el.getComputedTextLength = () => Array.from(el.textContent).reduce((w, c) => w + (c === 'W' ? 12 : 6), 0);
|
|
48
51
|
return el;
|
|
49
52
|
},
|
|
50
53
|
};
|
|
@@ -149,6 +152,31 @@ await test('a populated forest draws a role=img figure with no focus stops, and
|
|
|
149
152
|
assert.equal(h.$('goal-deps-count').textContent, '2');
|
|
150
153
|
});
|
|
151
154
|
|
|
155
|
+
await test('a node title wraps to two lines by MEASURED width, clipped as a backstop, and kept whole in the tooltip (task 1004200)', async () => {
|
|
156
|
+
const wide = 'W'.repeat(60);
|
|
157
|
+
const words = 'Move the remaining hall pages onto the kit and retire the aliases';
|
|
158
|
+
const h = await boot({ payload: { nodes: [t(1, { title: wide }), t(2, { title: 'Fix' }), t(3, { title: words })], edges: [e(2, 1), e(3, 1)] } });
|
|
159
|
+
await h.renderers.loadDependencyForest(7);
|
|
160
|
+
const svg = h.$('goal-deps-body').querySelector('svg');
|
|
161
|
+
const lib = createRequire(import.meta.url)(path.join(HALL, 'goal-forest-lib.js'));
|
|
162
|
+
const inner = lib.layout(lib.analyse({ nodes: [t(1), t(2)], edges: [e(2, 1)] })).nodeW - 2 * lib.TEXT_INSET;
|
|
163
|
+
const lines = (id) => svg.querySelectorAll('.gf-node').find((g) => g.attrs.transform && g.querySelector('title').textContent.startsWith(`#${id} `))
|
|
164
|
+
.querySelectorAll('.gf-name').map((l) => l.textContent);
|
|
165
|
+
for (const n of svg.querySelectorAll('.gf-name')) {
|
|
166
|
+
assert.ok(n.getComputedTextLength() <= inner, `"${n.textContent}" measures ${n.getComputedTextLength()}px, past the card's ${inner}px`);
|
|
167
|
+
assert.equal(n.getAttribute('clip-path'), 'url(#gf-name-clip)');
|
|
168
|
+
}
|
|
169
|
+
assert.equal(lines(1).length, lib.TITLE_LINES);
|
|
170
|
+
assert.deepEqual(lines(2), ['#2 Fix', ''], 'a title that fits is left whole, on one line');
|
|
171
|
+
const [first, second] = lines(3);
|
|
172
|
+
assert.ok(words.startsWith(first.replace(/^#3 /, '')) && !first.endsWith('ā¦'), 'line one breaks at a space, uncut');
|
|
173
|
+
assert.ok(second.endsWith('ā¦') && words.includes(second.slice(0, -1)), 'line two continues the title and says it was cut');
|
|
174
|
+
assert.ok(lines(1)[1].endsWith('ā¦'), 'an unbreakable run is cut on the last line, never run past the card');
|
|
175
|
+
assert.ok(svg.querySelector('#gf-name-clip'), 'the clip the lines point at exists');
|
|
176
|
+
const tip = svg.querySelectorAll('title').find((x) => x.textContent.startsWith('#1 '));
|
|
177
|
+
assert.ok(tip.textContent.includes(wide), 'the full title is still readable on hover');
|
|
178
|
+
});
|
|
179
|
+
|
|
152
180
|
await test('the picture scrolls inside its own region and names both overflow axes', async () => {
|
|
153
181
|
const h = await boot({ payload: { nodes: [t(1), t(2)], edges: [e(2, 1)] } });
|
|
154
182
|
await h.renderers.loadDependencyForest(7);
|