@bongos/core 1.19.297 → 1.19.299

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.
@@ -3,17 +3,17 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1339,
7
- "unique_ids": 1188,
8
- "certain": 1339,
6
+ "strings": 1354,
7
+ "unique_ids": 1202,
8
+ "certain": 1354,
9
9
  "heuristic": 0,
10
- "duplicate_ids": 121
10
+ "duplicate_ids": 122
11
11
  },
12
12
  "surfaces": {
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
- "files": 61,
16
- "strings": 1055
15
+ "files": 63,
16
+ "strings": 1070
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -191,7 +191,7 @@
191
191
  "at": [
192
192
  "modules/hall-ui/public/builders.js:553",
193
193
  "modules/hall-ui/public/builders.js:660",
194
- "modules/hall-ui/public/shell.js:265"
194
+ "modules/hall-ui/public/shell.js:273"
195
195
  ]
196
196
  },
197
197
  {
@@ -658,6 +658,16 @@
658
658
  "modules/hall-ui/public/primer.js:496"
659
659
  ]
660
660
  },
661
+ {
662
+ "id": "ef224c9de7c8",
663
+ "surface": "builders-hall",
664
+ "text": "Ideas — {{worldName}}",
665
+ "occurrences": 2,
666
+ "at": [
667
+ "modules/hall-ui/public/ideas.html:6",
668
+ "modules/hall-ui/public/ideas.html:6"
669
+ ]
670
+ },
661
671
  {
662
672
  "id": "41cc7f5ec97e",
663
673
  "surface": "status-dashboard",
@@ -1024,8 +1034,8 @@
1024
1034
  "text": "Sign out",
1025
1035
  "occurrences": 2,
1026
1036
  "at": [
1027
- "modules/hall-ui/public/shell.js:283",
1028
- "modules/hall-ui/public/shell.js:418"
1037
+ "modules/hall-ui/public/shell.js:291",
1038
+ "modules/hall-ui/public/shell.js:426"
1029
1039
  ]
1030
1040
  },
1031
1041
  {
@@ -1604,6 +1614,15 @@
1604
1614
  "origin": "html-text",
1605
1615
  "confidence": "certain"
1606
1616
  },
1617
+ {
1618
+ "id": "8ffdf4995e81",
1619
+ "surface": "builders-hall",
1620
+ "text": "A Full Idea answers five questions, so that someone else can deliver against it without having to interview you first. Open to every role — the score below is the only filter, and it counts substance, not who you are.",
1621
+ "file": "modules/hall-ui/public/ideas.html",
1622
+ "line": 33,
1623
+ "origin": "html-text",
1624
+ "confidence": "certain"
1625
+ },
1607
1626
  {
1608
1627
  "id": "3e5a62bc4887",
1609
1628
  "surface": "builders-hall",
@@ -3044,6 +3063,15 @@
3044
3063
  "origin": "js-markup",
3045
3064
  "confidence": "certain"
3046
3065
  },
3066
+ {
3067
+ "id": "b2147cc90e36",
3068
+ "surface": "builders-hall",
3069
+ "text": "Could not load the idea template. Reload, or file from the terminal with capture.js.",
3070
+ "file": "modules/hall-ui/public/ideas.js",
3071
+ "line": 311,
3072
+ "origin": "js-text-assign",
3073
+ "confidence": "certain"
3074
+ },
3047
3075
  {
3048
3076
  "id": "6264ebc9ede0",
3049
3077
  "surface": "builders-hall",
@@ -3458,6 +3486,15 @@
3458
3486
  "origin": "js-text-assign",
3459
3487
  "confidence": "certain"
3460
3488
  },
3489
+ {
3490
+ "id": "28ec06f6060a",
3491
+ "surface": "builders-hall",
3492
+ "text": "Counted, not judged: each question is worth the same, and earns its share in proportion to how much real text it carries. Whitespace and padding score nothing.",
3493
+ "file": "modules/hall-ui/public/ideas.html",
3494
+ "line": 82,
3495
+ "origin": "html-text",
3496
+ "confidence": "certain"
3497
+ },
3461
3498
  {
3462
3499
  "id": "38827a954f3b",
3463
3500
  "surface": "builders-hall",
@@ -3611,6 +3648,15 @@
3611
3648
  "origin": "html-text",
3612
3649
  "confidence": "certain"
3613
3650
  },
