@bongos/core 1.19.904 → 1.19.906

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 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.904",
6
- "core_contract": "1.19.904",
7
- "source_commit": "a20b94f5efe38b09072d0456c5e352a2cf7bba1c",
5
+ "core_version": "1.19.906",
6
+ "core_contract": "1.19.906",
7
+ "source_commit": "292f91e86ee9b852efe6c0e93158f5815ccdfa42",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-20T12:45:09.857Z",
9
+ "built_at": "2026-09-20T13:41:10.148Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 502,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2278,
14
+ "functional_verbatim": 2283,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2806,
20
- "tree_sha256": "e03f442cd778ef6dd35a6c2f47a07e0f4d7c72a8d86882256367239d4b87989b",
19
+ "file_count": 2811,
20
+ "tree_sha256": "160619c1eb4a21d556e0ed9151463e10b29b88a5aeb1249d213d371aaae5402a",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2097,12 +2097,12 @@
2097
2097
  {
2098
2098
  "path": "docs/copy-inventory.md",
2099
2099
  "mode": "0000644",
2100
- "sha256": "3f1539c0050241b9dd7200170d664fc193c977958c2fee57e4bf5309591ccae6"
2100
+ "sha256": "3e657714f3647bfb041f4eda9e4982a2b60747c4aceec0d8d8cbf8ee0de6eb47"
2101
2101
  },
2102
2102
  {
2103
2103
  "path": "docs/copy-registry.json",
2104
2104
  "mode": "0000644",
2105
- "sha256": "1be015a58b02240690be5a2c5fcad56b42074500dcc7ebb8255ffa5156063748"
2105
+ "sha256": "1ba7f556256ee4cbb637b9809698c69aff529c080131949b4055058f8fb4716b"
2106
2106
  },
