@bongos/core 1.19.921 → 1.19.923
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 +64 -24
- package/docs/adr/0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md +163 -0
- package/docs/adr/0322-a-remnant-needs-evidence-and-merged-wins.md +110 -0
- package/docs/adr/README.md +2 -0
- package/docs/module-api-changelog.md +4 -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/economy/reward.js +11 -0
- package/modules/ideas/idea-state.js +254 -0
- package/modules/ideas/routes/inbox.js +8 -0
- package/modules/ideas/trail.js +13 -1
- 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/tests/idea_state.mjs +399 -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.923",
|
|
6
|
+
"core_contract": "1.19.923",
|
|
7
|
+
"source_commit": "f854693589aaf4e6736cbe02c05c3fd5eb2f2ca1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-20T22:19:24.521Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 511,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2311,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2848,
|
|
20
|
+
"tree_sha256": "c600fca8af8dd65a3567fe7851b2ead8bc6c08f43dcb3b1b4964957c5c734e1a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2094,10 +2094,20 @@
|
|
|
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
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
"path": "docs/adr/0322-a-remnant-needs-evidence-and-merged-wins.md",
|
|
2104
|
+
"mode": "0000644",
|
|
2105
|
+
"sha256": "6a4ad58720701e760068d5ce1761733790beef0a142aa31a08782d029cc7a390"
|
|
2106
|
+
},
|
|
2097
2107
|
{
|
|
2098
2108
|
"path": "docs/adr/README.md",
|
|
2099
2109
|
"mode": "0000644",
|
|
2100
|
-
"sha256": "
|
|
2110
|
+
"sha256": "7c66ecbd1940f3cdd830f0fb4acf65c41178807a431bb8fab382e4b8a0dbc8ae"
|
|
2101
2111
|
},
|
|
2102
2112
|
{
|
|
2103
2113
|
"path": "docs/api-reference.md",
|
|
@@ -2667,7 +2677,7 @@
|
|
|
2667
2677
|
{
|
|
2668
2678
|
"path": "docs/module-api-changelog.md",
|
|
2669
2679
|
"mode": "0000644",
|
|
2670
|
-
"sha256": "
|
|
2680
|
+
"sha256": "2dc422992b2c8e66db2eb3e7492abb35d061caf0ea2e3cc1ad492d0b4b8083b4"
|
|
2671
2681
|
},
|
|
2672
2682
|
{
|
|
2673
2683
|
"path": "docs/modules-contract.md",
|
|
@@ -4229,15 +4239,20 @@
|
|
|
4229
4239
|
"mode": "0000644",
|
|
4230
4240
|
"sha256": "736eed2c89863d6eb28072627a40ac4f8945832cfb7b1d1aba10e6cc65483901"
|
|
4231
4241
|
},
|
|
4242
|
+
{
|
|
4243
|
+
"path": "modules/economy/idea-brightness.js",
|
|
4244
|
+
"mode": "0000644",
|
|
4245
|
+
"sha256": "745da28c27ebcd50b349c3fe027f7e3f14888f9e012e0c9443656de249444ba1"
|
|
4246
|
+
},
|
|
4232
4247
|
{
|
|
4233
4248
|
"path": "modules/economy/idea-size-historical.js",
|
|
4234
4249
|
"mode": "0000644",
|
|
4235
|
-
"sha256": "
|
|
4250
|
+
"sha256": "215eefac80aaf3624a8ff5d5284f29388a2e1b492424dd152b3d9bbe074e3a01"
|
|
4236
4251
|
},
|
|
4237
4252
|
{
|
|
4238
4253
|
"path": "modules/economy/idea-size.js",
|
|
4239
4254
|
"mode": "0000644",
|
|
4240
|
-
"sha256": "
|
|
4255
|
+
"sha256": "8818bea53d2ce90fe0bc98b53bfed1af11c4eeb541942b96c4cf50a162791634"
|
|
4241
4256
|
},
|
|
4242
4257
|
{
|
|
4243
4258
|
"path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
|
|
@@ -4262,7 +4277,7 @@
|
|
|
4262
4277
|
{
|
|
4263
4278
|
"path": "modules/economy/reward.js",
|
|
4264
4279
|
"mode": "0000644",
|
|
4265
|
-
"sha256": "
|
|
4280
|
+
"sha256": "79f225665dfc84fce1c50d01a25bbf81986c91047732d04113f24a68f766d29a"
|
|
4266
4281
|
},
|
|
4267
4282
|
{
|
|
4268
4283
|
"path": "modules/economy/routes/cost.js",
|
|
@@ -5389,6 +5404,11 @@
|
|
|
5389
5404
|
"mode": "0000644",
|
|
5390
5405
|
"sha256": "363598b5905c850967b407f20110a1d9e9d5b1090a958700cb6a4ed5cea70d80"
|
|
5391
5406
|
},
|
|
5407
|
+
{
|
|
5408
|
+
"path": "modules/ideas/idea-state.js",
|
|
5409
|
+
"mode": "0000644",
|
|
5410
|
+
"sha256": "e516f7951b91234c1f9dc6e327dc7365b6f6c9aad69bb3a85835061e2d072253"
|
|
5411
|
+
},
|
|
5392
5412
|
{
|
|
5393
5413
|
"path": "modules/ideas/inbox.js",
|
|
5394
5414
|
"mode": "0000644",
|
|
@@ -5467,7 +5487,7 @@
|
|
|
5467
5487
|
{
|
|
5468
5488
|
"path": "modules/ideas/routes/inbox.js",
|
|
5469
5489
|
"mode": "0000644",
|
|
5470
|
-
"sha256": "
|
|
5490
|
+
"sha256": "c302e5cd180ce9a6253ab30fad24b970d06f33f0916ac5d922323e536da7b213"
|
|
5471
5491
|
},
|
|
5472
5492
|
{
|
|
5473
5493
|
"path": "modules/ideas/routing.js",
|
|
@@ -5497,7 +5517,7 @@
|
|
|
5497
5517
|
{
|
|
5498
5518
|
"path": "modules/ideas/trail.js",
|
|
5499
5519
|
"mode": "0000644",
|
|
5500
|
-
"sha256": "
|
|
5520
|
+
"sha256": "5ea6bad2296406f56bcf3a00f040ce78a789f24fc0d7d5b933e1526cbbb97cd2"
|
|
5501
5521
|
},
|
|
5502
5522
|
{
|
|
5503
5523
|
"path": "modules/lifecycle/CLAUDE.md",
|
|
@@ -5522,7 +5542,7 @@
|
|
|
5522
5542
|
{
|
|
5523
5543
|
"path": "modules/lifecycle/cascade.js",
|
|
5524
5544
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5545
|
+
"sha256": "176f7463d6f3c7bf2297760d0934f88528ee180dfb0c9e49a4347a449af2820d"
|
|
5526
5546
|
},
|
|
5527
5547
|
{
|
|
5528
5548
|
"path": "modules/lifecycle/check-rollup.js",
|
|
@@ -5764,6 +5784,11 @@
|
|
|
5764
5784
|
"mode": "0000644",
|
|
5765
5785
|
"sha256": "e98efbb1537f0d99adb6b6b66c1fdccfd4bbac16b97362d479af903eb9de492c"
|
|
5766
5786
|
},
|
|
5787
|
+
{
|
|
5788
|
+
"path": "modules/lifecycle/migrations/lifecycle_010_removal_targets.sql",
|
|
5789
|
+
"mode": "0000644",
|
|
5790
|
+
"sha256": "e3e29e791eeb94ab1380ae2e2a13eafaea413faf0d0324a4e2007ab2b3002fca"
|
|
5791
|
+
},
|
|
5767
5792
|
{
|
|
5768
5793
|
"path": "modules/lifecycle/mingle.js",
|
|
5769
5794
|
"mode": "0000644",
|
|
@@ -5772,7 +5797,7 @@
|
|
|
5772
5797
|
{
|
|
5773
5798
|
"path": "modules/lifecycle/module.json",
|
|
5774
5799
|
"mode": "0000644",
|
|
5775
|
-
"sha256": "
|
|
5800
|
+
"sha256": "36a03e5d8a4fc1129439356ba6daaf284d2cd2ca3d98d35f990e3215ff11198a"
|
|
5776
5801
|
},
|
|
5777
5802
|
{
|
|
5778
5803
|
"path": "modules/lifecycle/pg-constraint-fail.js",
|
|
@@ -5804,6 +5829,11 @@
|
|
|
5804
5829
|
"mode": "0000644",
|
|
5805
5830
|
"sha256": "8270ce55255998a4f0bbf574a54df0ba2597013c727b4afb1ce2abad9c9d4d2a"
|
|
5806
5831
|
},
|
|
5832
|
+
{
|
|
5833
|
+
"path": "modules/lifecycle/removal-targets.js",
|
|
5834
|
+
"mode": "0000644",
|
|
5835
|
+
"sha256": "e80304e7b43fe86852763e2f859bc05c1b88bb50a3eea407342ae6d46b8f9eda"
|
|
5836
|
+
},
|
|
5807
5837
|
{
|
|
5808
5838
|
"path": "modules/lifecycle/rot.js",
|
|
5809
5839
|
"mode": "0000644",
|
|
@@ -5872,7 +5902,7 @@
|
|
|
5872
5902
|
{
|
|
5873
5903
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
5874
5904
|
"mode": "0000644",
|
|
5875
|
-
"sha256": "
|
|
5905
|
+
"sha256": "330e42fad79812a05a90256fe31931df2156093b80a648d7c838ecac136c91e4"
|
|
5876
5906
|
},
|
|
5877
5907
|
{
|
|
5878
5908
|
"path": "modules/lifecycle/routes/mingle.js",
|
|
@@ -5892,7 +5922,7 @@
|
|
|
5892
5922
|
{
|
|
5893
5923
|
"path": "modules/lifecycle/routes/removal-proposals.js",
|
|
5894
5924
|
"mode": "0000644",
|
|
5895
|
-
"sha256": "
|
|
5925
|
+
"sha256": "e0a0e14a8818c011334e8f61a50c8dd7ad11b4387acfd6ab9e18e570f54ec6c4"
|
|
5896
5926
|
},
|
|
5897
5927
|
{
|
|
5898
5928
|
"path": "modules/lifecycle/routes/rot.js",
|
|
@@ -7882,12 +7912,12 @@
|
|
|
7882
7912
|
{
|
|
7883
7913
|
"path": "package-lock.json",
|
|
7884
7914
|
"mode": "0000644",
|
|
7885
|
-
"sha256": "
|
|
7915
|
+
"sha256": "baab771a63f97afcbcfa72e466c8e3bcdb220724bf1a0fd2046e078c88c31cc4"
|
|
7886
7916
|
},
|
|
7887
7917
|
{
|
|
7888
7918
|
"path": "package.json",
|
|
7889
7919
|
"mode": "0000644",
|
|
7890
|
-
"sha256": "
|
|
7920
|
+
"sha256": "ff37869714eeb7c99b2120171573cee5a434667baa4eb7bc76a87755f4a67214"
|
|
7891
7921
|
},
|
|
7892
7922
|
{
|
|
7893
7923
|
"path": "public-docs/index.html",
|
|
@@ -8832,7 +8862,7 @@
|
|
|
8832
8862
|
{
|
|
8833
8863
|
"path": "scripts/gds/provision-core-upgrade.js",
|
|
8834
8864
|
"mode": "0000644",
|
|
8835
|
-
"sha256": "
|
|
8865
|
+
"sha256": "1fd6e38184bb0943fdc22e314b9e6cad0b5ebeae7f7370992adbd3b4f2ad5a9e"
|
|
8836
8866
|
},
|
|
8837
8867
|
{
|
|
8838
8868
|
"path": "scripts/gds/provision-disconnect.js",
|
|
@@ -9887,7 +9917,7 @@
|
|
|
9887
9917
|
{
|
|
9888
9918
|
"path": "src/module-api.js",
|
|
9889
9919
|
"mode": "0000644",
|
|
9890
|
-
"sha256": "
|
|
9920
|
+
"sha256": "7c96d3ff9b319a6e4b6e640b01d8e8d3d61a341efb292359c35c495a938eacd5"
|
|
9891
9921
|
},
|
|
9892
9922
|
{
|
|
9893
9923
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10802,7 +10832,7 @@
|
|
|
10802
10832
|
{
|
|
10803
10833
|
"path": "tests/core_upgrade_runner.mjs",
|
|
10804
10834
|
"mode": "0000644",
|
|
10805
|
-
"sha256": "
|
|
10835
|
+
"sha256": "5d0b4fa1ec48d40ebc801902b5279475533ae2dc8819e587bb95bc17783cba06"
|
|
10806
10836
|
},
|
|
10807
10837
|
{
|
|
10808
10838
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
@@ -11894,6 +11924,11 @@
|
|
|
11894
11924
|
"mode": "0000644",
|
|
11895
11925
|
"sha256": "0d8f07c64f4d17c8c894b2d6395281882efd4e186c5714fe9aab61e2808d4314"
|
|
11896
11926
|
},
|
|
11927
|
+
{
|
|
11928
|
+
"path": "tests/idea_brightness.mjs",
|
|
11929
|
+
"mode": "0000644",
|
|
11930
|
+
"sha256": "d67d60b1236ccedd01591356f5a5a9a72e4f1efd895ddf5af00c744219a2b3b6"
|
|
11931
|
+
},
|
|
11897
11932
|
{
|
|
11898
11933
|
"path": "tests/idea_capture_grades.mjs",
|
|
11899
11934
|
"mode": "0000644",
|
|
@@ -12009,6 +12044,11 @@
|
|
|
12009
12044
|
"mode": "0000644",
|
|
12010
12045
|
"sha256": "8bfc36537ffccae702e74e8763534fa535486a2eb8bbf9ee546a2c7368d1dc6b"
|
|
12011
12046
|
},
|
|
12047
|
+
{
|
|
12048
|
+
"path": "tests/idea_state.mjs",
|
|
12049
|
+
"mode": "0000644",
|
|
12050
|
+
"sha256": "f71f7007d72689e865b1945feac66012398b38557affdc776d0a6abdc2140e65"
|
|
12051
|
+
},
|
|
12012
12052
|
{
|
|
12013
12053
|
"path": "tests/idea_task_descent.mjs",
|
|
12014
12054
|
"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.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ADR 0322 — The five states are derived once, and a remnant needs evidence
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-09-20
|
|
5
|
+
- **Task:** [task 1004103](https://cloudbongos.com/builders#/task/1004103) (BV2.R17, goal 1000095 — BONGOS-V2)
|
|
6
|
+
- **Spec:** `docs/specs/<redacted>.md` decisions 4, 19 and 20; criterion `ideator-sky-tells-the-truth`
|
|
7
|
+
- **Design of record:** `docs/design/ideator-surface-direction.md` §"The five objects"
|
|
8
|
+
- **Under:** [ADR 0321](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md), [ADR 0313](0313-a-removal-is-proposed-by-the-ship-and-confirmed-by-a-person.md), [ADR 0312](0312-provenance-is-a-second-ideas-owned-edge-table.md)
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
The ideator surface draws a thought as one of five objects: **star** (it caused work that
|
|
13
|
+
is still alive), **protostar** (filed, not yet ignited), **satellite** (folded into another
|
|
14
|
+
thought), **remnant** (it burned, then the project removed it) and **cold body** (declined
|
|
15
|
+
at triage, so it never ignited and has no size at all). Dim and dark are different states
|
|
16
|
+
and must be different objects, not one dot at two opacities.
|
|
17
|
+
|
|
18
|
+
Three surfaces are about to draw them — the room (BV2.R03), the descent (R10) and the sky
|
|
19
|
+
(R20) — and the spec's own phrasing is that every state must be *computed from the ledger,
|
|
20
|
+
and no surface has to infer one*.
|
|
21
|
+
|
|
22
|
+
The facts already exist. `idea_inbox.status` carries the triage verdict (`open`,
|
|
23
|
+
`promoted`, `discarded`, `merged`; `core_195`). ADR 0321 gave economy `ideaBrightnesses()`,
|
|
24
|
+
returning `hasWork`, `share` and `attributable` per idea. `tasks.goal_id` and `tasks.status`
|
|
25
|
+
say where a thought's work sits and whether it awaits a nod. Nothing was missing; what was
|
|
26
|
+
missing was one place that reads them.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
**One resolver, `modules/ideas/idea-state.js`, and no surface decides a state.** A state
|
|
31
|
+
inferred in three renderers is three states: the sky can say a thought went out while the
|
|
32
|
+
room beside it still calls it alive, with nothing erroring and no way to tell which is
|
|
33
|
+
right. The resolver is pure — facts in, object out — the way `routing.js` is pure about a
|
|
34
|
+
landing. `attachStateToTrail` is the one async wrapper, composed on the trail route after
|
|
35
|
+
`attachSizeToTrail`.
|
|
36
|
+
|
|
37
|
+
**Brightness goes on the `reward` port.** `ideas` may not reach `credit_log` any other way
|
|
38
|
+
(ADR 0083 rule 1), and it already declares `reward` in `consumes`, so no new dependency
|
|
39
|
+
edge. Shipping size without brightness had left the module able to draw how big a thought
|
|
40
|
+
was and unable to say whether it was still alive.
|
|
41
|
+
|
|
42
|
+
**The order of the tests is the decision**, and each step could have gone the other way:
|
|
43
|
+
|
|
44
|
+
1. **Merged wins over everything.** A folded thought is a satellite even when it earned
|
|
45
|
+
money of its own — and it *keeps* that money; `trail-size.js` is emphatic that stream 1
|
|
46
|
+
books against the idea whose lineage row named the shipping task. This changes what the
|
|
47
|
+
thought **is**, not what it earned. Letting work win here draws a satellite as an
|
|
48
|
+
independent star beside the thought that absorbed it: one idea, twice in the sky.
|
|
49
|
+
2. **Work beats a discard.** The design defines a cold body as one that *"never ignited, so
|
|
50
|
+
it has no size at all"* — a stated property, and testing work before status is what
|
|
51
|
+
makes it true by construction rather than by hope. It is reachable: triage can discard a
|
|
52
|
+
duplicate whose twin already routed and paid, and `updateIdea` never touches the ledger.
|
|
53
|
+
3. **A remnant needs evidence, never the absence of an answer.** `share === 0` means the
|
|
54
|
+
project confirmed every piece of the descent gone. `share === null` means the removal
|
|
55
|
+
facts could not be read at all — ADR 0321 is explicit that those are different answers.
|
|
56
|
+
An idea with work and no readable facts is a **star** carrying `attributable: false`, so
|
|
57
|
+
a surface says "unknown" rather than "full". Reading a falsy share as death turns one
|
|
58
|
+
unmounted module into a sky of hollow rings.
|
|
59
|
+
4. **A protostar is the absence of work, not the presence of a status.** Ignition is being
|
|
60
|
+
*paid* for something, not being converted into a row, so a promoted-but-unshipped idea is
|
|
61
|
+
still unignited — which is exactly the state the nod queue exists for.
|
|
62
|
+
|
|
63
|
+
**Position is a second axis**, because the design says the object alone cannot carry it: a
|
|
64
|
+
protostar **in a well** has a home and awaits a nod; one in **open space** has nothing
|
|
65
|
+
holding it. The resolver returns `position`, `wellGoalId` and `awaitingNod` for every state
|
|
66
|
+
(a star has a position too), and the nod is asked of *every* task rather than only the one
|
|
67
|
+
that named the well — an idea that became two tasks, one promoted and one still at
|
|
68
|
+
`backlog`, still has something waiting on a person.
|
|
69
|
+
|
|
70
|
+
**`suggested_goal_id` is deliberately not a well.** It sits on the row, it names a goal, and
|
|
71
|
+
it is the wrong fact: `projection.js` records it as strictly advisory — the routing hint on
|
|
72
|
+
a filing that did **not** convert, with `tasks.goal_id` as the binding home. Reading it here
|
|
73
|
+
would draw a protostar inside a well that never caught it and put it in a nod queue nobody
|
|
74
|
+
can act on, because no task exists to promote. The binding fact is now selected on both of
|
|
75
|
+
the trail's task joins (`trail.js`), which costs no extra round trip.
|
|
76
|
+
|
|
77
|
+
**The page reads the ledger once.** `attachSizeToTrail` leaves economy's own `byTask`
|
|
78
|
+
breakdown on each row and brightness takes its share of exactly that, so the state attach
|
|
79
|
+
hands it straight back. All of the sizes or none: a partial map reads a missing entry as
|
|
80
|
+
"no work", which would draw a paid star as a protostar.
|
|
81
|
+
|
|
82
|
+
**An absent `reward` port does not empty the sky.** The states still resolve on status
|
|
83
|
+
alone, carrying `attributable: false` — the same posture `trail-size.js` takes with
|
|
84
|
+
`size: null`.
|
|
85
|
+
|
|
86
|
+
## Consequences
|
|
87
|
+
|
|
88
|
+
- `GET /inbox/by-builder/:id` now returns `state` per idea beside `size` and
|
|
89
|
+
`descent_tasks`. Additive; no existing consumer reads it yet, and R03/R10/R20 will.
|
|
90
|
+
- The five objects are spelled once (`IDEA_STATES`), so a renderer switching on hand-copied
|
|
91
|
+
strings cannot drift by a hyphen and silently draw nothing.
|
|
92
|
+
- Nothing here writes, and no credit moves. ADR 0313 stands: `shipped` is terminal, and
|
|
93
|
+
only the picture changes.
|
|
94
|
+
- `tests/idea_state.mjs` (31 cases, DB-free). The ordering, the zero-over-zero edge, the
|
|
95
|
+
well and the trail SQL are each mutation-probed.
|
|
96
|
+
|
|
97
|
+
## Rejected
|
|
98
|
+
|
|
99
|
+
- **Resolving the state in each renderer.** The cheapest thing to write and the reason the
|
|
100
|
+
done-when says "no surface has to infer one".
|
|
101
|
+
- **Deriving the state from `share` alone.** It merges a protostar with a remnant — the
|
|
102
|
+
precise distinction the two channels exist to draw, and the one BV2.R19's proof checks.
|
|
103
|
+
- **Treating an unreadable `share` as zero.** "We could not look" is not "everything was
|
|
104
|
+
removed"; ADR 0321 pays for that distinction upstream and discarding it here wastes it.
|
|
105
|
+
- **Status-first ordering** (a discarded idea is always a cold body). It contradicts the
|
|
106
|
+
object's own stated property the moment triage discards something that already paid.
|
|
107
|
+
- **Reading `suggested_goal_id` as a well.** Advisory by construction; it would populate a
|
|
108
|
+
nod queue with entries nothing can act on.
|
|
109
|
+
- **A sixth object for "unknown brightness".** The vocabulary is the owner's and is five;
|
|
110
|
+
the uncertainty is carried as a flag on the answer, not as a new thing in the sky.
|
package/docs/adr/README.md
CHANGED
|
@@ -412,3 +412,5 @@ 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 |
|
|
416
|
+
| 0322 | [**The five states are derived once, and a remnant needs evidence** ([task 1004103](https://cloudbongos.com/builders#/task/1004103), goal 1000095 — BONGOS-V2). The ideator surface draws a thought as one of five objects — star, protostar, satellite, remnant, cold body — and three surfaces are about to draw them (the room, the descent, the sky). Every fact already existed: `idea_inbox.status` (`core_195`), [ADR 0321](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md)’s `hasWork` / `share` / `attributable`, and `tasks.goal_id` + `tasks.status`. What was missing was ONE place that reads them: a state inferred in three renderers is three states, and the sky can say a thought went out while the room beside it still calls it alive, with nothing erroring. **Decision:** one pure resolver (`modules/ideas/idea-state.js`), brightness exposed on the `reward` port (ideas may not reach `credit_log` any other way — [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 — and already declares `reward` in `consumes`), and the ORDER of the tests IS the decision. **MERGED wins over everything:** a folded thought keeps its own money (`trail-size.js` is emphatic) but is no longer its own point in the sky, so letting work win draws one idea twice, beside the thought that absorbed it. **WORK beats a DISCARD:** a cold body “never ignited, so it has no size at all” is a stated PROPERTY, and testing work first makes it true by construction — triage really can discard a duplicate whose twin already routed and paid, and `updateIdea` never touches the ledger. **A REMNANT needs EVIDENCE, never the absence of an answer:** `share: 0` is a confirmed removal, `share: null` is an unreadable one, and ADR 0321 pays for that distinction upstream — so an idea with work and no readable facts is a STAR carrying `attributable: false`, because reading a falsy share as death turns one unmounted module into a sky of hollow rings. **A PROTOSTAR is the absence of WORK, not of a status:** ignition is being PAID for something, so a promoted-but-unshipped idea is still unignited, which is exactly what the nod queue exists for. **Position is a second axis** (`well` / `open-space` / `awaitingNod`) because the design says the object alone cannot carry it, and the nod is asked of EVERY task, not only the one that named the well. **`suggested_goal_id` is deliberately NOT a well** — `projection.js` records it as strictly advisory, the routing hint on a filing that did not convert, so reading it would put a thought in a nod queue with no task to promote; the binding `tasks.goal_id` now rides on both of the trail’s task joins at no extra round trip. The page reads the ledger ONCE: the state attach hands economy back the `byTask` breakdown size just computed, all of them or none (a partial map reads a missing entry as “no work” and draws a paid star as a protostar). An absent `reward` port does not empty the sky — states still resolve, flagged unattributable. Nothing writes; ADR 0313 stands. Rejected: resolving in each renderer; deriving from `share` alone (it merges a protostar with a remnant, the exact BV2.R19 distinction); treating an unreadable share as zero; status-first ordering; reading the hint as a home; a sixth object for “unknown brightness” (the vocabulary is the owner’s and is five — the uncertainty is a flag on the answer, not a new thing in the sky).](0322-a-remnant-needs-evidence-and-merged-wins.md) | ideas / economy |
|
|
@@ -2316,5 +2316,9 @@ 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)
|
|
2321
|
+
1.19.923 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2322
|
+
landed since 1.19.922 with no explicit bump. run 35541278656. (task 1002620)
|
|
2319
2323
|
---------------------------------------------------------------------------
|
|
2320
2324
|
```
|