3651
+ {
3652
+ "id": "65322ac89221",
3653
+ "surface": "builders-hall",
3654
+ "text": "Discard draft",
3655
+ "file": "modules/hall-ui/public/ideas.html",
3656
+ "line": 60,
3657
+ "origin": "html-text",
3658
+ "confidence": "certain"
3659
+ },
3614
3660
  {
3615
3661
  "id": "0f685daf1bd5",
3616
3662
  "surface": "builders-hall",
@@ -3746,6 +3792,15 @@
3746
3792
  "origin": "html-title",
3747
3793
  "confidence": "certain"
3748
3794
  },
3795
+ {
3796
+ "id": "2f8d0404039e",
3797
+ "surface": "builders-hall",
3798
+ "text": "Draft discarded.",
3799
+ "file": "modules/hall-ui/public/ideas.js",
3800
+ "line": 301,
3801
+ "origin": "js-notice",
3802
+ "confidence": "certain"
3803
+ },
3749
3804
  {
3750
3805
  "id": "d59beed97d58",
3751
3806
  "surface": "builders-hall",
@@ -3926,6 +3981,15 @@
3926
3981
  "origin": "html-text",
3927
3982
  "confidence": "certain"
3928
3983
  },
3984
+ {
3985
+ "id": "077cf99f49f6",
3986
+ "surface": "builders-hall",
3987
+ "text": "File this idea",
3988
+ "file": "modules/hall-ui/public/ideas.html",
3989
+ "line": 59,
3990
+ "origin": "html-text",
3991
+ "confidence": "certain"
3992
+ },
3929
3993
  {
3930
3994
  "id": "1def0e7c716e",
3931
3995
  "surface": "builders-hall",
@@ -3998,6 +4062,15 @@
3998
4062
  "origin": "html-text",
3999
4063
  "confidence": "certain"
4000
4064
  },
4065
+ {
4066
+ "id": "18ab1be9d3ad",
4067
+ "surface": "builders-hall",
4068
+ "text": "Five questions, and a score that counts how much of each you actually answered.",
4069
+ "file": "modules/hall-ui/public/ideas.js",
4070
+ "line": 321,
4071
+ "origin": "js-text-assign",
4072
+ "confidence": "certain"
4073
+ },
4001
4074
  {
4002
4075
  "id": "b9267033b1f1",
4003
4076
  "surface": "builders-hall",
@@ -4475,6 +4548,24 @@
4475
4548
  "origin": "js-markup",
4476
4549
  "confidence": "certain"
4477
4550
  },
4551
+ {
4552
+ "id": "ef224c9de7c8",
4553
+ "surface": "builders-hall",
4554
+ "text": "Ideas — {{worldName}}",
4555
+ "file": "modules/hall-ui/public/ideas.html",
4556
+ "line": 6,
4557
+ "origin": "html-text",
4558
+ "confidence": "certain"
4559
+ },
4560
+ {
4561
+ "id": "ef224c9de7c8",
4562
+ "surface": "builders-hall",
4563
+ "text": "Ideas — {{worldName}}",
4564
+ "file": "modules/hall-ui/public/ideas.html",
4565
+ "line": 6,
4566
+ "origin": "html-title",
4567
+ "confidence": "certain"
4568
+ },
4478
4569
  {
4479
4570
  "id": "b6c47d4ee42c",
4480
4571
  "surface": "builders-hall",
@@ -4615,7 +4706,7 @@
4615
4706
  "surface": "builders-hall",
4616
4707
  "text": "Jump to a page or record",
4617
4708
  "file": "modules/hall-ui/public/shell.js",
4618
- "line": 304,
4709
+ "line": 312,
4619
4710
  "origin": "js-markup",
4620
4711
  "confidence": "certain"
4621
4712
  },
