@bongos/core 1.19.921 → 1.19.922
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 +46 -21
- package/docs/adr/0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md +163 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/economy/idea-brightness.js +212 -0
- package/modules/economy/idea-size-historical.js +22 -3
- package/modules/economy/idea-size.js +30 -3
- package/modules/lifecycle/cascade.js +36 -0
- package/modules/lifecycle/migrations/lifecycle_010_removal_targets.sql +116 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/removal-targets.js +217 -0
- package/modules/lifecycle/routes/lifecycle.js +9 -0
- package/modules/lifecycle/routes/removal-proposals.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-core-upgrade.js +26 -2
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_runner.mjs +56 -0
- package/tests/idea_brightness.mjs +550 -0
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.922",
|
|
6
|
+
"core_contract": "1.19.922",
|
|
7
|
+
"source_commit": "b57f79a73cefa42840ff10d77aab958af1fe4ed4",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-20T20:
|
|
9
|
+
"built_at": "2026-09-20T20:40:52.111Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 510,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2309,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2845,
|
|
20
|
+
"tree_sha256": "36516d370c40955c53b7b0b70845d49e03d34aaecf7d5e58a54bf78ed454f694",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2094,10 +2094,15 @@
|
|
|
2094
2094
|
"mode": "0000644",
|
|
2095
2095
|
"sha256": "e582b4ece7a5b0acf4392d8b3d4942d55b52dc487d7c6a7c3a159203485ec633"
|
|
2096
2096
|
},
|
|
2097
|
+
{
|
|
2098
|
+
"path": "docs/adr/0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md",
|
|
2099
|
+
"mode": "0000644",
|
|
2100
|
+
"sha256": "3a743808de440eebd5fcdd74a4c88e41acc82cc02ce5632cb392b2aa0c05b48e"
|
|
2101
|
+
},
|
|
2097
2102
|
{
|
|
2098
2103
|
"path": "docs/adr/README.md",
|
|
2099
2104
|
"mode": "0000644",
|
|
2100
|
-
"sha256": "
|
|
2105
|
+
"sha256": "caece332e048f6ee15517740255c4d715efc025c82756ac095ccde7814b60daa"
|
|
2101
2106
|
},
|
|
2102
2107
|
{
|
|
2103
2108
|
"path": "docs/api-reference.md",
|
|
@@ -2667,7 +2672,7 @@
|
|
|
2667
2672
|
{
|
|
2668
2673
|
"path": "docs/module-api-changelog.md",
|
|
2669
2674
|
"mode": "0000644",
|
|
2670
|
-
"sha256": "
|
|
2675
|
+
"sha256": "d657d8b2fc7eb2bb4379a22a41d95b835ac655848626120a2218ab7603df5313"
|
|
2671
2676
|
},
|
|
2672
2677
|
{
|
|
2673
2678
|
"path": "docs/modules-contract.md",
|
|
@@ -4229,15 +4234,20 @@
|
|
|
4229
4234
|
"mode": "0000644",
|
|
4230
4235
|
"sha256": "736eed2c89863d6eb28072627a40ac4f8945832cfb7b1d1aba10e6cc65483901"
|
|
4231
4236
|
},
|
|
4237
|
+
{
|
|
4238
|
+
"path": "modules/economy/idea-brightness.js",
|
|
4239
|
+
"mode": "0000644",
|
|
4240
|
+
"sha256": "745da28c27ebcd50b349c3fe027f7e3f14888f9e012e0c9443656de249444ba1"
|
|
4241
|
+
},
|
|
4232
4242
|
{
|
|
4233
4243
|
"path": "modules/economy/idea-size-historical.js",
|
|
4234
4244
|
"mode": "0000644",
|
|
4235
|
-
"sha256": "
|
|
4245
|
+
"sha256": "215eefac80aaf3624a8ff5d5284f29388a2e1b492424dd152b3d9bbe074e3a01"
|
|
4236
4246
|
},
|
|
4237
4247
|
{
|
|
4238
4248
|
"path": "modules/economy/idea-size.js",
|
|
4239
4249
|
"mode": "0000644",
|
|
4240
|
-
"sha256": "
|
|
4250
|
+
"sha256": "8818bea53d2ce90fe0bc98b53bfed1af11c4eeb541942b96c4cf50a162791634"
|
|
4241
4251
|
},
|
|
4242
4252
|
{
|
|
4243
4253
|
"path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
|
|
@@ -5522,7 +5532,7 @@
|
|
|
5522
5532
|
{
|
|
5523
5533
|
"path": "modules/lifecycle/cascade.js",
|
|
5524
5534
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5535
|
+
"sha256": "176f7463d6f3c7bf2297760d0934f88528ee180dfb0c9e49a4347a449af2820d"
|
|
5526
5536
|
},
|
|
5527
5537
|
{
|
|
5528
5538
|
"path": "modules/lifecycle/check-rollup.js",
|
|
@@ -5764,6 +5774,11 @@
|
|
|
5764
5774
|
"mode": "0000644",
|
|
5765
5775
|
"sha256": "e98efbb1537f0d99adb6b6b66c1fdccfd4bbac16b97362d479af903eb9de492c"
|
|
5766
5776
|
},
|
|
5777
|
+
{
|
|
5778
|
+
"path": "modules/lifecycle/migrations/lifecycle_010_removal_targets.sql",
|
|
5779
|
+
"mode": "0000644",
|
|
5780
|
+
"sha256": "e3e29e791eeb94ab1380ae2e2a13eafaea413faf0d0324a4e2007ab2b3002fca"
|
|
5781
|
+
},
|
|
5767
5782
|
{
|
|
5768
5783
|
"path": "modules/lifecycle/mingle.js",
|
|
5769
5784
|
"mode": "0000644",
|
|
@@ -5772,7 +5787,7 @@
|
|
|
5772
5787
|
{
|
|
5773
5788
|
"path": "modules/lifecycle/module.json",
|
|
5774
5789
|
"mode": "0000644",
|
|
5775
|
-
"sha256": "
|
|
5790
|
+
"sha256": "36a03e5d8a4fc1129439356ba6daaf284d2cd2ca3d98d35f990e3215ff11198a"
|
|
5776
5791
|
},
|
|
5777
5792
|
{
|
|
5778
5793
|
"path": "modules/lifecycle/pg-constraint-fail.js",
|
|
@@ -5804,6 +5819,11 @@
|
|
|
5804
5819
|
"mode": "0000644",
|
|
5805
5820
|
"sha256": "8270ce55255998a4f0bbf574a54df0ba2597013c727b4afb1ce2abad9c9d4d2a"
|
|
5806
5821
|
},
|
|
5822
|
+
{
|
|
5823
|
+
"path": "modules/lifecycle/removal-targets.js",
|
|
5824
|
+
"mode": "0000644",
|
|
5825
|
+
"sha256": "e80304e7b43fe86852763e2f859bc05c1b88bb50a3eea407342ae6d46b8f9eda"
|
|
5826
|
+
},
|
|
5807
5827
|
{
|
|
5808
5828
|
"path": "modules/lifecycle/rot.js",
|
|
5809
5829
|
"mode": "0000644",
|
|
@@ -5872,7 +5892,7 @@
|
|
|
5872
5892
|
{
|
|
5873
5893
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
5874
5894
|
"mode": "0000644",
|
|
5875
|
-
"sha256": "
|
|
5895
|
+
"sha256": "330e42fad79812a05a90256fe31931df2156093b80a648d7c838ecac136c91e4"
|
|
5876
5896
|
},
|
|
5877
5897
|
{
|
|
5878
5898
|
"path": "modules/lifecycle/routes/mingle.js",
|
|
@@ -5892,7 +5912,7 @@
|
|
|
5892
5912
|
{
|
|
5893
5913
|
"path": "modules/lifecycle/routes/removal-proposals.js",
|
|
5894
5914
|
"mode": "0000644",
|
|
5895
|
-
"sha256": "
|
|
5915
|
+
"sha256": "e0a0e14a8818c011334e8f61a50c8dd7ad11b4387acfd6ab9e18e570f54ec6c4"
|
|
5896
5916
|
},
|
|
5897
5917
|
{
|
|
5898
5918
|
"path": "modules/lifecycle/routes/rot.js",
|
|
@@ -7882,12 +7902,12 @@
|
|
|
7882
7902
|
{
|
|
7883
7903
|
"path": "package-lock.json",
|
|
7884
7904
|
"mode": "0000644",
|
|
7885
|
-
"sha256": "
|
|
7905
|
+
"sha256": "0169c3fafa67ebf63c9ae35566e8104bcf210ae80973c8e1837874109fe85354"
|
|
7886
7906
|
},
|
|
7887
7907
|
{
|
|
7888
7908
|
"path": "package.json",
|
|
7889
7909
|
"mode": "0000644",
|
|
7890
|
-
"sha256": "
|
|
7910
|
+
"sha256": "b62dd828c8e8b167dfc4bca7d54fdec8c76b13f89a44e61365d48962abf77ba9"
|
|
7891
7911
|
},
|
|
7892
7912
|
{
|
|
7893
7913
|
"path": "public-docs/index.html",
|
|
@@ -8832,7 +8852,7 @@
|
|
|
8832
8852
|
{
|
|
8833
8853
|
"path": "scripts/gds/provision-core-upgrade.js",
|
|
8834
8854
|
"mode": "0000644",
|
|
8835
|
-
"sha256": "
|
|
8855
|
+
"sha256": "1fd6e38184bb0943fdc22e314b9e6cad0b5ebeae7f7370992adbd3b4f2ad5a9e"
|
|
8836
8856
|
},
|
|
8837
8857
|
{
|
|
8838
8858
|
"path": "scripts/gds/provision-disconnect.js",
|
|
@@ -9887,7 +9907,7 @@
|
|
|
9887
9907
|
{
|
|
9888
9908
|
"path": "src/module-api.js",
|
|
9889
9909
|
"mode": "0000644",
|
|
9890
|
-
"sha256": "
|
|
9910
|
+
"sha256": "86c69951f0059e5374245ef798c5cd24cd82d836a4ccf706e80f186de986b473"
|
|
9891
9911
|
},
|
|
9892
9912
|
{
|
|
9893
9913
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10802,7 +10822,7 @@
|
|
|
10802
10822
|
{
|
|
10803
10823
|
"path": "tests/core_upgrade_runner.mjs",
|
|
10804
10824
|
"mode": "0000644",
|
|
10805
|
-
"sha256": "
|
|
10825
|
+
"sha256": "5d0b4fa1ec48d40ebc801902b5279475533ae2dc8819e587bb95bc17783cba06"
|
|
10806
10826
|
},
|
|
10807
10827
|
{
|
|
10808
10828
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
@@ -11894,6 +11914,11 @@
|
|
|
11894
11914
|
"mode": "0000644",
|
|
11895
11915
|
"sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
|
|
11896
11916
|
},
|
|
11917
|
+
{
|
|
11918
|
+
"path": "tests/idea_brightness.mjs",
|
|
11919
|
+
"mode": "0000644",
|
|
11920
|
+
"sha256": "d67d60b1236ccedd01591356f5a5a9a72e4f1efd895ddf5af00c744219a2b3b6"
|
|
11921
|
+
},
|
|
11897
11922
|
{
|
|
11898
11923
|
"path": "tests/idea_capture_grades.mjs",
|
|
11899
11924
|
"mode": "0000644",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# ADR 0321 — Brightness is a payout-weighted share, and a removal's targets are rows
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-20
|
|
5
|
+
- **Task:** [task 1004099](https://cloudbongos.com/builders#/task/1004099) (BV2.R16, goal 1000095 — BONGOS-V2)
|
|
6
|
+
- **Spec:** `docs/specs/<redacted>.md` decisions 4 and 20
|
|
7
|
+
- **Under:** [ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md), [ADR 0319](0319-a-removal-verdict-is-a-row-beside-the-question.md)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
An idea's **size** is its total ideator payout from its descent and never falls
|
|
12
|
+
([ADR 0312](0312-provenance-is-a-second-ideas-owned-edge-table.md) and
|
|
13
|
+
`modules/economy/idea-size.js`). **Brightness** is the other channel: how much of that
|
|
14
|
+
work is still alive. The pair is what lets a thought whose version the project later tore
|
|
15
|
+
out draw *large and dark* — visibly different from a thought that never caused anything
|
|
16
|
+
and draws *small and dark*. One number cannot express both, which is why there are two.
|
|
17
|
+
|
|
18
|
+
Two things had to be settled before it could be computed.
|
|
19
|
+
|
|
20
|
+
**First, what a share is a share OF.** The spec says graded, not binary (decision 20): a
|
|
21
|
+
descent of five things with three alive is partly lit, and goes out only when nothing
|
|
22
|
+
survives.
|
|
23
|
+
|
|
24
|
+
**Second, and the real blocker: the removal facts are stored in the wrong direction.**
|
|
25
|
+
ADR 0319 gave a confirmed removal a row — `lifecycle_removal_verdicts`, keyed on the
|
|
26
|
+
proposal, carrying the verdict and `ship_task_id`. It also said, in the migration and in
|
|
27
|
+
its own text, that the list of tasks whose work was removed was deliberately absent, and
|
|
28
|
+
that *"brightness designs its own storage for targets when it needs one."* That deferral
|
|
29
|
+
is now due.
|
|
30
|
+
|
|
31
|
+
The gap is not cosmetic. `ship_task_id` is the task whose merge **deleted** the files.
|
|
32
|
+
Brightness needs the previously-shipped tasks that **stopped counting**. Those are
|
|
33
|
+
different rows, almost always under different ideas — a later, unrelated piece of work
|
|
34
|
+
tears out an earlier one. Reading the ship as the removed task dims the wrong star, and
|
|
35
|
+
dims it for the wrong idea, while looking like a working query.
|
|
36
|
+
|
|
37
|
+
The attribution itself already exists. `modules/lifecycle/removal-derivation.js` blames
|
|
38
|
+
each deleted path back to the task that introduced it, and the cascade renders the result
|
|
39
|
+
as prose in the proposal's body. `removal-proposals.js` refuses to parse that prose back
|
|
40
|
+
and is right to: *"a body is written for a person to read, and a reader that silently
|
|
41
|
+
depends on its line shape breaks the day somebody improves the wording."*
|
|
42
|
+
|
|
43
|
+
## Decision
|
|
44
|
+
|
|
45
|
+
### D1 — The targets are rows, written at proposal time
|
|
46
|
+
|
|
47
|
+
`modules/lifecycle/migrations/lifecycle_010_removal_targets.sql` adds
|
|
48
|
+
`lifecycle_removal_targets (proposal_task_id, removed_task_id, paths)`, written by
|
|
49
|
+
`recordRemovalTargets` from the same `ctx.removals` objects the proposal body renders.
|
|
50
|
+
**One derivation, two renderings** — the prose and the rows cannot disagree, and
|
|
51
|
+
`removal-proposals.js` keeps its refusal intact because something else now carries the
|
|
52
|
+
fact.
|
|
53
|
+
|
|
54
|
+
It must be written at **proposal** time rather than at verdict time. The attribution comes
|
|
55
|
+
from GitHub commit-history reads, capped at eight paths per ship
|
|
56
|
+
(`MAX_ATTRIBUTED_PATHS`) and running on the server's shared push credential — the same
|
|
57
|
+
credential the publish path depends on. Re-deriving when a person clicks Confirm, days
|
|
58
|
+
later, would spend that budget again against a history that has moved on.
|
|
59
|
+
|
|
60
|
+
This is **not** the cascade runner fanning out, which [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md)
|
|
61
|
+
forbids: the removal rule still files exactly one task. This records a side fact *about*
|
|
62
|
+
that one task — the same shape as the verdict row its answer writes.
|
|
63
|
+
|
|
64
|
+
`ON CONFLICT DO NOTHING`, never `DO UPDATE`. A second walk can legitimately find *fewer*
|
|
65
|
+
paths, because the cap and the rate limit make an exhausted page yield no attribution at
|
|
66
|
+
all; overwriting would let a degraded re-read erase a good first one. For the same
|
|
67
|
+
reason the writer runs on the cascade's `skipped` row as well as its `created` one, so a
|
|
68
|
+
re-ship **repairs** an attribution that a derivation outage missed.
|
|
69
|
+
|
|
70
|
+
### D2 — Brightness is weighted by payout, not by task count
|
|
71
|
+
|
|
72
|
+
The share is `live / credits`, where each descent task is weighted by what it actually
|
|
73
|
+
earned the idea.
|
|
74
|
+
|
|
75
|
+
Counting tasks instead would make deleting a one-line fix dim a star as hard as deleting
|
|
76
|
+
the feature it belongs to. Weighting by payout makes brightness fall in proportion to how
|
|
77
|
+
much of the idea's *worth* died — and since size is drawn from the same quantity, the two
|
|
78
|
+
channels become a share of one number rather than two unrelated measurements sitting side
|
|
79
|
+
by side. It also makes "large and dead" exact: full size, zero brightness.
|
|
80
|
+
|
|
81
|
+
The basis is **paid + estimated**, matching size. A historical idea's descent was never
|
|
82
|
+
paid for (`idea-size-historical.js` prices those hops); counting only banked credits would
|
|
83
|
+
mean removing an estimated hop dims nothing while the size drawn beside it still counts
|
|
84
|
+
that hop. To support this, `priceDescentForIdea` and `ideaSizes` now return a per-task
|
|
85
|
+
`byTask` breakdown that **sums exactly to `credits`** — assembled from the priced descent
|
|
86
|
+
plus any ledger payment for a task the descent no longer reaches, because `idea-size.js`
|
|
87
|
+
deliberately keeps those in `credits` so size cannot fall, and dropping them here would
|
|
88
|
+
make the parts sum to less than the whole and silently overstate brightness.
|
|
89
|
+
|
|
90
|
+
### D3 — An idea that earned nothing is LIT, not dark
|
|
91
|
+
|
|
92
|
+
Zero over zero is not zero. A freshly-filed thought has had nothing removed from it, so
|
|
93
|
+
`share: 1` with `hasWork: false`. Returning 0 would make it pixel-identical to the thought
|
|
94
|
+
whose version was torn out — which is precisely the distinction BV2.R19's proof exists to
|
|
95
|
+
check, and would quietly defeat the reason size and brightness are two channels.
|
|
96
|
+
`hasWork` is what lets the five states (BV2.R17) tell a protostar from a remnant without
|
|
97
|
+
re-deriving it.
|
|
98
|
+
|
|
99
|
+
### D4 — "Could not look" is not "nothing was removed"
|
|
100
|
+
|
|
101
|
+
When the removal facts cannot be read — the lifecycle module is off, the port is
|
|
102
|
+
unregistered — `share` is `null` and `attributable` is `false`, never a confident 1. Same
|
|
103
|
+
posture `idea-size.js` already takes with `historical: null` when the descent walk is
|
|
104
|
+
unavailable: an established absence and an unreadable one are different answers, and only
|
|
105
|
+
one of them may be drawn as a plain full-brightness star.
|
|
106
|
+
|
|
107
|
+
A **confirmed removal that names no target** is the residual case D1 cannot close: it is
|
|
108
|
+
genuinely gone, and brightness does not know which idea to dim. It happens for proposals
|
|
109
|
+
filed before `lifecycle_010`, for attributions where every deletion predates
|
|
110
|
+
`claims.published_head_sha`, and for a derivation outage. Guessing in either direction is
|
|
111
|
+
worse than not dimming — so nothing dims, and `countUnattributedConfirmed()` exists so a
|
|
112
|
+
surface can say *"the sky may read brighter than the truth by up to N confirmed
|
|
113
|
+
removals"* instead of being silently optimistic.
|
|
114
|
+
|
|
115
|
+
### D5 — Brightness lives in `economy`, behind a port
|
|
116
|
+
|
|
117
|
+
It is a share of the same payouts size is built from, so it sits beside size in
|
|
118
|
+
`modules/economy/idea-brightness.js`; putting it in `lifecycle` would mean that module
|
|
119
|
+
doing money arithmetic. That makes the removal facts a cross-module read, so they come
|
|
120
|
+
through a kernel-mediated port — `lifecycle.removals` — and never a `require`
|
|
121
|
+
([ADR 0083](<redacted>.md) rule 1), exactly as economy already
|
|
122
|
+
reaches `ideas.descent`.
|
|
123
|
+
|
|
124
|
+
Economy resolves it with `resolveOptional` and **does not declare it in `consumes`**.
|
|
125
|
+
`consumes` means *required*, and `verifyEnabledPorts` refuses to boot when an enabled
|
|
126
|
+
module consumes an unprovided port — declaring it would turn "lifecycle off" into
|
|
127
|
+
"instance will not start". The same non-declaration, for the same reason, as
|
|
128
|
+
`ideas.descent`.
|
|
129
|
+
|
|
130
|
+
## Consequences
|
|
131
|
+
|
|
132
|
+
- Brightness is computable per idea and falls only when a **person** confirms a removal.
|
|
133
|
+
A dismissed or unanswered proposal dims nothing, which is what stops refactors, renames
|
|
134
|
+
and moves from dimming the sky continuously — ADR 0313's central finding.
|
|
135
|
+
- **No credit is ever clawed back.** `modules/economy/idea-brightness.js` contains no
|
|
136
|
+
writer and is unreachable from any ship path. `shipped` stays terminal, the ledger is
|
|
137
|
+
never rewritten, and size does not move. Only the picture changes.
|
|
138
|
+
- `ideaSizes` grew a field. Callers that spread its result now carry a `Map`; nothing
|
|
139
|
+
serialises it to JSON today, and a surface that wants to must project it rather than
|
|
140
|
+
pass it through.
|
|
141
|
+
- BV2.R17 (the five states) can resolve protostar-vs-remnant from `hasWork` and `share`
|
|
142
|
+
without re-deriving either, and BV2.R18 (the sky) has `removedTasks` to explain *why* a
|
|
143
|
+
star dimmed rather than only that it did.
|
|
144
|
+
|
|
145
|
+
## Rejected
|
|
146
|
+
|
|
147
|
+
- **Parsing the proposal body.** The attribution is right there as
|
|
148
|
+
` - task <id> — <title>`. It is also documentation prose that a copy edit may reword at
|
|
149
|
+
any time, and ADR 0319 already refused this for that reason. A reader that depends on a
|
|
150
|
+
human-facing line shape fails silently and dims nothing.
|
|
151
|
+
- **Re-deriving targets at verdict time.** Spends the capped GitHub budget a second time,
|
|
152
|
+
against a history that has moved, on the interactive path where a person is waiting.
|
|
153
|
+
- **Treating `ship_task_id` as the removed task.** The wrong direction; dims the ship's
|
|
154
|
+
own idea instead of the one whose work died. Named here because the column is right
|
|
155
|
+
there on the verdict row and is the obvious thing to reach for.
|
|
156
|
+
- **Counting tasks rather than payouts.** Simpler, and makes a one-line fix weigh the same
|
|
157
|
+
as a feature.
|
|
158
|
+
- **Clamping the share at both ends.** The bottom is clamped so an anomaly renders as a
|
|
159
|
+
legible "nothing survives" rather than a negative; the top is not, and `live` / `removed`
|
|
160
|
+
are reported unclamped, so an impossible figure stays visible instead of being hidden by
|
|
161
|
+
the thing that claims to prevent it — `idea-size.js`'s argument against clamping size.
|
|
162
|
+
- **Storing the removed task's title beside the row.** It lives on `tasks` and can be
|
|
163
|
+
joined for; a copy goes stale the first time a title is edited.
|
package/docs/adr/README.md
CHANGED
|
@@ -412,3 +412,4 @@ This keeps the decision history honest and traceable.
|
|
|
412
412
|
| 0320 | [**An empty diff is not an ungraded diff: grade the claim instead** ([task 1004011](https://cloudbongos.com/builders#/task/1004011), goal 1000118 — BONGOS-V2; scoped out of task 1003950 shape 2). `ship.js --allow-empty` closes a task that produced no committed artifact, and it used to do two things at once: set `skipGrade`, so the subagent grader never ran, and therefore route the close through `POST /tasks/:id/confirm`, gated on `task.confirm.grade_bypass` — one of [ADR 0157](0157-archon-is-rank-and-identity-only.md)'s four trust-boundary keys, so a Metic could not close an artifact-free task at all. **Both halves were wrong.** The permission was the wrong question: `grade_bypass` means *ratify past a grade that FAILED*, a genuine override of an existing verdict, and a no-artifact close has no verdict to override — charging the same key for "it was judged and rejected" and "there was nothing to judge" conflates an override with an absence. And nothing was being checked, which is the more serious half: the assertions in the handoff notes ("the premise was already fixed", "this was a pure ops verification", "the row was updated directly") went into the ledger unread, on exactly the ships where the record is the ONLY evidence that exists. A permission gate is not scrutiny — an Archon clicking confirm is not someone checking whether the claim is true. **Decision:** on `--allow-empty` the grader is not skipped, it is RE-POINTED — it grades the notes + value summary against the task description, asking whether the task is genuinely artifact-free and whether the evidence supports that. A pass is a real passing grade confirming through the ordinary rank-open path; a fail is answered with better evidence, not a key. Three sub-decisions carry it: the rubric axes are **re-asked, not replaced** (`functional_fidelity` → is this genuinely artifact-free, `code_quality` → is the evidence specific and checkable), because the gate arithmetic and `/grade-audit`, the aggregates and the cost model all read those columns and a third pair of names would mean teaching every one of them a synonym; **two generalist shots, not the four-lens panel** (Hacker/Quality/Efficiency are lenses over code — over three paragraphs of prose they are four readings of the same short text at four times the cost — so the lenses go, recorded in `signals.lenses_dropped` per the #513 trivial-diff precedent; but the panel's UNANIMOUS gate was also the cross-check that makes injection hard, and the first cut kept a single shot and lost it, so the species asks the same question twice and collapses by **unanimity, not median** — under a median the mean of a fooled 10 and a suspicious 3 is a passing 6.5 and the second reading launders the first; a split is recorded as `signals.shot_dissent`; the collapse covers ALL THREE gate axes including `confidence`, which the second grading round caught missing — it is the one axis living in `signals` rather than `rubric_scores`, and the server derives `lowConfidence` purely from the submitted value, so an honest 0.2 "I cannot judge this" averaged with a confident 0.9 would have cleared the 0.4 gate at 0.55); and the species marker lives in **`signals.no_artifact_grade`, not `grader_kind`**, because the server's `validateAndScore` hardcodes `task_grades.grader_kind` to `'subagent'` for everything on that route. **D4: every builder-controlled field is scrubbed and fenced** (`grader-workers/fence.js`, SR-15 / #995) — the panel workers have fenced the diff, title, description and value summary since #995, and this species is the worse case on both sides: the notes are not context around an artifact the model can independently check, they ARE the evidence, and a successful injection does not flip one of four workers, it closes a task past the very scrutiny this ADR added to replace the Archon-only gate. The first implementation shipped it unfenced and the grader caught it as a blocker. The no-follow rule is restated AFTER the artifact too, since the notes may run to 24,000 characters and a rule stated once at the top is a long way behind by the end of an adversarial payload. The prompt renders no changed-files or diff section at all, so the absence cannot read as the finding. No diff hash is stored — hashing the empty diff makes it a reuse key shared by every empty-diff ship of the task, handing the next close a pass this one earned. Unchanged: `--skip-grade` still reaches the Archon gate (the task 1003950 escalation split still chooses the refusal), and below the protected-path floor the server's Guard 1b (task 1002646) still fails a zero-`committed_files` pass closed, correctly — this moves the Metic-and-above case, which is the case that was filed. **Drops task 1004010**, the alternative shape — a new lower-floored permission atom for the same close — by owner decision of 2026-09-20: it answers "who may skip the check" when the better question is "why is there no check", and it permanently widens the permission surface, the same class of change the owner declined three days earlier for `version.create` ([task 1003853](https://cloudbongos.com/builders#/task/1003853), [ADR 0250](0250-strict-versioning-the-version-boundary-is-the-scope-gate.md) §6a). Also rejected: lowering the floor on the existing key (treats the gate as the point; nothing would read the notes); grading the notes with the full panel; a new pair of rubric axes for prose (honest about the measurement, but moves the gate arithmetic and every consumer of it for a naming gain).](0320-an-empty-diff-is-not-an-ungraded-diff.md) | grading / permissions |
|
|
413
413
|
| 0319 | [**A removal verdict is a row beside the question, because both answers close it the same way** ([task 1004095](https://cloudbongos.com/builders#/task/1004095), goal 1000095 — BONGOS-V2). [ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md) settled that a ship deleting an earlier shipped task’s files files a QUESTION and a person answers it; task 1004090 built the question and nothing could answer it. Building the answer surfaced the property that decides the design: a proposal is a question, not work, so CONFIRMING and DISMISSING both leave nothing to build and both end the task at `abandoned` — meaning a status alone records neither, and brightness could not tell the star it must dim from the one it must not. ADR 0313 had already rejected the obvious alternative (a `removed` status; `shipped` is terminal and much of the ledger leans on it — a removal is a fact recorded BESIDE work, not a new terminal state). Decision: `lifecycle_removal_verdicts` (migration `lifecycle_009`), PRIMARY KEY on `proposal_task_id` so one proposal holds one answer ever — a second decide is REFUSED (409), never upserted and never an idempotent 200, because overwriting destroys the record that a person answered differently; the row is locked `FOR UPDATE` first, so simultaneous Confirm and Dismiss cannot both read “undecided”, and `ALREADY_DECIDED` is checked BEFORE `PROPOSAL_CLOSED` because an answered proposal is also abandoned and the useful message is the one naming the earlier verdict. The verdict and the status change commit in ONE transaction. What this does NOT ship is the target list: the affected tasks and paths stay PROSE in the proposal body (the owner’s 2026-09-20 decision, since [ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md) forbids widening the runner to fan out), and nothing here parses it — brightness designs that storage. What brightness does get is structured: the verdict plus `ship_task_id`, denormalised out of the `source_ref` that encodes it as a string nothing can join on. Surface: spec decision 15’s “existing awaiting-nod surface” is the CARD, not the QUEUE — [ADR 0299](0299-the-nod-queue-stays-scoped-to-routed-filings.md) keeps that queue scoped to routed filings and a removal proposal has no idea lineage at all, so it gets its own slice on the same card at the same floor. The PERMISSION is reused (`task.promote`, Metic+, read and write both) and the VERB is not: no existing route means “that work is genuinely gone”, and `/tasks/:id/abandon` would close the question without recording which answer was given. Top-level `/removal-proposals` rather than `/tasks/removal-proposals`, because the manifest mounts `tasks` first and its `/tasks/:id` swallows a later file’s literal segment. Rot is INHERITED: `backlog` is rottable and `abandoned` is not, so an unanswered proposal rots and an answered one leaves the feed with no code written for either — pinned against the rot module’s own exported vocabulary. Rejected: the status as the record; parsing the body at confirm time; widening the nod queue; separate `/confirm` and `/dismiss` routes; upserting a second verdict; a `LIKE ‘prefix%’` match (a rule key may contain an underscore, which is a LIKE wildcard).](0319-a-removal-verdict-is-a-row-beside-the-question.md) | ideas / lifecycle |
|
|
414
414
|
| 0318 | [**Listing a file for task affinity is not claiming to own it** ([task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2). The case [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) named and left open, closed by neither of the two routes it predicted. `builder-settings` derived `protected` — Archon-only to scope a goal to — on exactly one glob, `src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix. **That glob was never an ownership claim:** `MODULE_GLOBS` listed it for task AFFINITY and said in the same comment that the `GET /me` aggregator stays a thin core facade, and `isProtected()` cannot tell the two apart. **Both predicted routes were measured and rejected.** Narrowing the blanket prefix: all eighteen files under `src/bongos/routes/` are protected by that prefix AND NOTHING ELSE, so narrowing unwalls `auth.js`, `builders.js`, `backup.js` and `core-update.js` together — now pinned as a tripwire test rather than left as a caution. Moving `me.js` into `modules/builder-settings/`: the aggregator resolves TEN module ports (`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`, `box`, `art` ×2, `discord`) and `builder-settings` is one of them, so hosting it inside one module means resolving nine foreign ports from the inside — the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 forbids. Decision: **ownership follows what the file is** — `me.js` moves to `kernel`, which already owns `src/bongos/routes/security.js`. This is 0314 one step on: owning the command is not owning the authority, and **being listed next to a module is not owning it either**. **Rejected: a general affinity tier** in `MODULE_GLOBS` mirroring `isCommandPath` — the audit does not support it. Three other modules (`kernel`, `devsecops`, `methodology`) are also protected only by paths outside their own directory, but in each the foreign path genuinely IS the domain; `builder-settings` was the only module protected by a path its own source called core, so a new tier would be a new place for a hole to hide, built for a population of one. `protectedModules()` goes 9 -> 8. **The wall that matters does not move:** a sub-Metic builder touching `/me` is still refused by `checkPermissionPaths`, asserted directly rather than assumed. The sixteen still-unowned route files remain [task 1004116](https://cloudbongos.com/builders#/task/1004116), whose option (a) this de-risks.](0318-listing-a-file-for-task-affinity-is-not-owning-it.md) | permissions / module scope |
|
|
415
|
+
| 0321 | [**Brightness is a payout-weighted share, and a removal's targets are rows** ([task 1004099](https://cloudbongos.com/builders#/task/1004099), goal 1000095 — BONGOS-V2). Size is an idea's total ideator payout and never falls; brightness is how much of that work is still alive, and the pair is what lets a thought whose version was torn out draw LARGE AND DARK rather than merging with one that never mattered. Two things had to be settled. **The removal facts were stored in the wrong direction**: [ADR 0319](0319-a-removal-verdict-is-a-row-beside-the-question.md) gave a confirmed removal a row carrying the verdict and `ship_task_id` — the task whose merge DELETED the files — and deliberately left out the tasks whose work was removed, saying brightness would design its own storage when it needed one. Those are different rows under different ideas, so reading one as the other dims the wrong star while looking like a working query. `<redacted>` stores `(proposal_task_id, removed_task_id, paths)` at PROPOSAL time, from the SAME derivation objects the proposal body renders as prose — one derivation, two renderings, so they cannot disagree and `removal-proposals.js` keeps its refusal to parse a body back. Proposal time and not verdict time because the attribution comes from capped, rate-limited GitHub commit-history reads that only exist while the cascade runs. NOT a runner fan-out ([ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md)): the rule still files one task; this records a side fact about it. `ON CONFLICT DO NOTHING`, never `DO UPDATE`, since a degraded second walk finds FEWER paths and must not erase a good first read — and the writer runs on the cascade's SKIPPED row too, so a re-ship repairs what a derivation outage missed. **The share is weighted by PAYOUT, not by task count**, or deleting a one-line fix dims a star as hard as deleting the feature; weighting by what each hop earned also makes 'large and dead' exact, and makes the two channels a share of ONE number rather than two unrelated measurements. Basis is paid + estimated to match size, which required `priceDescentForIdea`/`ideaSizes` to return a per-task `byTask` summing EXACTLY to `credits` — including ledger payments for tasks the descent no longer reaches, which `idea-size.js` keeps in `credits` so size cannot fall and which would otherwise make the parts sum to less than the whole. Three edges, each silent: an idea that earned nothing is `share: 1, hasWork: false`, because zero over zero is not zero and a protostar is not a remnant (the BV2.R19 distinction); an unreadable removal port gives `share: null, attributable: false`, because 'could not look' is not 'nothing was removed'; and a CONFIRMED removal naming no target dims nothing at all — brightness cannot know which idea — so `countUnattributedConfirmed()` exists to say the gap out loud instead of quietly brightening the sky. Brightness lives in `economy` beside size and reaches lifecycle through the new `lifecycle.removals` port, resolved OPTIONALLY and NOT declared in `consumes` (declaring it would turn 'lifecycle off' into 'instance will not start' — the `ideas.descent` precedent). **No credit is clawed back and nothing here writes**: ADR 0313 stands, `shipped` is terminal, only the picture changes. Rejected: parsing the proposal body (ADR 0319 already refused it — a human-facing line shape fails silently when reworded); re-deriving at verdict time; treating `ship_task_id` as the removed task; counting tasks rather than payouts; clamping the share at the top, which would hide the anomaly it claims to prevent.](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md) | ideas / economy |
|
|
@@ -2316,5 +2316,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2316
2316
|
landed since 1.19.919 with no explicit bump. run 35531720944. (task 1002620)
|
|
2317
2317
|
1.19.921 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2318
2318
|
landed since 1.19.920 with no explicit bump. run 35534076467. (task 1002620)
|
|
2319
|
+
1.19.922 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2320
|
+
landed since 1.19.921 with no explicit bump. run 35536293721. (task 1002620)
|
|
2319
2321
|
---------------------------------------------------------------------------
|
|
2320
2322
|
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/economy/idea-brightness.js — BRIGHTNESS: the live share of an idea's
|
|
4
|
+
// descent (task 1004099 / BV2.R16, goal 1000095; spec
|
|
5
|
+
// docs/specs/bongos-v2-ideator-surface.md decisions 4 and 20; ADR 0321, under
|
|
6
|
+
// ADR 0313 + 0319).
|
|
7
|
+
//
|
|
8
|
+
// WHAT BRIGHTNESS IS. Size is an idea's total ideator payout from its descent and
|
|
9
|
+
// NEVER falls (./idea-size.js). Brightness is how much of that payout is still
|
|
10
|
+
// alive: the same sum, restricted to the descent tasks the project has not since
|
|
11
|
+
// confirmed as removed, over the whole. Two channels, two facts — and the pair is
|
|
12
|
+
// the entire point. A thought that caused a version the project later tore out
|
|
13
|
+
// draws LARGE AND DARK, which is visibly different from a thought that never
|
|
14
|
+
// caused anything and draws small and dark. One number could not say that.
|
|
15
|
+
//
|
|
16
|
+
// GRADED, NOT BINARY (spec decision 20). A descent of five things with three
|
|
17
|
+
// still alive is partly lit. It goes out only when nothing survives. So this is a
|
|
18
|
+
// share in [0, 1] and not a flag — an idea dims progressively as pieces of its
|
|
19
|
+
// work are removed.
|
|
20
|
+
//
|
|
21
|
+
// WEIGHTED BY PAYOUT, NOT BY COUNT, and that is the decision in this file.
|
|
22
|
+
// Counting tasks would make removing a one-line fix dim a star as much as
|
|
23
|
+
// removing the feature it belongs to. Weighting by what each hop actually earned
|
|
24
|
+
// means brightness falls in proportion to how much of the idea's WORTH died,
|
|
25
|
+
// which is the same quantity size is drawn from — so the two channels are a
|
|
26
|
+
// share of one number rather than two unrelated measurements that happen to sit
|
|
27
|
+
// side by side. It also makes "large and dead" exact: full size, zero brightness.
|
|
28
|
+
//
|
|
29
|
+
// THE BASIS IS PAID + ESTIMATED, the same as size. A historical idea's descent
|
|
30
|
+
// was never paid for (./idea-size-historical.js prices those hops), and if
|
|
31
|
+
// brightness counted only banked credits, removing an estimated hop would dim
|
|
32
|
+
// nothing while the size drawn beside it still counted that hop. The share would
|
|
33
|
+
// then be a fraction of a different number than the one on screen.
|
|
34
|
+
//
|
|
35
|
+
// BRIGHTNESS NEVER CLAWS BACK CREDITS, and nothing here could. This module only
|
|
36
|
+
// READS — no writer, no ledger row, no status change. ADR 0313 settles that
|
|
37
|
+
// `shipped` is terminal and the ledger is never rewritten when the project
|
|
38
|
+
// changes its mind: only the picture changes. Size is computed from credit_log
|
|
39
|
+
// and is untouched by anything in this file.
|
|
40
|
+
//
|
|
41
|
+
// WHAT COUNTS AS REMOVED IS A PERSON'S ANSWER, never a derived one. A ship that
|
|
42
|
+
// deletes an earlier shipped task's files files a QUESTION (the
|
|
43
|
+
// `removal-proposed-on-ship` cascade), someone confirms or dismisses it, and only
|
|
44
|
+
// 'confirmed' reaches this read. Refactors, renames and file moves all look like
|
|
45
|
+
// deletions to a diff, so a derived-only signal would dim the sky continuously —
|
|
46
|
+
// ADR 0313's central finding, and the reason `dismissed` exists at all.
|
|
47
|
+
//
|
|
48
|
+
// MODULE BOUNDARY: the removal facts live in the LIFECYCLE module and are reached
|
|
49
|
+
// through the kernel-mediated `lifecycle.removals` port, never a cross-module
|
|
50
|
+
// require (ADR 0083 rule 1) — the same way ./idea-size.js reaches `ideas.descent`.
|
|
51
|
+
|
|
52
|
+
const api = require('../../src/module-api');
|
|
53
|
+
const { resolveOptional } = api;
|
|
54
|
+
const { ideaSizes } = require('./idea-size');
|
|
55
|
+
|
|
56
|
+
// A positive integer id as it arrives off a row or a caller, where it may be a
|
|
57
|
+
// bigint serialised to a string. Never coerced with Number(); the guard
|
|
58
|
+
// ./idea-size.js uses, for the same precision reason.
|
|
59
|
+
function idOf(v) {
|
|
60
|
+
return /^[1-9]\d*$/.test(String(v)) ? String(v) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ideaBrightnesses — brightness for MANY ideas, keyed by idea id (string).
|
|
64
|
+
//
|
|
65
|
+
// Returns Map<ideaIdString, {
|
|
66
|
+
// share number in [0,1] — the live fraction of the descent's payout,
|
|
67
|
+
// or null when it could not be established (see below)
|
|
68
|
+
// live credits still alive
|
|
69
|
+
// removed credits whose work is confirmed gone
|
|
70
|
+
// credits the total the two divide (equals size's `credits`)
|
|
71
|
+
// removedTasks [{ taskId, credits, proposalTaskId, paths }] — WHAT went out,
|
|
72
|
+
// so a surface can say why a star dimmed instead of only that it
|
|
73
|
+
// did
|
|
74
|
+
// hasWork false when the idea has earned nothing at all
|
|
75
|
+
// attributable false when the removal facts could not be read
|
|
76
|
+
// }>
|
|
77
|
+
//
|
|
78
|
+
// EVERY ID ASKED FOR IS PRESENT. Absent and dark are different answers, and an
|
|
79
|
+
// idea that caused nothing yet is a real state the sky has to draw.
|
|
80
|
+
//
|
|
81
|
+
// AN IDEA THAT EARNED NOTHING IS `share: 1`, `hasWork: false` — LIT, NOT DARK,
|
|
82
|
+
// and this is the sharpest edge in the file. Zero over zero is not zero. A
|
|
83
|
+
// freshly-filed thought has had nothing removed from it; it is a protostar, not a
|
|
84
|
+
// remnant. Returning 0 would light-for-light merge it with the thought whose
|
|
85
|
+
// version was torn out, which is exactly the distinction BV2.R19's proof exists
|
|
86
|
+
// to check. `hasWork` is what lets the five states (BV2.R17) tell the two apart
|
|
87
|
+
// without re-deriving it.
|
|
88
|
+
//
|
|
89
|
+
// WHEN THE REMOVAL FACTS CANNOT BE READ — the lifecycle module is off, or the
|
|
90
|
+
// port is unregistered — `share` is NULL and `attributable` is false, rather than
|
|
91
|
+
// 1. "Nothing was removed" and "we could not look" are different answers and only
|
|
92
|
+
// one of them may be drawn as a plain full-brightness star. Same posture
|
|
93
|
+
// ./idea-size.js takes with `historical: null` when the descent walk is
|
|
94
|
+
// unavailable.
|
|
95
|
+
//
|
|
96
|
+
// ideaIds — array of ids (bigint-safe strings or numbers)
|
|
97
|
+
// client — optional tx client; falls back to the pool inside the reads
|
|
98
|
+
// descentByIdea — the caller's already-walked descent, passed straight through
|
|
99
|
+
// to ideaSizes so a page that has one costs no extra walk
|
|
100
|
+
// sizes — an already-computed ideaSizes() result, when the caller has
|
|
101
|
+
// one. Brightness needs the same per-task figures size does,
|
|
102
|
+
// so a surface drawing both should compute sizes once and hand
|
|
103
|
+
// them here rather than paying for the whole read twice.
|
|
104
|
+
async function ideaBrightnesses(ideaIds, { client = null, descentByIdea = null, sizes = null } = {}) {
|
|
105
|
+
const ids = [...new Set((Array.isArray(ideaIds) ? ideaIds : []).map(idOf).filter(Boolean))];
|
|
106
|
+
const out = new Map();
|
|
107
|
+
if (ids.length === 0) return out;
|
|
108
|
+
|
|
109
|
+
const sized = sizes || await ideaSizes(ids, { client, descentByIdea });
|
|
110
|
+
|
|
111
|
+
// The per-task breakdown. `ideaSizes` carries the totals; `byTask` is what
|
|
112
|
+
// lets a share be taken of them, and it is absent only when the descent could
|
|
113
|
+
// not be walked at all — in which case there is nothing to attribute against
|
|
114
|
+
// and the idea reads unattributable below.
|
|
115
|
+
const perTask = new Map();
|
|
116
|
+
for (const id of ids) {
|
|
117
|
+
const s = sized.get(id);
|
|
118
|
+
perTask.set(id, (s && s.byTask instanceof Map) ? s.byTask : null);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Every descent task across every idea asked for, in ONE port call. A lookup
|
|
122
|
+
// per idea would be a round trip per star on a page of them.
|
|
123
|
+
const allTaskIds = new Set();
|
|
124
|
+
for (const byTask of perTask.values()) {
|
|
125
|
+
if (!byTask) continue;
|
|
126
|
+
for (const taskId of byTask.keys()) allTaskIds.add(taskId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const port = resolveOptional && resolveOptional('lifecycle.removals');
|
|
130
|
+
let confirmed = null;
|
|
131
|
+
if (port && typeof port.confirmedRemovalsForTasks === 'function' && allTaskIds.size > 0) {
|
|
132
|
+
confirmed = await port.confirmedRemovalsForTasks([...allTaskIds], { client });
|
|
133
|
+
} else if (port && typeof port.confirmedRemovalsForTasks === 'function') {
|
|
134
|
+
// The port is there and there is simply nothing to ask about. That is an
|
|
135
|
+
// established answer — no removals — not an unreadable one.
|
|
136
|
+
confirmed = new Map();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const id of ids) {
|
|
140
|
+
const s = sized.get(id) || { credits: 0 };
|
|
141
|
+
const byTask = perTask.get(id);
|
|
142
|
+
const credits = Number(s.credits) || 0;
|
|
143
|
+
|
|
144
|
+
if (!confirmed || !byTask) {
|
|
145
|
+
out.set(id, {
|
|
146
|
+
share: null, live: null, removed: null, credits,
|
|
147
|
+
removedTasks: [], hasWork: credits > 0, attributable: false,
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let removed = 0;
|
|
153
|
+
const removedTasks = [];
|
|
154
|
+
for (const [taskId, taskCredits] of byTask) {
|
|
155
|
+
const hit = confirmed.get(taskId);
|
|
156
|
+
if (!hit) continue;
|
|
157
|
+
const c = Number(taskCredits) || 0;
|
|
158
|
+
removed += c;
|
|
159
|
+
removedTasks.push({
|
|
160
|
+
taskId,
|
|
161
|
+
credits: c,
|
|
162
|
+
proposalTaskId: hit.proposalTaskId,
|
|
163
|
+
paths: hit.paths,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The live half comes off the TOTAL minus what died, not off a second sum
|
|
168
|
+
// over the surviving tasks. The two differ: `credits` includes payouts for
|
|
169
|
+
// tasks the descent no longer reaches (a re-parented task, a deleted edge),
|
|
170
|
+
// which ./idea-size.js keeps on purpose so size cannot fall. Summing the
|
|
171
|
+
// survivors would quietly drop those and make a re-parenting dim a star.
|
|
172
|
+
const live = credits - removed;
|
|
173
|
+
// Clamped at the bottom only, and never at the top. `removed` is a subset of
|
|
174
|
+
// the same figures `credits` was built from so it cannot exceed it — but if
|
|
175
|
+
// it ever did, a negative brightness is a rendering bug where a zero is a
|
|
176
|
+
// legible "nothing survives". The anomaly is still visible in `live` and
|
|
177
|
+
// `removed`, which are reported unclamped.
|
|
178
|
+
const share = credits > 0 ? Math.max(0, live) / credits : 1;
|
|
179
|
+
|
|
180
|
+
out.set(id, {
|
|
181
|
+
share,
|
|
182
|
+
live,
|
|
183
|
+
removed,
|
|
184
|
+
credits,
|
|
185
|
+
// Largest loss first: a surface explaining why a star dimmed wants the
|
|
186
|
+
// piece that mattered, not the first one the descent happened to yield.
|
|
187
|
+
removedTasks: removedTasks.sort((a, b) => b.credits - a.credits),
|
|
188
|
+
hasWork: credits > 0,
|
|
189
|
+
attributable: true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ideaBrightness — the same answer for one idea. A thin wrapper on purpose: two
|
|
196
|
+
// implementations of "how lit is this thought" is two answers, and this is the
|
|
197
|
+
// shape a single-idea panel wants. Mirrors ./idea-size.js's ideaSize.
|
|
198
|
+
async function ideaBrightness(ideaId, opts = {}) {
|
|
199
|
+
const key = idOf(ideaId);
|
|
200
|
+
const unknown = {
|
|
201
|
+
share: null, live: null, removed: null, credits: 0,
|
|
202
|
+
removedTasks: [], hasWork: false, attributable: false,
|
|
203
|
+
};
|
|
204
|
+
if (!key) return unknown;
|
|
205
|
+
const all = await ideaBrightnesses([key], opts);
|
|
206
|
+
return all.get(key) || unknown;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
ideaBrightness,
|
|
211
|
+
ideaBrightnesses,
|
|
212
|
+
};
|