@bongos/core 1.19.900 → 1.19.901
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 +29 -19
- package/docs/copy-inventory.md +8 -7
- package/docs/copy-registry.json +17 -8
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/profile.js +51 -5
- package/modules/ideas/descent.js +103 -32
- package/modules/ideas/routes/inbox.js +10 -0
- package/modules/ideas/trail-descent.js +224 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/helpers.mjs +137 -0
- package/tests/idea_task_descent.mjs +9 -2
- package/tests/idea_trail_descent.mjs +399 -0
- package/tests/profile_activity.mjs +4 -122
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.901",
|
|
6
|
+
"core_contract": "1.19.901",
|
|
7
|
+
"source_commit": "eaeb90f6f672cebb32a56069fe0f4cdab41c983a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-20T06:23:47.726Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 502,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2283,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2811,
|
|
20
|
+
"tree_sha256": "c3fd3f2ffac72933f622b7cb06be2429710627f38d9a3d541cb1ce7f595cdf6a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2097,12 +2097,12 @@
|
|
|
2097
2097
|
{
|
|
2098
2098
|
"path": "docs/copy-inventory.md",
|
|
2099
2099
|
"mode": "0000644",
|
|
2100
|
-
"sha256": "
|
|
2100
|
+
"sha256": "bee7d1411eaf22aaaf7887b53e1151a0654055f8b4a7f6d78802b1eea766aa71"
|
|
2101
2101
|
},
|
|
2102
2102
|
{
|
|
2103
2103
|
"path": "docs/copy-registry.json",
|
|
2104
2104
|
"mode": "0000644",
|
|
2105
|
-
"sha256": "
|
|
2105
|
+
"sha256": "120d68ae5ad77ce670704ccd385b14b523ff7a530742b0038788e480084c6e34"
|
|
2106
2106
|
},
|
|
2107
2107
|
{
|
|
2108
2108
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2632,7 +2632,7 @@
|
|
|
2632
2632
|
{
|
|
2633
2633
|
"path": "docs/module-api-changelog.md",
|
|
2634
2634
|
"mode": "0000644",
|
|
2635
|
-
"sha256": "
|
|
2635
|
+
"sha256": "3ff6099cba4fdc3c31689aa4f007bdb23a8aa5fca100f118eb817e44cf8986c0"
|
|
2636
2636
|
},
|
|
2637
2637
|
{
|
|
2638
2638
|
"path": "docs/modules-contract.md",
|
|
@@ -5037,7 +5037,7 @@
|
|
|
5037
5037
|
{
|
|
5038
5038
|
"path": "modules/hall-ui/public/profile.js",
|
|
5039
5039
|
"mode": "0000644",
|
|
5040
|
-
"sha256": "
|
|
5040
|
+
"sha256": "0ca0433fa846d12e2ea2d69084ad406d6e23fbe92e20c7364da4e92e54e26cf4"
|
|
5041
5041
|
},
|
|
5042
5042
|
{
|
|
5043
5043
|
"path": "modules/hall-ui/public/profile.states.json",
|
|
@@ -5312,7 +5312,7 @@
|
|
|
5312
5312
|
{
|
|
5313
5313
|
"path": "modules/ideas/descent.js",
|
|
5314
5314
|
"mode": "0000644",
|
|
5315
|
-
"sha256": "
|
|
5315
|
+
"sha256": "778558f148d8b403b64f0714b27436b08c41ae0d21ed18b4ed73939928e05cd4"
|
|
5316
5316
|
},
|
|
5317
5317
|
{
|
|
5318
5318
|
"path": "modules/ideas/homeless-home.js",
|
|
@@ -5377,7 +5377,7 @@
|
|
|
5377
5377
|
{
|
|
5378
5378
|
"path": "modules/ideas/routes/inbox.js",
|
|
5379
5379
|
"mode": "0000644",
|
|
5380
|
-
"sha256": "
|
|
5380
|
+
"sha256": "5324f8d2eb8d55decadd371f2c48ee141e88b9aa3d62de0921d467aef401c2ae"
|
|
5381
5381
|
},
|
|
5382
5382
|
{
|
|
5383
5383
|
"path": "modules/ideas/routing.js",
|
|
@@ -5394,6 +5394,11 @@
|
|
|
5394
5394
|
"mode": "0000644",
|
|
5395
5395
|
"sha256": "eade9a2d07da0a39af17c086d188a195be00c544889d61454ed3c6368d39ea04"
|
|
5396
5396
|
},
|
|
5397
|
+
{
|
|
5398
|
+
"path": "modules/ideas/trail-descent.js",
|
|
5399
|
+
"mode": "0000644",
|
|
5400
|
+
"sha256": "2d0693b00dded081b6a0082c450762763d324d54f098a8f42a9aa25ec27370c4"
|
|
5401
|
+
},
|
|
5397
5402
|
{
|
|
5398
5403
|
"path": "modules/lifecycle/CLAUDE.md",
|
|
5399
5404
|
"mode": "0000644",
|
|
@@ -7757,12 +7762,12 @@
|
|
|
7757
7762
|
{
|
|
7758
7763
|
"path": "package-lock.json",
|
|
7759
7764
|
"mode": "0000644",
|
|
7760
|
-
"sha256": "
|
|
7765
|
+
"sha256": "81176248d1045cd137a823208e30bdf4907b4f08ae1c0c0ff85750a38d2aeed3"
|
|
7761
7766
|
},
|
|
7762
7767
|
{
|
|
7763
7768
|
"path": "package.json",
|
|
7764
7769
|
"mode": "0000644",
|
|
7765
|
-
"sha256": "
|
|
7770
|
+
"sha256": "bff9286aa9803a2eedaffb0f0798fcd15a27791208c786a6544424aa04db4a16"
|
|
7766
7771
|
},
|
|
7767
7772
|
{
|
|
7768
7773
|
"path": "public-docs/index.html",
|
|
@@ -9772,7 +9777,7 @@
|
|
|
9772
9777
|
{
|
|
9773
9778
|
"path": "src/module-api.js",
|
|
9774
9779
|
"mode": "0000644",
|
|
9775
|
-
"sha256": "
|
|
9780
|
+
"sha256": "5ac46b294e25abc7c2a076f538a123991a638f3069dd4980ba2b75392942b710"
|
|
9776
9781
|
},
|
|
9777
9782
|
{
|
|
9778
9783
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11722,7 +11727,7 @@
|
|
|
11722
11727
|
{
|
|
11723
11728
|
"path": "tests/helpers.mjs",
|
|
11724
11729
|
"mode": "0000644",
|
|
11725
|
-
"sha256": "
|
|
11730
|
+
"sha256": "40b3377e8f1e69f5f3465297018c9e148582a23aca85d5ea5802c119620562c6"
|
|
11726
11731
|
},
|
|
11727
11732
|
{
|
|
11728
11733
|
"path": "tests/hierarchy-config.mjs",
|
|
@@ -11887,7 +11892,7 @@
|
|
|
11887
11892
|
{
|
|
11888
11893
|
"path": "tests/idea_task_descent.mjs",
|
|
11889
11894
|
"mode": "0000644",
|
|
11890
|
-
"sha256": "
|
|
11895
|
+
"sha256": "36674ef87ccf2d10fca66f4999e62ec8c3518798618c1abfc77f3a6dcc9c330f"
|
|
11891
11896
|
},
|
|
11892
11897
|
{
|
|
11893
11898
|
"path": "tests/idea_task_lineage.mjs",
|
|
@@ -11904,6 +11909,11 @@
|
|
|
11904
11909
|
"mode": "0000644",
|
|
11905
11910
|
"sha256": "943afa2366f92c4d6188ce9f8d68f499aac8d67930f91d6860a2cb83020642ab"
|
|
11906
11911
|
},
|
|
11912
|
+
{
|
|
11913
|
+
"path": "tests/idea_trail_descent.mjs",
|
|
11914
|
+
"mode": "0000644",
|
|
11915
|
+
"sha256": "00701012dbdc9c4d9cb2484aeff4426cb1af0a4643bd979211b361b00af80c69"
|
|
11916
|
+
},
|
|
11907
11917
|
{
|
|
11908
11918
|
"path": "tests/idea_triage.mjs",
|
|
11909
11919
|
"mode": "0000644",
|
|
@@ -12577,7 +12587,7 @@
|
|
|
12577
12587
|
{
|
|
12578
12588
|
"path": "tests/profile_activity.mjs",
|
|
12579
12589
|
"mode": "0000644",
|
|
12580
|
-
"sha256": "
|
|
12590
|
+
"sha256": "483d249339dd696809a4d9ac53aa10d7b67a35ce064884ef5590cc7249418047"
|
|
12581
12591
|
},
|
|
12582
12592
|
{
|
|
12583
12593
|
"path": "tests/profile_connections_ui.mjs",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**1827 strings** across 3 surfaces · 1585 distinct pieces of copy · 1827 certain, 0 heuristic · 166 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 84 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 84 | 1337 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 5 | 390 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `996edffc1966` | 2x | Access requests | modules/hall-ui/public/watch.html:46, modules/hall-ui/public/watch.html:61 |
|
|
79
79
|
| … | | _126 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1337 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -309,7 +309,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
309
309
|
| `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:437` |
|
|
310
310
|
| `a2f2e8babef5` | certain | Could not load that page. Try again. | `modules/hall-ui/public/blockers.js:274` |
|
|
311
311
|
| `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:626` |
|
|
312
|
-
| `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:
|
|
312
|
+
| `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:836` |
|
|
313
313
|
| `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:244` |
|
|
314
314
|
| `edd5bafbe075` | certain | Could not load the queue just now. | `modules/hall-ui/public/ideas.js:565` |
|
|
315
315
|
| `02bb0570996a` | certain | Could not load the ranks. | `modules/hall-ui/public/ranks.js:197` |
|
|
@@ -601,7 +601,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
601
601
|
| `6e73716aec6c` | certain | May be offered for sale later | `modules/hall-ui/public/settings.html:297` |
|
|
602
602
|
| `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/hall-render.js:563` |
|
|
603
603
|
| `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/profile.js:94` |
|
|
604
|
-
| `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:
|
|
604
|
+
| `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:452` |
|
|
605
605
|
| `dce0546c9e34` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this platform — or any p | `modules/hall-ui/public/deploy.html:39` |
|
|
606
606
|
| `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:781` |
|
|
607
607
|
| `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:87` |
|
|
@@ -919,7 +919,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
919
919
|
| `9d4372b173cf` | certain | Standing sections | `modules/hall-ui/public/index.html:79` |
|
|
920
920
|
| `ec1c2dae4a90` | certain | Standing there | `modules/hall-ui/public/profile.js:314` |
|
|
921
921
|
| `eaad12cacb9d` | certain | Start over | `modules/hall-ui/public/studio.js:235` |
|
|
922
|
-
| `efe530c0e3a7` | certain | Still open | `modules/hall-ui/public/profile.js:
|
|
922
|
+
| `efe530c0e3a7` | certain | Still open | `modules/hall-ui/public/profile.js:457` |
|
|
923
923
|
| `9a4ced1afb6c` | certain | Stop waiting on task {…} | `modules/hall-ui/public/task.js:278` |
|
|
924
924
|
| `b0861c874a2f` | certain | Stopped using it. | `modules/hall-ui/public/settings-specialities.js:200` |
|
|
925
925
|
| `6c6544f5c011` | certain | Stored encrypted; never shown again after saving — only its last four digits. | `modules/hall-ui/public/settings.html:193` |
|
|
@@ -1180,6 +1180,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1180
1180
|
| `bc960f460250` | certain | and install dependencies ( | `modules/hall-ui/public/hall-render.js:864` |
|
|
1181
1181
|
| `2177da93be9b` | certain | and leave this alone. | `modules/hall-ui/public/settings.html:339` |
|
|
1182
1182
|
| `b867876e8b6c` | certain | and press Enter. | `modules/hall-ui/public/hall-render.js:825` |
|
|
1183
|
+
| `0257cb508ae7` | certain | and {…} more below this thought | `modules/hall-ui/public/profile.js:415` |
|
|
1183
1184
|
| `d86265f99668` | certain | and {…} more waiting{…} | `modules/hall-ui/public/studio.js:277` |
|
|
1184
1185
|
| `520853131591` | certain | artist mode | `modules/hall-ui/public/studio.html:36` |
|
|
1185
1186
|
| `b4033000c537` | certain | as a table | `modules/hall-ui/public/profile-charts.js:205` |
|
|
@@ -1352,7 +1353,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1352
1353
|
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-lib.js:32` |
|
|
1353
1354
|
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-render.js:682` |
|
|
1354
1355
|
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals.js:45` |
|
|
1355
|
-
| `c3d9687e2375` | certain | {…} of {…} shipped | `modules/hall-ui/public/profile.js:
|
|
1356
|
+
| `c3d9687e2375` | certain | {…} of {…} shipped | `modules/hall-ui/public/profile.js:558` |
|
|
1356
1357
|
| `f32710d0ef37` | certain | {…} of {…} steps cleared. | `modules/hall-ui/public/hall-render.js:1037` |
|
|
1357
1358
|
| `60743fe1e40d` | certain | {…} per page | `modules/hall-ui/public/hall-kit.js:74` |
|
|
1358
1359
|
| `154bc518bab4` | certain | {…} proposed | `modules/hall-ui/public/government.js:695` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1827,
|
|
7
|
+
"unique_ids": 1585,
|
|
8
|
+
"certain": 1827,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 166
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 84,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1337
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -3824,7 +3824,7 @@
|
|
|
3824
3824
|
"surface": "builders-hall",
|
|
3825
3825
|
"text": "Could not load the profile.",
|
|
3826
3826
|
"file": "modules/hall-ui/public/profile.js",
|
|
3827
|
-
"line":
|
|
3827
|
+
"line": 836,
|
|
3828
3828
|
"origin": "js-notice",
|
|
3829
3829
|
"confidence": "certain"
|
|
3830
3830
|
},
|
|
@@ -6452,7 +6452,7 @@
|
|
|
6452
6452
|
"surface": "builders-hall",
|
|
6453
6453
|
"text": "Merged into",
|
|
6454
6454
|
"file": "modules/hall-ui/public/profile.js",
|
|
6455
|
-
"line":
|
|
6455
|
+
"line": 452,
|
|
6456
6456
|
"origin": "js-markup",
|
|
6457
6457
|
"confidence": "certain"
|
|
6458
6458
|
},
|
|
@@ -9314,7 +9314,7 @@
|
|
|
9314
9314
|
"surface": "builders-hall",
|
|
9315
9315
|
"text": "Still open",
|
|
9316
9316
|
"file": "modules/hall-ui/public/profile.js",
|
|
9317
|
-
"line":
|
|
9317
|
+
"line": 457,
|
|
9318
9318
|
"origin": "js-markup",
|
|
9319
9319
|
"confidence": "certain"
|
|
9320
9320
|
},
|
|
@@ -11658,6 +11658,15 @@
|
|
|
11658
11658
|
"origin": "js-markup",
|
|
11659
11659
|
"confidence": "certain"
|
|
11660
11660
|
},
|
|
11661
|
+
{
|
|
11662
|
+
"id": "0257cb508ae7",
|
|
11663
|
+
"surface": "builders-hall",
|
|
11664
|
+
"text": "and {…} more below this thought",
|
|
11665
|
+
"file": "modules/hall-ui/public/profile.js",
|
|
11666
|
+
"line": 415,
|
|
11667
|
+
"origin": "js-markup",
|
|
11668
|
+
"confidence": "certain"
|
|
11669
|
+
},
|
|
11661
11670
|
{
|
|
11662
11671
|
"id": "d86265f99668",
|
|
11663
11672
|
"surface": "builders-hall",
|
|
@@ -13211,7 +13220,7 @@
|
|
|
13211
13220
|
"surface": "builders-hall",
|
|
13212
13221
|
"text": "{…} of {…} shipped",
|
|
13213
13222
|
"file": "modules/hall-ui/public/profile.js",
|
|
13214
|
-
"line":
|
|
13223
|
+
"line": 558,
|
|
13215
13224
|
"origin": "js-markup",
|
|
13216
13225
|
"confidence": "certain"
|
|
13217
13226
|
},
|
|
@@ -2274,5 +2274,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2274
2274
|
landed since 1.19.898 with no explicit bump. run 35488237998. (task 1002620)
|
|
2275
2275
|
1.19.900 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2276
2276
|
landed since 1.19.899 with no explicit bump. run 35492534307. (task 1002620)
|
|
2277
|
+
1.19.901 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2278
|
+
landed since 1.19.900 with no explicit bump. run 35494090938. (task 1002620)
|
|
2277
2279
|
---------------------------------------------------------------------------
|
|
2278
2280
|
```
|
|
@@ -378,9 +378,55 @@
|
|
|
378
378
|
const ideaHref = (id) => href(`${hallBase()}/idea/${encodeURIComponent(id)}`);
|
|
379
379
|
const goalHref = (id) => href(`${hallBase()}/goals#/goal/${encodeURIComponent(id)}`);
|
|
380
380
|
|
|
381
|
+
// THE DESCENT — everything an idea's tasks went on to cause, at any depth and
|
|
382
|
+
// across version boundaries (task 1004089, BV2.R09; ADR 0312). Served as
|
|
383
|
+
// `descent_tasks`, already ordered nearest-first by the server, each entry at
|
|
384
|
+
// its shortest depth from one of the idea's own tasks.
|
|
385
|
+
//
|
|
386
|
+
// WHY IT RENDERS HERE, under the outcome and not beside it. The outcome line
|
|
387
|
+
// answers "what became of my idea" with the FIRST thing it became, which is
|
|
388
|
+
// all the trail could ever say while the read was one hop. The descent is the
|
|
389
|
+
// rest of that sentence, so it belongs in the same cell, subordinate to it.
|
|
390
|
+
//
|
|
391
|
+
// CAPPED, and the cap says so. The whole descent is the idea page's job
|
|
392
|
+
// (BV2.R10); a ledger row that paints forty tasks stops being a row. The
|
|
393
|
+
// overflow is a LINK to that page, never a silent truncation.
|
|
394
|
+
const DESCENT_SHOWN = 6;
|
|
395
|
+
function descentHtml(idea) {
|
|
396
|
+
const rows = Array.isArray(idea.descent_tasks) ? idea.descent_tasks : [];
|
|
397
|
+
if (!rows.length) return '';
|
|
398
|
+
const cells = rows.slice(0, DESCENT_SHOWN).map((t) => {
|
|
399
|
+
// The version is printed rather than compared away. A descendant living
|
|
400
|
+
// in a LATER version is the interesting case — it is the proof that an
|
|
401
|
+
// origin tag survives a version boundary — so it is labelled, not
|
|
402
|
+
// filtered and not silently flattened into the idea's own version.
|
|
403
|
+
const meta = [
|
|
404
|
+
`${escapeHtml(String(t.relation || 'descends'))}`,
|
|
405
|
+
`depth ${escapeHtml(String(t.depth))}`,
|
|
406
|
+
t.version_id ? escapeHtml(String(t.version_id)) : null,
|
|
407
|
+
].filter(Boolean).join(' · ');
|
|
408
|
+
return `<span style="display:block;margin-top:2px">`
|
|
409
|
+
+ `<a href="${taskHref(t.task_id)}">#${escapeHtml(String(t.task_id))} ${escapeHtml(t.title || '')}</a>`
|
|
410
|
+
+ ` <span style="font-size:11px;color:var(--ink-faint)">${meta}</span>`
|
|
411
|
+
+ `</span>`;
|
|
412
|
+
}).join('');
|
|
413
|
+
const rest = rows.length - DESCENT_SHOWN;
|
|
414
|
+
const more = rest > 0
|
|
415
|
+
? `<span style="display:block;margin-top:2px;font-size:12px"><a href="${ideaHref(idea.id)}">and ${escapeHtml(String(rest))} more below this thought</a></span>`
|
|
416
|
+
: '';
|
|
417
|
+
return `<span style="display:block;margin-top:4px;font-size:12px;color:var(--ink-faint)">`
|
|
418
|
+
+ `Then caused ${escapeHtml(String(rows.length))} ${rows.length === 1 ? 'task' : 'tasks'}</span>`
|
|
419
|
+
+ cells + more;
|
|
420
|
+
}
|
|
421
|
+
|
|
381
422
|
// What became of one idea, in a sentence a person can read. The four outcomes
|
|
382
423
|
// the criterion names, each linked through to the thing it became.
|
|
383
424
|
function ideaOutcomeHtml(idea) {
|
|
425
|
+
// The descent rides under EVERY outcome, not just a promotion: a merged
|
|
426
|
+
// idea inherits the descent of the idea it merged into (that is what
|
|
427
|
+
// "nothing is lost" means), and an idea whose first task was later split
|
|
428
|
+
// has a descent whatever its own status says.
|
|
429
|
+
const descent = descentHtml(idea);
|
|
384
430
|
const note = idea.triage_note
|
|
385
431
|
? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(idea.triage_note)}</span>`
|
|
386
432
|
: '';
|
|
@@ -396,19 +442,19 @@
|
|
|
396
442
|
: []);
|
|
397
443
|
// 'promoted' with nothing to point at is a real (old) state, not a bug to
|
|
398
444
|
// paper over — say what is known rather than inventing a link.
|
|
399
|
-
if (!links.length) return `<span class="task__tag">Promoted</span>${note}`;
|
|
400
|
-
return `<span class="task__tag">Became ${links.length === 1 ? 'a task' : `${links.length} tasks`}</span> ${links.join(' · ')}${note}`;
|
|
445
|
+
if (!links.length) return `<span class="task__tag">Promoted</span>${note}${descent}`;
|
|
446
|
+
return `<span class="task__tag">Became ${links.length === 1 ? 'a task' : `${links.length} tasks`}</span> ${links.join(' · ')}${note}${descent}`;
|
|
401
447
|
}
|
|
402
448
|
if (idea.status === 'merged') {
|
|
403
449
|
const target = idea.merged_into_idea_id
|
|
404
450
|
? ` <a href="${ideaHref(idea.merged_into_idea_id)}">#${escapeHtml(String(idea.merged_into_idea_id))} ${escapeHtml(idea.merged_into_idea_title || '')}</a>`
|
|
405
451
|
: '';
|
|
406
|
-
return `<span class="task__tag">Merged into</span>${target}${note}`;
|
|
452
|
+
return `<span class="task__tag">Merged into</span>${target}${note}${descent}`;
|
|
407
453
|
}
|
|
408
454
|
if (idea.status === 'discarded') {
|
|
409
|
-
return `<span class="task__tag">Discarded</span>${note}`;
|
|
455
|
+
return `<span class="task__tag">Discarded</span>${note}${descent}`;
|
|
410
456
|
}
|
|
411
|
-
return `<span class="task__tag">Still open</span>${note}`;
|
|
457
|
+
return `<span class="task__tag">Still open</span>${note}${descent}`;
|
|
412
458
|
}
|
|
413
459
|
|
|
414
460
|
// One ledger driven by one server-paged endpoint. `fetchPage(offset, perPage)`
|
package/modules/ideas/descent.js
CHANGED
|
@@ -22,8 +22,94 @@ const DESCENT_MAX_DEPTH = 64;
|
|
|
22
22
|
|
|
23
23
|
const DESCENT_RELATIONS = Object.freeze(['rewrite', 'split', 'merge', 'fold', 'cascade']);
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// ── the walk: one implementation, batched and single-root ─────────────────
|
|
26
|
+
// walkMany — a breadth-first walk from MANY roots at once: ONE query per LEVEL
|
|
27
|
+
// for the whole set, rather than one walk per root.
|
|
26
28
|
//
|
|
29
|
+
// WHY THE BATCH IS THE POINT OF THIS FUNCTION (task 1004089, BV2.R09). The
|
|
30
|
+
// descent is read for a PAGE of ideas, not for one. Walking each root on its
|
|
31
|
+
// own costs (roots x depth) round trips, which is the 4-6 serial round trips
|
|
32
|
+
// per read that sank the previous attempt at this read. Batched, the cost is
|
|
33
|
+
// depth round trips for the whole page — INDEPENDENT of how many ideas the page
|
|
34
|
+
// carries, which is the property tests/idea_trail_descent.mjs pins.
|
|
35
|
+
//
|
|
36
|
+
// WHY NOT A RECURSIVE CTE, which collapses it to literally one query: the walk
|
|
37
|
+
// comment above says it, and nothing about batching changes that argument. A
|
|
38
|
+
// CTE re-expands every path, so a convergent DAG costs work exponential in
|
|
39
|
+
// depth; here the whole page costs at most one query per level whatever the
|
|
40
|
+
// shape. One query per level is the floor a walk that must not blow up can
|
|
41
|
+
// reach, and this is that floor.
|
|
42
|
+
//
|
|
43
|
+
// PER-ROOT ATTRIBUTION is why the row carries `from_id`. The union of every
|
|
44
|
+
// root's frontier goes to the database in one array, so a row coming back has
|
|
45
|
+
// to say which frontier node produced it or the answer cannot be split back
|
|
46
|
+
// apart. Depth is per-root and must stay that way: the payout decays by the
|
|
47
|
+
// depth from THAT origin, so one shared answer would pay the wrong rate.
|
|
48
|
+
//
|
|
49
|
+
// Returns Map<rootKey, rows> — rootKey is String(rootId), rows exactly as the
|
|
50
|
+
// single-root walk returns them.
|
|
51
|
+
async function walkMany(startIds, direction, { maxDepth = DESCENT_MAX_DEPTH, client = null } = {}) {
|
|
52
|
+
const q = client || pool;
|
|
53
|
+
// Two literal statements rather than one with an interpolated column name:
|
|
54
|
+
// nothing here is caller-controlled, and a query built by concatenation is a
|
|
55
|
+
// shape a reader (and a scanner) has to stop and prove safe.
|
|
56
|
+
const SQL_UP = `SELECT parent_task_id AS id, task_id AS from_id, relation
|
|
57
|
+
FROM ideas_task_descent
|
|
58
|
+
WHERE task_id = ANY($1::bigint[])`;
|
|
59
|
+
const SQL_DOWN = `SELECT task_id AS id, parent_task_id AS from_id, relation
|
|
60
|
+
FROM ideas_task_descent
|
|
61
|
+
WHERE parent_task_id = ANY($1::bigint[])`;
|
|
62
|
+
const sql = direction === 'up' ? SQL_UP : SQL_DOWN;
|
|
63
|
+
|
|
64
|
+
// A root that is not a positive integer is dropped here rather than sent to
|
|
65
|
+
// the database as part of a bigint[]. linkTaskDescent already rejects a bad
|
|
66
|
+
// id by name before it ever walks; this is the second line, for the readers
|
|
67
|
+
// that take their roots from a row rather than from an argument.
|
|
68
|
+
const roots = [...new Set((startIds || []).map((r) => String(r)))].filter((r) => /^[1-9]\d*$/.test(r));
|
|
69
|
+
|
|
70
|
+
// One independent walk state per root, stepped in lockstep so that every
|
|
71
|
+
// root's frontier is expanded by the same query.
|
|
72
|
+
const state = new Map();
|
|
73
|
+
for (const r of roots) {
|
|
74
|
+
state.set(r, { found: new Map(), visited: new Set([r]), frontier: [Number(r)] });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let depth = 1; depth <= maxDepth; depth += 1) {
|
|
78
|
+
const union = new Set();
|
|
79
|
+
for (const s of state.values()) for (const n of s.frontier) union.add(n);
|
|
80
|
+
if (union.size === 0) break;
|
|
81
|
+
const { rows } = await q.query(sql, [[...union]]);
|
|
82
|
+
// from_id -> the edges it produced, so each root can expand only the nodes
|
|
83
|
+
// that were actually on ITS frontier.
|
|
84
|
+
const adjacency = new Map();
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const from = String(row.from_id);
|
|
87
|
+
if (!adjacency.has(from)) adjacency.set(from, []);
|
|
88
|
+
adjacency.get(from).push(row);
|
|
89
|
+
}
|
|
90
|
+
for (const s of state.values()) {
|
|
91
|
+
const next = [];
|
|
92
|
+
for (const node of s.frontier) {
|
|
93
|
+
for (const edge of adjacency.get(String(node)) || []) {
|
|
94
|
+
const key = String(edge.id);
|
|
95
|
+
if (s.visited.has(key)) continue; // already reached at a shallower depth
|
|
96
|
+
s.visited.add(key);
|
|
97
|
+
s.found.set(key, { id: edge.id, depth, relation: edge.relation });
|
|
98
|
+
next.push(Number(edge.id));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
s.frontier = next;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const out = new Map();
|
|
106
|
+
for (const [root, s] of state) {
|
|
107
|
+
out.set(root, [...s.found.values()].sort((a, b) => a.depth - b.depth || Number(a.id) - Number(b.id)));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Both of the above are breadth-first over the edge table, one query PER LEVEL.
|
|
27
113
|
// WHY NOT A RECURSIVE CTE, which is the obvious shape and was the first cut.
|
|
28
114
|
// A recursive CTE's working table re-expands every path independently, so a
|
|
29
115
|
// densely-connected DAG — paths that converge and diverge again, which is
|
|
@@ -43,37 +129,13 @@ const DESCENT_RELATIONS = Object.freeze(['rewrite', 'split', 'merge', 'fold', 'c
|
|
|
43
129
|
// The cost is one round trip per level instead of one per walk. Depth decay
|
|
44
130
|
// halves each hop (spec decision 16), so a descent deep enough for that to
|
|
45
131
|
// matter is already worth almost nothing.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
WHERE task_id = ANY($1::bigint[])`;
|
|
54
|
-
const SQL_DOWN = `SELECT task_id AS id, relation
|
|
55
|
-
FROM ideas_task_descent
|
|
56
|
-
WHERE parent_task_id = ANY($1::bigint[])`;
|
|
57
|
-
const sql = direction === 'up' ? SQL_UP : SQL_DOWN;
|
|
58
|
-
|
|
59
|
-
const found = new Map();
|
|
60
|
-
const visited = new Set([String(startId)]);
|
|
61
|
-
let frontier = [Number(startId)];
|
|
62
|
-
|
|
63
|
-
for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth += 1) {
|
|
64
|
-
const { rows } = await q.query(sql, [frontier]);
|
|
65
|
-
const next = [];
|
|
66
|
-
for (const r of rows) {
|
|
67
|
-
const key = String(r.id);
|
|
68
|
-
if (visited.has(key)) continue; // already reached at a shallower depth
|
|
69
|
-
visited.add(key);
|
|
70
|
-
found.set(key, { id: r.id, depth, relation: r.relation });
|
|
71
|
-
next.push(Number(r.id));
|
|
72
|
-
}
|
|
73
|
-
frontier = next;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return [...found.values()].sort((a, b) => a.depth - b.depth || Number(a.id) - Number(b.id));
|
|
132
|
+
// walk — the single-root walk, which is walkMany with one root. ONE
|
|
133
|
+
// implementation on purpose: the payout (R11) walks up and the descent surface
|
|
134
|
+
// (R10) walks down, and a second copy of the de-duplication would be free to
|
|
135
|
+
// drift from this one — a drift that shows up as money, not as a render bug.
|
|
136
|
+
async function walk(startId, direction, opts = {}) {
|
|
137
|
+
const byRoot = await walkMany([startId], direction, opts);
|
|
138
|
+
return byRoot.get(String(startId)) || [];
|
|
77
139
|
}
|
|
78
140
|
|
|
79
141
|
// ancestorsOfTask — every task this one descends from, nearest first, each at
|
|
@@ -87,6 +149,14 @@ function descendantsOfTask(taskId, opts = {}) {
|
|
|
87
149
|
return walk(taskId, 'down', opts);
|
|
88
150
|
}
|
|
89
151
|
|
|
152
|
+
// descendantsOfTasks — the descent below SEVERAL roots, in one batch. What the
|
|
153
|
+
// trail read (modules/ideas/trail-descent.js) consumes: a page of ideas has a
|
|
154
|
+
// root task per promotion, and the whole page must cost one query per level,
|
|
155
|
+
// not one walk per idea. Returns Map<String(rootId), rows>.
|
|
156
|
+
function descendantsOfTasks(taskIds, opts = {}) {
|
|
157
|
+
return walkMany(taskIds, 'down', opts);
|
|
158
|
+
}
|
|
159
|
+
|
|
90
160
|
// linkTaskDescent — record that `taskId` came out of `parentTaskId`.
|
|
91
161
|
//
|
|
92
162
|
// Idempotent on the pair, because the derivation below runs on every ship and
|
|
@@ -209,6 +279,7 @@ module.exports = {
|
|
|
209
279
|
linkTaskDescent,
|
|
210
280
|
ancestorsOfTask,
|
|
211
281
|
descendantsOfTask,
|
|
282
|
+
descendantsOfTasks,
|
|
212
283
|
deriveEdgesForTask,
|
|
213
284
|
syncDescentForTask,
|
|
214
285
|
};
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
const express = require('express');
|
|
36
36
|
const api = require('../../../src/module-api');
|
|
37
37
|
const inbox = require('../inbox');
|
|
38
|
+
// The trail read to the FULL depth of the descent (task 1004089, BV2.R09). Its
|
|
39
|
+
// own file because inbox.js is AT the size ratchet and because a task->task
|
|
40
|
+
// walk is a different relation from the idea->task trail.
|
|
41
|
+
const { attachDescentToTrail } = require('../trail-descent');
|
|
38
42
|
// Full Idea capture (task 1002919 / BV1.R143): the template validator (R141)
|
|
39
43
|
// and the deterministic scorer (R142) — both pure, both walk the declaration.
|
|
40
44
|
const { validateFieldValues, templateFor, fieldOrder } = require('../templates');
|
|
@@ -552,6 +556,12 @@ module.exports = function buildInboxRouter() {
|
|
|
552
556
|
inbox.listIdeaTrailForBuilder(builderId, { limit, offset, viewerRank }),
|
|
553
557
|
inbox.countIdeaTrailForBuilder(builderId, { viewerRank }),
|
|
554
558
|
]);
|
|
559
|
+
// Everything those tasks went on to cause, to any depth and across version
|
|
560
|
+
// boundaries (BV2.R09, ADR 0312). Composed here rather than inside the trail
|
|
561
|
+
// read: the descent is a task->task walk, the trail is an idea->task read,
|
|
562
|
+
// and the walk is batched across the WHOLE page — so it stays one query per
|
|
563
|
+
// level however many ideas the window carries, never one walk per idea.
|
|
564
|
+
await attachDescentToTrail(ideas);
|
|
555
565
|
return res.json({ builder_id: builderId, ideas, total, limit, offset });
|
|
556
566
|
}, { errorCode: 'idea_trail_failed' }));
|
|
557
567
|
|