@@ -4624,7 +4715,7 @@
4624
4715
  "surface": "builders-hall",
4625
4716
  "text": "Jump to…",
4626
4717
  "file": "modules/hall-ui/public/shell.js",
4627
- "line": 306,
4718
+ "line": 314,
4628
4719
  "origin": "js-markup",
4629
4720
  "confidence": "certain"
4630
4721
  },
@@ -4916,6 +5007,24 @@
4916
5007
  "origin": "html-text",
4917
5008
  "confidence": "certain"
4918
5009
  },
5010
+ {
5011
+ "id": "fcd755e8dd30",
5012
+ "surface": "builders-hall",
5013
+ "text": "Loading the five questions…",
5014
+ "file": "modules/hall-ui/public/ideas.html",
5015
+ "line": 53,
5016
+ "origin": "html-text",
5017
+ "confidence": "certain"
5018
+ },
5019
+ {
5020
+ "id": "95a5382e5277",
5021
+ "surface": "builders-hall",
5022
+ "text": "Loading the idea template…",
5023
+ "file": "modules/hall-ui/public/ideas.html",
5024
+ "line": 23,
5025
+ "origin": "html-text",
5026
+ "confidence": "certain"
5027
+ },
4919
5028
  {
4920
5029
  "id": "9972fc9b4727",
4921
5030
  "surface": "builders-hall",
@@ -5717,6 +5826,15 @@
5717
5826
  "origin": "js-text-assign",
5718
5827
  "confidence": "certain"
5719
5828
  },
5829
+ {
5830
+ "id": "d5f0b7156024",
5831
+ "surface": "builders-hall",
5832
+ "text": "One line. This is what the idea is called everywhere else.",
5833
+ "file": "modules/hall-ui/public/ideas.html",
5834
+ "line": 42,
5835
+ "origin": "html-text",
5836
+ "confidence": "certain"
5837
+ },
5720
5838
  {
5721
5839
  "id": "0bc1d1470e22",
5722
5840
  "surface": "builders-hall",
@@ -5776,7 +5894,7 @@
5776
5894
  "surface": "builders-hall",
5777
5895
  "text": "Open navigation",
5778
5896
  "file": "modules/hall-ui/public/shell.js",
5779
- "line": 295,
5897
+ "line": 303,
5780
5898
  "origin": "js-markup",
5781
5899
  "confidence": "certain"
5782
5900
  },
@@ -7171,7 +7289,7 @@
7171
7289
  "surface": "builders-hall",
7172
7290
  "text": "Sign in with GitHub",
7173
7291
  "file": "modules/hall-ui/public/shell.js",
7174
- "line": 265,
7292
+ "line": 273,
7175
7293
  "origin": "js-markup",
7176
7294
  "confidence": "certain"
7177
7295
  },
@@ -7180,7 +7298,7 @@
7180
7298
  "surface": "builders-hall",
7181
7299
  "text": "Sign out",
7182
7300
  "file": "modules/hall-ui/public/shell.js",
7183
- "line": 283,
7301
+ "line": 291,
7184
7302
  "origin": "js-markup",
7185
7303
  "confidence": "certain"
7186
7304
  },
@@ -7189,7 +7307,7 @@
7189
7307
  "surface": "builders-hall",
7190
7308
  "text": "Sign out",
7191
7309
  "file": "modules/hall-ui/public/shell.js",
7192
- "line": 418,
7310
+ "line": 426,
7193
7311
  "origin": "js-markup",
7194
7312
  "confidence": "certain"
7195
7313
  },
@@ -7351,7 +7469,7 @@
7351
7469
  "surface": "builders-hall",
7352
7470
  "text": "Switch theme",
7353
7471
  "file": "modules/hall-ui/public/shell.js",
7354
- "line": 310,
7472
+ "line": 318,
7355
7473
  "origin": "js-markup",
7356
7474
  "confidence": "certain"
7357
7475
  },
@@ -7486,7 +7604,7 @@
7486
7604
  "surface": "builders-hall",
7487
7605
  "text": "Text size",
7488
7606
  "file": "modules/hall-ui/public/shell.js",