2107
2107
  {
2108
2108
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2632,7 +2632,7 @@
2632
2632
  {
2633
2633
  "path": "docs/module-api-changelog.md",
2634
2634
  "mode": "0000644",
2635
- "sha256": "25f3aef34d52ca5c8a93b25fa0cd79aeba0e2395e2f66d2e192ca57464d71b65"
2635
+ "sha256": "d329f2a3999d8bc88ce4715be5ee51d9e7a60ba0e0f0a9e06163c12ff94d87ba"
2636
2636
  },
2637
2637
  {
2638
2638
  "path": "docs/modules-contract.md",
@@ -3734,6 +3734,11 @@
3734
3734
  "mode": "0000644",
3735
3735
  "sha256": "599449a48b3850eb457a34630a7bd5d95d354602f1a9e9b887593183a457cd21"
3736
3736
  },
3737
+ {
3738
+ "path": "migrations/core_251_credit_log_idea_size_index.sql",
3739
+ "mode": "0000644",
3740
+ "sha256": "75bb644b32a70b52ac544edb901abff91aad07701848a470cc52103dc7e74234"
3741
+ },
3737
3742
  {
3738
3743
  "path": "modules/agents/CLAUDE.md",
3739
3744
  "mode": "0000644",
@@ -4182,7 +4187,17 @@
4182
4187
  {
4183
4188
  "path": "modules/economy/credits.js",
4184
4189
  "mode": "0000644",
4185
- "sha256": "b0f36f5d71bc376e090b14c6c95b3ac005fecd464e59bc73213d1d3a2b662469"
4190
+ "sha256": "736eed2c89863d6eb28072627a40ac4f8945832cfb7b1d1aba10e6cc65483901"
4191
+ },
4192
+ {
4193
+ "path": "modules/economy/idea-size-historical.js",
4194
+ "mode": "0000644",
4195
+ "sha256": "f99f62eb4eaeda10b32284dbe27f87e64d5b6b82c9eb4aa442057d0de13be761"
4196
+ },
4197
+ {
4198
+ "path": "modules/economy/idea-size.js",
4199
+ "mode": "0000644",
4200
+ "sha256": "d1fa5fda6b34bd2a8cf8d563c780bc4b12f1cdfb955e13f51799a589609ac1c0"
4186
4201
  },
4187
4202
  {
4188
4203
  "path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
@@ -4207,7 +4222,7 @@
4207
4222
  {
4208
4223
  "path": "modules/economy/reward.js",
4209
4224
  "mode": "0000644",
4210
- "sha256": "05f2aa731ed9bfc19088c0c6a59e0f91c6096806285e74e74b48f4530bcfa5f0"
4225
+ "sha256": "ef4b939d83324c927dd1b1c8e3200212d0b5da973a87db6f9810c9c7edb942ad"
4211
4226
  },
4212
4227
  {
4213
4228
  "path": "modules/economy/routes/cost.js",
@@ -4977,7 +4992,7 @@
4977
4992
  {
4978
4993
  "path": "modules/hall-ui/public/people.js",
4979
4994
  "mode": "0000644",
4980
- "sha256": "4740be7b7d28c8c3e4098054c63de85086265149f7024b15cff443d59283c087"
4995
+ "sha256": "37d7c9b14bb42a473ef7caf45f138cd87b01b976bb593d1fd2a0c82c94208539"
4981
4996
  },
4982
4997
  {
4983
4998
  "path": "modules/hall-ui/public/people.states.json",
@@ -5037,7 +5052,7 @@
5037
5052
  {
5038
5053
  "path": "modules/hall-ui/public/profile.js",
5039
5054
  "mode": "0000644",
5040
- "sha256": "0ca0433fa846d12e2ea2d69084ad406d6e23fbe92e20c7364da4e92e54e26cf4"
5055
+ "sha256": "04015850c00627f2439ec919358516c841d4c32c64a7ba18ca758b3804e804b5"
5041
5056
  },
5042
5057
  {
5043
5058
  "path": "modules/hall-ui/public/profile.states.json",
@@ -5377,7 +5392,7 @@
5377
5392
  {
5378
5393
  "path": "modules/ideas/routes/inbox.js",
5379
5394
  "mode": "0000644",
5380
- "sha256": "d4c91dcc603e4fc72ef6110a32350b2f59df19858f83a70962645a7fa09a55c4"
5395
+ "sha256": "cd4e4d741dcbc4bb581f86aeec5b1e17dfe630712a0e5466a108daba78c74429"
5381
5396
  },
5382
5397
  {
5383
5398
  "path": "modules/ideas/routing.js",
@@ -5399,6 +5414,11 @@
5399
5414
  "mode": "0000644",
5400
5415
  "sha256": "2d0693b00dded081b6a0082c450762763d324d54f098a8f42a9aa25ec27370c4"
5401
5416
  },
5417
+ {
5418
+ "path": "modules/ideas/trail-size.js",
5419
+ "mode": "0000644",
5420
+ "sha256": "f1e60dbc8c6dc0e9f5f5cb93bf26c659422503e8ec66937a3dcee5b64971e995"
5421
+ },
5402
5422
  {
5403
5423
  "path": "modules/lifecycle/CLAUDE.md",
5404
5424
  "mode": "0000644",
@@ -7762,12 +7782,12 @@
7762
7782
  {
7763
7783
  "path": "package-lock.json",
7764
7784
  "mode": "0000644",
7765
- "sha256": "f6623c9d73536e00c2c9a3d44dc68f3e8bdbe44e47543c32aaa22f3059efa410"
7785
+ "sha256": "6a2f94c668864dc88fdc003473496102838182e8d18636930537c32bd67c7e47"
7766
7786
  },
7767
7787
  {
7768
7788
  "path": "package.json",
7769
7789
  "mode": "0000644",
7770
- "sha256": "f092d5149aa7e2518d2ddd4b940e92c4d61f8c553afedd810d97752efa9a207e"
7790
+ "sha256": "1ddcd43de722cb5eff00c36a440b0df181f1c8168a8c8b3193fc3d9a586afdbd"
7771
7791
  },
7772
7792
  {
7773
7793
  "path": "public-docs/index.html",
@@ -9762,7 +9782,7 @@
9762
9782
  {
9763
9783
  "path": "src/module-api.js",
9764
9784
  "mode": "0000644",
9765
- "sha256": "f15a6c9c7458ce744ffbe72c8997b25e0098a2a26b2f6f41e7e3513fc073110d"
9785
+ "sha256": "4593c8749b7b52a5bb6116e0d3dbe6004dc24346bb79ca0a5757359e98dc259e"
9766
9786
  },
9767
9787
  {
9768
9788
  "path": "src/module-loader/catalog.js",
@@ -11652,7 +11672,7 @@
11652
11672
  {
11653
11673
  "path": "tests/hall_roster_world.mjs",
11654
11674
  "mode": "0000644",
11655
- "sha256": "191184eeaf40d8e10704ae57bfe1d58574f7b6053634d69844d47212bf05f18a"
11675
+ "sha256": "477ec31839036884ac0923a078e7cb1e60d37548c758e24701b338c1e8179c6a"
11656
11676
  },
11657
11677
  {
11658
11678
  "path": "tests/hall_sessions_ledger.mjs",
@@ -11849,6 +11869,11 @@
11849
11869
  "mode": "0000644",
11850
11870
  "sha256": "3a2c5d2e6c3231f69847e1e2aea073e78caf26228b500dbdf612b5bae354d0a2"
11851
11871
  },
11872
+ {
11873
+ "path": "tests/idea_size.mjs",
11874
+ "mode": "0000644",
11875
+ "sha256": "c307e7c796e7e9f30b38b04144b426895642ea2d978ea5ace0f701ba7744420c"
11876
+ },
11852
11877
  {
11853
11878
  "path": "tests/idea_spark_hall.mjs",
11854
11879
  "mode": "0000644",
@@ -12487,7 +12512,7 @@
12487
12512
  {
12488
12513
  "path": "tests/people_directory_clip.mjs",
12489
12514
  "mode": "0000644",
12490
- "sha256": "b412cf076656c3400d76f46a2df18adb5a2ec376ed7e3da6beefcf344446993b"
12515
+ "sha256": "e3b2f36780b35fd66690b1c426832322b2781c5386cf7fd50e995a7c2cc089bb"
12491
12516
  },
12492
12517
  {
12493
12518
  "path": "tests/permission_path.mjs",
@@ -308,7 +308,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
308
308
  | `7775fec0fa85` | certain | Could not load sessions. | `modules/hall-ui/public/sessions.js:437` |
309
309
  | `a2f2e8babef5` | certain | Could not load that page. Try again. | `modules/hall-ui/public/blockers.js:274` |
310
310
  | `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:626` |
311
- | `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:836` |
311
+ | `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:873` |
312
312
  | `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:244` |
313
313
  | `edd5bafbe075` | certain | Could not load the queue just now. | `modules/hall-ui/public/ideas.js:565` |
314
314
  | `02bb0570996a` | certain | Could not load the ranks. | `modules/hall-ui/public/ranks.js:197` |
@@ -600,7 +600,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
600
600
  | `6e73716aec6c` | certain | May be offered for sale later | `modules/hall-ui/public/settings.html:297` |
601
601
  | `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/hall-render.js:563` |
602
602
  | `df335a1224ae` | certain | Member since {…} | `modules/hall-ui/public/profile.js:94` |
603
- | `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:452` |
603
+ | `c7a670e9552d` | certain | Merged into | `modules/hall-ui/public/profile.js:489` |
604
604
  | `dce0546c9e34` | certain | Merging work does not put it in front of anyone. This is the step that does: it moves this platform — or any p | `modules/hall-ui/public/deploy.html:39` |
605
605
  | `7637613c231b` | certain | Metic rank and above may vote | `modules/hall-ui/public/builders.js:781` |
606
606
  | `91328a97fe68` | certain | Mingle pairs you with someone from another role. Saying no costs nothing. | `modules/hall-ui/public/collab.html:87` |
@@ -918,7 +918,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
918
918
  | `9d4372b173cf` | certain | Standing sections | `modules/hall-ui/public/index.html:79` |
919
919
  | `ec1c2dae4a90` | certain | Standing there | `modules/hall-ui/public/profile.js:314` |
920
920
  | `eaad12cacb9d` | certain | Start over | `modules/hall-ui/public/studio.js:235` |
921
- | `efe530c0e3a7` | certain | Still open | `modules/hall-ui/public/profile.js:457` |
921
+ | `efe530c0e3a7` | certain | Still open | `modules/hall-ui/public/profile.js:494` |
922
922
  | `9a4ced1afb6c` | certain | Stop waiting on task {…} | `modules/hall-ui/public/task.js:278` |
923
923
  | `b0861c874a2f` | certain | Stopped using it. | `modules/hall-ui/public/settings-specialities.js:200` |
924
924
  | `6c6544f5c011` | certain | Stored encrypted; never shown again after saving — only its last four digits. | `modules/hall-ui/public/settings.html:193` |
@@ -1352,7 +1352,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1352
1352
  | `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-lib.js:32` |
1353
1353
  | `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-render.js:682` |
1354
1354
  | `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals.js:45` |
1355
- | `c3d9687e2375` | certain | {…} of {…} shipped | `modules/hall-ui/public/profile.js:558` |
1355
+ | `c3d9687e2375` | certain | {…} of {…} shipped | `modules/hall-ui/public/profile.js:595` |
1356
1356
  | `f32710d0ef37` | certain | {…} of {…} steps cleared. | `modules/hall-ui/public/hall-render.js:1037` |
1357
1357
  | `60743fe1e40d` | certain | {…} per page | `modules/hall-ui/public/hall-kit.js:74` |
1358
1358
  | `154bc518bab4` | certain | {…} proposed | `modules/hall-ui/public/government.js:695` |
@@ -3815,7 +3815,7 @@
3815
3815
  "surface": "builders-hall",
3816
3816
  "text": "Could not load the profile.",
3817
3817
  "file": "modules/hall-ui/public/profile.js",
3818
- "line": 836,
3818
+ "line": 873,
3819
3819
  "origin": "js-notice",
3820
3820
  "confidence": "certain"
3821
3821
  },
@@ -6443,7 +6443,7 @@
6443
6443
  "surface": "builders-hall",
6444
6444
  "text": "Merged into",
6445
6445
  "file": "modules/hall-ui/public/profile.js",
6446
- "line": 452,
6446
+ "line": 489,
6447
6447
  "origin": "js-markup",
6448
6448
  "confidence": "certain"
6449
6449
  },
@@ -9305,7 +9305,7 @@
9305
9305
  "surface": "builders-hall",
9306
9306
  "text": "Still open",
9307
9307
  "file": "modules/hall-ui/public/profile.js",
9308
- "line": 457,
9308
+ "line": 494,
9309
9309
  "origin": "js-markup",
9310
9310
  "confidence": "certain"
9311
9311
  },
@@ -13211,7 +13211,7 @@
13211
13211
  "surface": "builders-hall",
13212
13212
  "text": "{…} of {…} shipped",
13213
13213
  "file": "modules/hall-ui/public/profile.js",
13214
- "line": 558,
13214
+ "line": 595,
13215
13215
  "origin": "js-markup",
13216
13216
  "confidence": "certain"
13217
13217
  },
@@ -2282,5 +2282,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2282
2282
  landed since 1.19.902 with no explicit bump. run 35510679104. (task 1002620)
2283
2283
  1.19.904 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2284
2284
  landed since 1.19.903 with no explicit bump. run 35511494246. (task 1002620)
2285
+ 1.19.905 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2286
+ landed since 1.19.904 with no explicit bump. run 35513060618. (task 1002620)
2287
+ 1.19.906 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2288
+ landed since 1.19.905 with no explicit bump. run 35514202453. (task 1002620)
2285
2289
  ---------------------------------------------------------------------------
2286
2290
  ```
@@ -0,0 +1,61 @@
1
+ -- core_251 an index for SIZE -- the per-idea rollup of the stream-1 ledger
2
+ -- Applied under task 1004093 (BV2.R12, goal 1000095).
3
+ --
4
+ -- WHY. modules/economy/idea-size.js answers "what has this idea's descent
5
+ -- paid" by summing credit_log rows whose reason embeds the idea:
6
+ --
7
+ -- idea.credit.task_ship:<ideaId>:<taskId>
8
+ --
9
+ -- That needs no column and no new table -- the ledger already carries the
10
+ -- attribution, which is why this task needed no SCHEMA change. What it does
11
+ -- need is a way to reach those rows without reading the whole ledger.
12
+ --
13
+ -- WHAT IT COSTS WITHOUT ONE. credit_log is the whole-instance, append-only
14
+ -- credit ledger: every task payout, every session reward, every bonus, for
15
+ -- every builder, forever. The size read runs on GET /inbox/by-builder/:id --
16
+ -- an ordinary page view of a builder's idea trail -- so an unindexed version
17
+ -- is a sequential scan of the entire lifetime ledger on every such view, and
18
+ -- its cost grows with total credit events rather than with anything about the
19
+ -- idea being looked at. A grader panel flagged exactly this on the first round
20
+ -- of task 1004093, and it was right to.
21
+ --
22
+ -- THE SHAPE. A PARTIAL EXPRESSION index:
23
+ --
24
+ -- * the EXPRESSION is split_part(reason, ':', 2) -- the idea id, taken off
25
+ -- the separator. The read groups on that same expression rather than on a
26
+ -- per-idea LIKE, because a prefix match is correct only by virtue of its
27
+ -- trailing colon and dropping it makes idea 1 absorb every payout of ideas
28
+ -- 10, 100 and 1000, silently. An index on the expression is what lets the
29
+ -- honest form of the query stay cheap.
30
+ -- * PARTIAL on the stream-1 prefix, so the index covers only ideator
31
+ -- task-ship rows. Those are a small minority of credit_log: the bulk is
32
+ -- task.shipped and session.token_reward, whose split_part(reason,':',2) is
33
+ -- the empty string and which no size read will ever want. A full index
34
+ -- would be several times the size to answer the same questions.
35
+ --
36
+ -- THE PREDICATE IS A LITERAL ON BOTH SIDES, AND THAT IS LOAD-BEARING. Postgres
37
+ -- only uses a partial index when it can prove the query's WHERE implies the
38
+ -- index predicate, and it cannot prove that about a parameterised
39
+ -- `reason LIKE $1 || '%'` -- the parameter's value is not known when a generic
40
+ -- plan is made. So idea-size.js spells the prefix into the SQL TEXT as a
41
+ -- literal (validated at module load), matching the string below character for
42
+ -- character. If the two ever drift, nothing errors and nothing renders
43
+ -- differently: the index is simply never used again and the scan quietly comes
44
+ -- back. tests/idea_size.mjs reads THIS FILE and holds its predicate against the
45
+ -- economy constant, so the drift reds a test instead of a page.
46
+ --
47
+ -- THE UNDERSCORE IS ESCAPED, and it is not decoration. In LIKE, _ matches any
48
+ -- single character, so an unescaped 'idea.credit.task_ship:%' also matches
49
+ -- 'idea.credit.taskXship:'. No such key exists, which is precisely why nobody
50
+ -- would ever notice; backslash is LIKE's default escape in Postgres, so no
51
+ -- ESCAPE clause is needed and idea-size.js generates the identical string.
52
+ --
53
+ -- Additive and idempotent: it creates an index and touches no row. Not
54
+ -- CONCURRENTLY -- the migration runner wraps each file in a transaction and
55
+ -- CREATE INDEX CONCURRENTLY cannot run inside one. On an instance whose ledger
56
+ -- is large enough for that lock to matter, build it by hand outside the runner
57
+ -- and this statement becomes a no-op.
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_credit_log_idea_task_ship
60
+ ON credit_log ((split_part(reason, ':', 2)))
61
+ WHERE reason LIKE 'idea.credit.task\_ship:%';
@@ -1273,6 +1273,14 @@ module.exports = {
1273
1273
  IDEA_FULL_PASS_CREDIT,
1274
1274
  IDEA_FULL_PASS_BAR,
1275
1275
  IDEA_TASK_SHIP_BASE,
1276
+ // The two pieces of the stream-1 payout ARITHMETIC, exported for
1277
+ // modules/economy/idea-size.js (task 1004097 / BV2.R18). A pre-existing
1278
+ // idea backfills the payouts its descent never received, and a second copy
1279
+ // of this arithmetic would make the backfilled figure differ from the money
1280
+ // the same descent will earn from now on — the exact drift the
1281
+ // ideaGradeFactor comment above exists to prevent.
1282
+ ideaGradeFactor,
1283
+ descentPayoutMaxDepth,
1276
1284
  QUICK_IDEA_FACTOR,
1277
1285
  IDEA_GOVERNANCE_PASS_MAX,
1278
1286
  IDEA_GOVERNANCE_PASS_FLOOR,
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ // modules/economy/idea-size-historical.js — THE HISTORICAL HALF OF SIZE
4
+ // (task 1004097, BV2.R18, goal 1000095; spec
5
+ // docs/specs/bongos-v2-ideator-surface.md decision 17).
6
+ //
7
+ // WHAT IS MISSING WITHOUT THIS, and it is not what the task title suggests.
8
+ // A pre-existing idea's ledger figure is not ABSENT, it is PARTIAL. Stream 1's
9
+ // direct payout has existed since ADR 0172, so a task promoted from an idea and
10
+ // shipped since then really was paid and really is in credit_log. What never
11
+ // happened is the DESCENT payout: awardIdeaDescentShipCredit only started
12
+ // paying with task 1004088, so every hop below the first, for every task that
13
+ // shipped before that, earned its origin nothing and never will.
14
+ //
15
+ // So the backfill is not "compute a size for ideas that have none". It is the
16
+ // GAP between the descent an idea actually has and the part of that descent the
17
+ // ledger was there to pay for.
18
+ //
19
+ // NO MONEY IS BOOKED (spec decision 17, the owner's ruling on interview Q6).
20
+ // Nothing here writes to credit_log, and nothing here is reachable from a ship
21
+ // path. The estimate is computed at READ time, every time, from the descent as
22
+ // it stands. Re-paying work already paid for once is exactly what the owner
23
+ // declined, and a backfill that wrote rows would also break stream 1's
24
+ // idempotency key — the ledger would then hold a payment for a ship that never
25
+ // triggered one.
26
+ //
27
+ // WHICH IS WHY THE TWO HALVES ARE KEPT APART RATHER THAN ADDED UP AND FORGOTTEN.
28
+ // `paid` is money that moved; `estimated` is money that did not. A single total
29
+ // with no marker is the number the spec calls a quiet lie, and the done-when
30
+ // asks for the opposite — that anywhere size is shown makes clear which kind of
31
+ // number it is. So this returns both, always, and the surfaces carry the split
32
+ // rather than a boolean bolted on at the end.
33
+ //
34
+ // THE ARITHMETIC IS NOT REIMPLEMENTED HERE. `ideaGradeFactor`,
35
+ // `descentPayoutMaxDepth` and `IDEA_TASK_SHIP_BASE` come from credits.js, which
36
+ // is where the real payout gets them. A second copy would make an idea's
37
+ // backfilled past disagree with the money the same descent earns from tomorrow
38
+ // — the same drift the "EXTRACTED so the two cannot disagree" note on
39
+ // ideaGradeFactor was written to prevent, one module over.
40
+
41
+ const {
42
+ IDEA_TASK_SHIP_BASE,
43
+ ideaGradeFactor,
44
+ descentPayoutMaxDepth,
45
+ } = require('./credits');
46
+
47
+ // priceDescentForIdea — PURE. What one idea's descent is worth, split into the
48
+ // part the ledger actually paid and the part it never did.
49
+ //
50
+ // tasks — every task under this idea, each { task_id, depth, status }.
51
+ // Depth 0 is a task the idea was promoted INTO; depth n is n
52
+ // hops below one of those. Shortest depth per task, which is the
53
+ // rule the walk and the payout both already apply.
54
+ // grade — the idea's grade ('quick' | 'spark' | 'full')
55
+ // completeness— its completeness score, or null
56
+ // decay — the resolved per-instance dial (reward-policy ideaDescentDecay)
57
+ // paidByTask — Map<taskIdString, creditsPaid> from credit_log, per task
58
+ //
59
+ // Returns { paid, estimated, payments, estimatedPayments }.
60
+ //
61
+ // ONLY A SHIPPED TASK COUNTS. The payout fires at ship, so a ready or abandoned
62
+ // descendant has earned its origin nothing and estimating for it would credit
63
+ // work that has not happened — the surface would then show a size that FELL
64
+ // when the task was later abandoned. Everything here is anchored on ships that
65
+ // actually occurred.
66
+ //
67
+ // PAID ALWAYS WINS FOR A TASK THE LEDGER KNOWS. If credit_log holds a payment
68
+ // for (idea, task), that figure is used verbatim and no estimate is made for it
69
+ // — money that moved is not re-derived. This is what keeps the paid half
70
+ // immutable while the arithmetic around it is free to change: a later decay dial
71
+ // or a re-scored completeness cannot retroactively alter what was banked.
72
+ //
73
+ // ROUNDED ONCE PER PAYMENT, the way the writer rounds — per (idea, task), never
74
+ // on the total. Rounding the sum would make the backfilled figure differ from
75
+ // what the same descent would have been paid hop by hop, which is the whole
76
+ // thing this function exists to reproduce.
77
+ function priceDescentForIdea({ tasks, grade, completeness, decay, paidByTask } = {}) {
78
+ const out = { paid: 0, estimated: 0, payments: 0, estimatedPayments: 0 };
79
+ const list = Array.isArray(tasks) ? tasks : [];
80
+ const paidMap = paidByTask instanceof Map ? paidByTask : new Map();
81
+
82
+ // A decay of 0 is a legal instance setting meaning "only the direct task
83
+ // pays", and it makes the cap 0 — so depth 0 is still priced and nothing
84
+ // below it is. Guarding on the cap rather than on the rate keeps that in one
85
+ // place.
86
+ const rate = Number.isFinite(Number(decay)) && Number(decay) >= 0 && Number(decay) < 1 ? Number(decay) : null;
87
+ if (rate === null) return out;
88
+ const maxDepth = descentPayoutMaxDepth(rate);
89
+ const factor = ideaGradeFactor(grade, completeness);
90
+
91
+ // One entry per task: a task reachable twice is one ship and owes one
92
+ // payment, at its shortest depth. The caller hands shortest-depth rows, and
93
+ // this keeps the guarantee local rather than trusting it.
94
+ const seen = new Set();
95
+ for (const t of list) {
96
+ const key = String(t && t.task_id);
97
+ if (!/^[1-9]\d*$/.test(key) || seen.has(key)) continue;
98
+ seen.add(key);
99
+
100
+ if (paidMap.has(key)) {
101
+ out.paid += Number(paidMap.get(key)) || 0;
102
+ out.payments += 1;
103
+ continue;
104
+ }
105
+
106
+ if (t.status !== 'shipped') continue;
107
+ const depth = Number(t.depth);
108
+ if (!Number.isInteger(depth) || depth < 0) continue;
109
+ // Depth 0 is the direct payout at full share; below that the cap applies,
110
+ // because past it even a perfect idea's decayed share rounds away.
111
+ if (depth > 0 && (rate === 0 || depth > maxDepth)) continue;
112
+ const delta = Math.round(IDEA_TASK_SHIP_BASE * factor * Math.pow(rate, depth));
113
+ if (delta <= 0) continue;
114
+ out.estimated += delta;
115
+ out.estimatedPayments += 1;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // descentRowsFromTrail — the tasks under an idea, as the TRAIL already holds
121
+ // them. Pure, and deliberately not a walk.
122
+ //
123
+ // modules/ideas/trail-descent.js has already walked the whole page by the time
124
+ // size is attached (the route composes them in that order), so the trail path
125
+ // re-uses that result and costs no extra round trip at all. `lineage_tasks` are
126
+ // the idea's own tasks — depth 0, the full-share hop — and `descent_tasks` are
127
+ // everything below them, already carrying their shortest depth and their status.
128
+ //
129
+ // A lineage row that also appears in the descent is dropped by the depth-0 entry
130
+ // winning: the Set in priceDescentForIdea keeps the first sighting, and the
131
+ // direct tasks are emitted first for exactly that reason.
132
+ function descentRowsFromTrail(idea) {
133
+ const rows = [];
134
+ const direct = Array.isArray(idea && idea.lineage_tasks) ? idea.lineage_tasks : [];
135
+ for (const t of direct) rows.push({ task_id: t.task_id, depth: 0, status: t.status });
136
+ // The legacy single-FK promotion, for rows that predate ideas_task_lineage —
137
+ // the same fallback the trail and trail-descent both apply.
138
+ if (direct.length === 0 && idea && idea.promoted_to_task_id != null) {
139
+ rows.push({ task_id: idea.promoted_to_task_id, depth: 0, status: idea.promoted_task_status ?? null });
140
+ }
141
+ const below = Array.isArray(idea && idea.descent_tasks) ? idea.descent_tasks : [];
142
+ for (const t of below) rows.push({ task_id: t.task_id, depth: t.depth, status: t.status });
143
+ return rows;
144
+ }
145
+
146
+ module.exports = {
147
+ priceDescentForIdea,
148
+ descentRowsFromTrail,
149
+ };