7489
- "line": 309,
7607
+ "line": 317,
7490
7608
  "origin": "js-markup",
7491
7609
  "confidence": "certain"
7492
7610
  },
@@ -8498,6 +8616,15 @@
8498
8616
  "origin": "js-markup",
8499
8617
  "confidence": "certain"
8500
8618
  },
8619
+ {
8620
+ "id": "52e5fe3c6075",
8621
+ "surface": "builders-hall",
8622
+ "text": "Write a Full Idea",
8623
+ "file": "modules/hall-ui/public/ideas.html",
8624
+ "line": 32,
8625
+ "origin": "html-text",
8626
+ "confidence": "certain"
8627
+ },
8501
8628
  {
8502
8629
  "id": "c368b56ecf40",
8503
8630
  "surface": "builders-hall",
@@ -9317,6 +9444,15 @@
9317
9444
  "origin": "js-markup",
9318
9445
  "confidence": "certain"
9319
9446
  },
9447
+ {
9448
+ "id": "7df22e74c87a",
9449
+ "surface": "builders-hall",
9450
+ "text": "idea {…}",
9451
+ "file": "modules/hall-ui/public/ideas.js",
9452
+ "line": 288,
9453
+ "origin": "js-markup",
9454
+ "confidence": "certain"
9455
+ },
9320
9456
  {
9321
9457
  "id": "b2b399fdd6a4",
9322
9458
  "surface": "builders-hall",
@@ -10001,6 +10137,15 @@
10001
10137
  "origin": "html-text",
10002
10138
  "confidence": "certain"
10003
10139
  },
10140
+ {
10141
+ "id": "b1c5eaa20a9d",
10142
+ "surface": "builders-hall",
10143
+ "text": "what the idea is, in one line",
10144
+ "file": "modules/hall-ui/public/ideas.html",
10145
+ "line": 45,
10146
+ "origin": "html-attr:placeholder",
10147
+ "confidence": "certain"
10148
+ },
10004
10149
  {
10005
10150
  "id": "1651cb1876f5",
10006
10151
  "surface": "builders-hall",
package/docs/file-map.md CHANGED
@@ -234,7 +234,7 @@
234
234
  │ ├── terrain.js ← (Phase D + ADR 0012, CJS): TERRAIN + OBJECT enums + BLOCKING_TERRAINS + BLOCKING_OBJECTS (server twin of public/game/world/terrain.js)
235
235
  │ └── map.js ← (Phase D + ADR 0012): two-grid INITIAL_MAP {ground, objects}; isBlocked checks both layers (mirrors public/game/world/{terrain,map}.js)
236
236
  │ (public-status/ moved to modules/status-ui/public/ in BV1.R84 — see the modules/ block above)
237
- │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
237
+ │ (public-builders/ moved to modules/hall-ui/public/ in BV1.R85 — the authenticated builders' hall; see the modules/ block above. It keeps its DEDICATED serving block in src/bongos/serve-internal.js: the builders.<apex> + apex /builders/* shim, canonical redirects, the citizen/Archon page gates, asset-stamping, and the module hall-widget injection — partitioned out of the generic web-surface loop. index.html=the hall dashboard; work/board/roadmap/goals/watch/harbor/gate/pair/settings/ranks/sessions/primer/diagrams/atlas pages; ideas.{html,js,css}=the Full Idea composer (task 1002928 / R152 of goal 1000062 — the five R141 fields with a live R142 completeness meter, both fetched from the server so the page carries no copy of the rules; drafts in localStorage so navigation costs nothing); profile.html/profile.js=the public, shareable builder profile page (/profile?id=N — rank, works, credits, karma, achievements; task 1467); task.html/task.js=the task-detail page (/task/:id, task 751 — replaced the old #/task/N overlay); builders.js=the controller; dom-utils.js/hall-widgets.js=the widget+settings-panel registries.)
238
238
  │
239
239
  ├── infra/ ← ON-BOX scripts (task 1002699): the four reporter/heartbeat crons every dev box runs from its /workspace clone — box-heartbeat.sh (idle-sweep activity signal), box-report-version.sh (HEAD → POST /box/version), box-report-host-keys.sh (SSH host keys → POST /box/host-keys), box-report-terminal.sh (ttyd URL+credential → POST /box/terminal). Delivered by the workspace clone itself (modules/dev-box/box-access.js sparse scope includes infra/); crons self-installed by box-source-fetch.sh's per-tick bring-up. API base resolves GDS_BASE env → session-file api_base → skip (no hardcoded host in the neutral core).
240
240
  ├── devbox-app/ ← OTB Dev Box — Electron menu-bar/tray switch for the builder's box (#904, ADR 0045; internal). SELF-CONTAINED: own package.json (never feeds the droplet's npm ci); built by .github/workflows/build-devbox-app.yml; published to the public releases repo per config/devbox-app.json; release recipe docs/recipes/devbox-app-release.md
@@ -416,8 +416,12 @@ function createClient(opts = {}) {
416
416
  postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
417
417
  // GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
418
418
  getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
419
+ // POST /inbox/score — rank: any-builder — POST /inbox/score
420
+ postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
419
421
  // GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
420
422
  getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
423
+ // GET /inbox/template — rank: any-builder — GET /inbox/template
424
+ getInboxTemplate: (args) => request("GET", "/inbox/template", { hasBody: false }, args),
421
425
  },
422
426
  "instance": {
423
427
  // GET /instance — rank: public — GET /instance
@@ -334,6 +334,102 @@ async function maybeAwardIdeaPromotionBonus(client, taskId, shipperBuilderId, mu
334
334
  };
335
335
  }
336
336
 
337
+ // -------------------------------------------------------------------------
338
+ // The spark-handoff split (task 1002923 / BV1.R147, goal 1000062; ADR 0185)
339
+ //
340
+ // When one builder develops ANOTHER builder's Big Idea Spark into a Full Idea
341
+ // (R146), two people made the idea: the one who had the thought and said "I do
342
+ // not have the full picture", and the one who wrote out the five fields. Both
343
+ // existing ideator streams paid `captured_by` alone, so the developer earned
344
+ // nothing and the R145 queue had no reason to ever be picked up.
345
+ //
346
+ // OWNER DECISION (2026-08-22): 70% to the DEVELOPER, 30% to the spark author,
347
+ // on BOTH existing streams. The developer takes the larger share because
348
+ // developing someone else's spark is the scarce act the queue depends on — at
349
+ // 50/50 it pays strictly worse than authoring your own Full Idea, so nobody
350
+ // picks anything up. The author is not left thin: they keep 30% here AND the
351
+ // full trailing stream-1 share on every task shipped under the idea forever.
352
+ //
353
+ // A POLICY CONSTANT IN THIS FILE, not a table — the ADR 0172 §Consequences rule
354
+ // ("thresholds/factors are policy constants in credits.js", the ADR 0023
355
+ // precedent): it deserves a review and a commit, not a runtime edit.
356
+ const IDEA_DEVELOPER_SHARE = 0.70;
357
+
358
+ // splitIdeaCredit — divide one payout between the two parties.
359
+ //
360
+ // THREE PROPERTIES THIS MUST HAVE, each of which is a real bug if missed:
361
+ //
362
+ // 1. THE TOTAL IS CONSERVED. The developer's share is rounded and the author
363
+ // gets THE REMAINDER — never both rounded independently, which would pay
364
+ // 6 credits out of a 5-credit payout (round(3.5) + round(1.5) = 4 + 2).
365
+ // 2. SELF-DEVELOPMENT COLLAPSES TO ONE PAYEE. R146 deliberately allows an
366
+ // author to develop their own spark. Emitting two rows for the same builder
367
+ // would not double-pay — it would UNDER-pay, because the idempotency
368
+ // predicate is (builder_id, reason) and the second insert is silently
369
+ // refused, so the builder would receive only one of their two shares.
370
+ // 3. ZERO-VALUE ROWS ARE DROPPED. A 1-credit payout splits to 1/0; booking a
371
+ // 0-credit ledger row is noise that reads like a payment.
372
+ //
373
+ // Pure — no DB, no config. Returns [{ builderId, delta, role }], biggest first.
374
+ function splitIdeaCredit(delta, { authorId, developedBy } = {}) {
375
+ const total = Math.round(Number(delta) || 0);
376
+ if (total <= 0 || authorId == null) return [];
377
+ // IDS PASS THROUGH UNCONVERTED. builder_id is a bigint, which pg hands back as
378
+ // a STRING; Number()-ing it would risk precision on a large id and would also
379
+ // change the value type the pre-R147 code fed straight into the INSERT. Only
380
+ // the COMPARISON below normalises, via String() — a number/string mismatch
381
+ // between captured_by and developed_by must not become a phantom second payee.
382
+ if (developedBy == null) {
383
+ return [{ builderId: authorId, delta: total, role: 'author' }];
384
+ }
385
+ if (String(developedBy) === String(authorId)) {
386
+ return [{ builderId: authorId, delta: total, role: 'author_developer' }];
387
+ }
388
+ const developerCut = Math.round(total * IDEA_DEVELOPER_SHARE);
389
+ const authorCut = total - developerCut; // the remainder — conserves the total
390
+ const out = [];
391
+ if (developerCut > 0) out.push({ builderId: developedBy, delta: developerCut, role: 'developer' });
392
+ if (authorCut > 0) out.push({ builderId: authorId, delta: authorCut, role: 'spark_author' });
393
+ return out;
394
+ }
395
+
396
+ // Who are the two parties for this idea? Read from the DB rather than trusted
397
+ // from the caller, deliberately: R146's develop route could not safely pay
398
+ // anything precisely because the recipient was ambiguous at the call site. With
399
+ // the payees derived HERE, a caller cannot direct a payment at the wrong builder,
400
+ // and there is exactly one place that decides who earns from an idea.
401
+ async function ideaCreditParties(client, ideaId) {
402
+ const { rows } = await client.query(
403
+ `SELECT i.captured_by, g.developed_by
404
+ FROM idea_inbox i
405
+ LEFT JOIN ideas_grades g ON g.idea_id = i.id
406
+ WHERE i.id = $1`,
407
+ [ideaId]
408
+ );
409
+ if (rows.length === 0) return null;
410
+ return { authorId: rows[0].captured_by, developedBy: rows[0].developed_by };
411
+ }
412
+
413
+ // Book one split across the ledger, idempotently per (builder, reason). Returns
414
+ // { credited, payees } where credited is the sum of what was NEWLY booked — so a
415
+ // re-run that finds both rows present reports 0, and a partially-booked split
416
+ // (one payee inserted, then a crash) completes on the next call.
417
+ async function bookIdeaSplit(client, { payees, reason, taskId = null, describe }) {
418
+ let credited = 0;
419
+ const booked = [];
420
+ for (const p of payees) {
421
+ const { rowCount } = await client.query(
422
+ `INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
423
+ SELECT $1, $2, $3, $4, $5
424
+ WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $4)`,
425
+ [p.builderId, taskId, p.delta, reason, describe(p)]
426
+ );
427
+ if (rowCount > 0) credited += p.delta;
428
+ booked.push({ ...p, booked: rowCount > 0 });
429
+ }
430
+ return { credited, payees: booked };
431
+ }
432
+
337
433
  // Stream 2 (ADR 0172 / IC2): a Full Idea with a passing completeness score pays
338
434
  // its author IDEA_FULL_PASS_CREDIT, ONCE. Transaction-participant like the rest
339
435
  // of the reward port — the caller passes its `client` (a pool works too, since
@@ -362,29 +458,46 @@ async function awardIdeaFullPassCredit(client, { ideaId, authorId, completenessS
362
458
  if (completenessScore == null || !Number.isFinite(Number(completenessScore)) || Number(completenessScore) < IDEA_FULL_PASS_BAR) {
363
459
  return { credited: 0, reason: 'below_bar' };
364
460
  }
365
- if (authorId == null || ideaId == null) {
461
+ if (ideaId == null) {
366
462
  return { credited: 0, reason: 'missing_author_or_idea' };
367
463
  }
464
+ // R147: the PARTIES come from the DB, not the call site. `authorId` is kept as
465
+ // a fallback only for a caller holding an idea the lookup cannot see (and for
466
+ // the pre-R147 call shape); when the row is there, it wins.
467
+ const parties = await ideaCreditParties(client, ideaId);
468
+ const resolvedAuthor = (parties && parties.authorId != null) ? parties.authorId : authorId;
469
+ if (resolvedAuthor == null) {
470
+ return { credited: 0, reason: 'missing_author_or_idea' };
471
+ }
472
+ const payees = splitIdeaCredit(IDEA_FULL_PASS_CREDIT, {
473
+ authorId: resolvedAuthor,
474
+ developedBy: parties ? parties.developedBy : null,
475
+ });
476
+ if (payees.length === 0) {
477
+ return { credited: 0, reason: 'zero_after_scaling' };
478
+ }
479
+ // Idempotency predicate is `builder_id = payee AND reason = <per-idea key>`.
480
+ // The `builder_id` term was originally an index optimisation (hitting
481
+ // `idx_credit_log_builder` instead of scanning a growing credit_log — the
482
+ // R143/IC2 grader finding), and R147 makes it load-bearing for CORRECTNESS
483
+ // too: a split books one row per payee under the SAME reason, so the predicate
484
+ // has to be per-builder or the second payee would be refused. That is also
485
+ // exactly why splitIdeaCredit collapses a self-developed idea to a single
486
+ // payee — two rows for one builder would be silently under-paid, not
487
+ // double-paid. credit_log is a CORE table, so a module migration cannot add an
488
+ // index on `reason` anyway (the additive-namespace rule).
368
489
  const reason = `idea.credit.full_pass:${ideaId}`;
369
- const description = `Full Idea #${ideaId} passed (completeness ${Math.round(Number(completenessScore))}/100) → ${IDEA_FULL_PASS_CREDIT}c to its author (ideator lane, ADR 0172)`;
370
- // Idempotency predicate is `builder_id = author AND reason = <per-idea key>`.
371
- // The `builder_id` term is NOT for correctness — the reason string is already
372
- // globally unique per idea — it exists to hit the existing
373
- // `idx_credit_log_builder (builder_id, recorded_at)` index: the NOT EXISTS
374
- // narrows to the author's handful of credit rows (index scan) instead of a
375
- // sequential scan of a credit_log that grows with total platform activity.
376
- // credit_log is a CORE table, so a module migration cannot add an index on
377
- // `reason` (the additive-namespace rule) — reusing the builder index is the
378
- // in-scope fix. (R143/IC2 grader finding.)
379
- const { rowCount } = await client.query(
380
- `INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
381
- SELECT $1, NULL, $2, $3, $4
382
- WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $3)`,
383
- [authorId, IDEA_FULL_PASS_CREDIT, reason, description]
384
- );
385
- return rowCount > 0
386
- ? { credited: IDEA_FULL_PASS_CREDIT, reason: 'paid' }
387
- : { credited: 0, reason: 'already_paid' };
490
+ const pct = Math.round(Number(completenessScore));
491
+ const { credited, payees: booked } = await bookIdeaSplit(client, {
492
+ payees,
493
+ reason,
494
+ describe: (p) => `Full Idea #${ideaId} passed (completeness ${pct}/100) → ${p.delta}c to its ${p.role.replace(/_/g, ' ')} (ideator lane, ADR 0172${p.role === 'developer' || p.role === 'spark_author' ? '; split ADR 0185' : ''})`,
495
+ });
496
+ return {
497
+ credited,
498
+ reason: credited > 0 ? 'paid' : 'already_paid',
499
+ payees: booked,
500
+ };
388
501
  }
389
502
 
390
503
  // Stream 1 (ADR 0172 / goal 1000067 IC4): as each task descended from an idea
@@ -420,7 +533,8 @@ async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig()
420
533
  `SELECT l.idea_id,
421
534
  i.captured_by,
422
535
  COALESCE(g.grade, 'quick') AS grade,
423
- g.completeness_score AS completeness
536
+ g.completeness_score AS completeness,
537
+ g.developed_by AS developed_by
424
538
  FROM ideas_task_lineage l
425
539
  JOIN idea_inbox i ON i.id = l.idea_id
426
540
  LEFT JOIN ideas_grades g ON g.idea_id = l.idea_id
@@ -430,7 +544,7 @@ async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig()
430
544
  if (rows.length === 0 || rows[0].captured_by == null) {
431
545
  return { credited: 0, reason: 'no_source_idea', ideaId: null, authorId: null };
432
546
  }
433
- const { idea_id: ideaId, captured_by: authorId, grade, completeness } = rows[0];
547
+ const { idea_id: ideaId, captured_by: authorId, grade, completeness, developed_by: developedBy } = rows[0];
434
548
  // Grade factor: a Full Idea earns in proportion to its completeness; a Quick
435
549
  // or Spark idea (no completeness score) earns the flat quick factor. A full
436
550
  // idea with no score yet also falls back to the quick factor rather than 0 —
@@ -442,16 +556,24 @@ async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig()
442
556
  if (delta <= 0) {
443
557
  return { credited: 0, reason: 'zero_after_scaling', ideaId: Number(ideaId), authorId: Number(authorId) };
444
558
  }
559
+ // R147 (ADR 0185): if someone else developed this spark, the per-task stream is
560
+ // split with them too — the same 70/30 rule as stream 2, so there is one rule
561
+ // to explain rather than two. A developer who shaped the idea shares in what
562
+ // the idea goes on to deliver.
563
+ const payees = splitIdeaCredit(delta, { authorId, developedBy });
564
+ if (payees.length === 0) {
565
+ return { credited: 0, reason: 'zero_after_scaling', ideaId: Number(ideaId), authorId: Number(authorId) };
566
+ }
445
567
  const reason = `idea.credit.task_ship:${ideaId}:${taskId}`;
446
- const description = `Task #${taskId} shipped under idea #${ideaId} (${grade}${completeness != null ? ` ${Math.round(Number(completeness))}%` : ''}) → ${delta}c to its author (ideator lane stream 1, ADR 0172)`;
447
- const { rowCount } = await client.query(
448
- `INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
449
- SELECT $1, $2, $3, $4, $5
450
- WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $4)`,
451
- [authorId, taskId, delta, reason, description]
452
- );
453
- return rowCount > 0
454
- ? { credited: delta, reason: 'paid', ideaId: Number(ideaId), authorId: Number(authorId) }
568
+ const gradeLabel = `${grade}${completeness != null ? ` ${Math.round(Number(completeness))}%` : ''}`;
569
+ const { credited, payees: booked } = await bookIdeaSplit(client, {
570
+ payees,
571
+ reason,
572
+ taskId,
573
+ describe: (p) => `Task #${taskId} shipped under idea #${ideaId} (${gradeLabel}) → ${p.delta}c to its ${p.role.replace(/_/g, ' ')} (ideator lane stream 1, ADR 0172${p.role === 'developer' || p.role === 'spark_author' ? '; split ADR 0185' : ''})`,
574
+ });
575
+ return credited > 0
576
+ ? { credited, reason: 'paid', ideaId: Number(ideaId), authorId: Number(authorId), payees: booked }
455
577
  : { credited: 0, reason: 'already_paid', ideaId: Number(ideaId), authorId: Number(authorId) };
456
578
  }
457
579
 
@@ -667,6 +789,9 @@ module.exports = {
667
789
  maybeAwardIdeaPromotionBonus,
668
790
  awardIdeaFullPassCredit,
669
791
  awardIdeaTaskShipCredit,
792
+ // the spark-handoff split (task 1002923 / BV1.R147, ADR 0185)
793
+ splitIdeaCredit,
794
+ IDEA_DEVELOPER_SHARE,
670
795
  IDEA_FULL_PASS_CREDIT,
671
796
  IDEA_FULL_PASS_BAR,
672
797
  IDEA_TASK_SHIP_BASE,