@bongos/core 1.19.903 → 1.19.905

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.
@@ -0,0 +1,88 @@
1
+ // modules/ideas/trail-size.js — SIZE on the idea trail: what each thought's
2
+ // work has actually earned (task 1004093, BV2.R12, goal 1000095; spec
3
+ // docs/specs/bongos-v2-ideator-surface.md decisions 4 + 5).
4
+ //
5
+ // The trail already answers "what became of my idea" (listIdeaTrailForBuilder)
6
+ // and, since BV2.R09, "and what did THAT go on to cause" (trail-descent.js).
7
+ // Size is the third column of the same sentence and the only one denominated in
8
+ // money: the sum of every stream-1 payout the descent has produced.
9
+ //
10
+ // WHY IT IS A SEPARATE FILE, like trail-descent.js beside it. inbox.js is AT the
11
+ // 1500-line size ratchet (scripts/gds/fitness-ratchets.js), so a line added
12
+ // there fails the build. And this is a different read again: the trail is
13
+ // idea → task, the descent is a task → task walk, and size is a read of the
14
+ // LEDGER keyed by idea. Three relations, three files.
15
+ //
16
+ // WHY IT GOES THROUGH THE `reward` PORT AND NOT A SQL READ OF ITS OWN.
17
+ // credit_log is economy's table and modules may not require each other
18
+ // (ADR 0083 rule 1). The read is small enough that a copy here would be
19
+ // tempting — and that is exactly the trap: the reason key `idea.credit.
20
+ // task_ship:<idea>:<task>` is economy's idempotency predicate, economy is free
21
+ // to change it, and a second reader of it in this module would silently start
22
+ // summing nothing the day it did. `ideas` already declares `reward` in its
23
+ // module.json `consumes`, so this adds no new dependency edge.
24
+ //
25
+ // RESOLVED OPTIONALLY EVEN SO. `consumes` means the instance will not boot
26
+ // without the port, so in production the resolve cannot fail — but tests rebuild
27
+ // the GDS router with an arbitrary subset of modules, and a trail read that
28
+ // throws because economy was not mounted is a worse failure than a trail read
29
+ // with no money on it. When the port is absent every idea gets `size: null`,
30
+ // NOT zero: zero is a real answer meaning "this thought has earned nothing yet",
31
+ // and rendering it for "we could not ask" would tell an ideator their work paid
32
+ // nothing. tests/idea_size.mjs drives both branches.
33
+
34
+ const api = require('../../src/module-api');
35
+
36
+ // attachSizeToTrail — mutates each idea in place with `size`, and returns the
37
+ // list, mirroring attachDescentToTrail's shape so the route composes the two the
38
+ // same way.
39
+ //
40
+ // `size` is `{ credits, payments, scale }` — the TRUE figure the panel prints,
41
+ // how many payouts made it up, and the compressed magnitude a drawing uses. Both
42
+ // are carried because the spec asks for both: compressed for drawing, true
43
+ // figure available (decision 4). The compression is economy's, applied once at
44
+ // the read, so nothing downstream invents a second curve.
45
+ //
46
+ // ONE query for the whole page, not one per idea — the same property
47
+ // trail-descent.js pins, for the same reason: this rides a paged trail read and
48
+ // a per-idea round trip would make a long trail quadratic in round trips.
49
+ async function attachSizeToTrail(ideas, { client = null } = {}) {
50
+ const list = Array.isArray(ideas) ? ideas : [];
51
+ if (list.length === 0) return list;
52
+
53
+ const reward = api.resolveOptional && api.resolveOptional('reward');
54
+ if (!reward || typeof reward.ideaSizes !== 'function' || typeof reward.descentRowsFromTrail !== 'function') {
55
+ for (const idea of list) idea.size = null;
56
+ return list;
57
+ }
58
+
59
+ // The ids are asked for as the idea's OWN id, never the id it merged into.
60
+ // The descent a merged idea SHOWS is inherited (trail-descent.js followMerges,
61
+ // ADR 0311 — nothing is lost), but the money it EARNED is its own: stream 1
62
+ // books against the idea whose lineage row named the shipping task, and
63
+ // crediting A's star with B's payouts would count B's work twice on one page.
64
+ // That asymmetry is the same one the payout itself draws (spec decision 7):
65
+ // the descent displays more than the number counts.
66
+ // THE DESCENT IS ALREADY IN HAND, so the historical half costs nothing
67
+ // (task 1004097 / BV2.R18). attachDescentToTrail ran first — the route
68
+ // composes them in that order — so every idea on this page already carries its
69
+ // own tasks and everything below them, each with a status and a shortest
70
+ // depth. Handing that to economy is the difference between one extra walk per
71
+ // page and none at all; economy falls back to walking only for a caller that
72
+ // has no descent, which is a single-idea panel and never this one.
73
+ //
74
+ // The SHAPE is economy's, not this module's: `descentRowsFromTrail` lives with
75
+ // the pricing that consumes it, so a change to what the pricing needs cannot
76
+ // leave a hand-built projection here quietly dropping a field.
77
+ const descentByIdea = new Map();
78
+ for (const idea of list) {
79
+ descentByIdea.set(String(idea.id), reward.descentRowsFromTrail(idea));
80
+ }
81
+ const sizes = await reward.ideaSizes(list.map((i) => i.id), { client, descentByIdea });
82
+ for (const idea of list) {
83
+ idea.size = sizes.get(String(idea.id)) || { credits: 0, payments: 0, scale: 0 };
84
+ }
85
+ return list;
86
+ }
87
+
88
+ module.exports = { attachSizeToTrail };
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.903",
3
+ "version": "1.19.905",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.903",
9
+ "version": "1.19.905",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.903",
3
+ "version": "1.19.905",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.903'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.905'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -421,6 +421,50 @@ test('the SIDEBAR badge: the Collab item declares the help_requests count', () =
421
421
  assert.match(fn, /href: '\/collab'/, 'pointing at the page, not the retired hash view');
422
422
  });
423
423
 
424
+ // ---- THE MINGLE ROW SPEAKS THE ROLE ---------------------------------------
425
+ // task 1003967 (artist review of 1003960). Run, not grepped: the first version of this
426
+ // guard matched identifier names in the source, which would have agreed with a swapped
427
+ // article just as happily as with a correct one. The sentence is pure, so execute it.
428
+
429
+ test('the mingle sub-line reads as English for every pair, and never shows the column', () => {
430
+ const L = lib();
431
+ const OTB = domUtils();
432
+ const word = (d) => (OTB.roleIsUnset(d) ? '' : OTB.roleWord(d));
433
+ const line = (a, b) => L.pairSentence(word(a), word(b));
434
+
435
+ assert.equal(line('engineer', 'artist'), 'An engineer — you are an artist. Mingle pairs across roles.');
436
+ assert.equal(line('artist', 'ideator'), 'An artist — you are an ideator. Mingle pairs across roles.');
437
+
438
+ // 'ui' is the acronym exception roleWord() already makes, and it is why the article
439
+ // cannot be a plain vowel test — "an UI" is how that shortcut fails.
440
+ assert.equal(line('ui', 'engineer'), 'A UI — you are an engineer. Mingle pairs across roles.');
441
+ assert.equal(line('engineer', 'ui'), 'An engineer — you are a UI. Mingle pairs across roles.');
442
+ });
443
+
444
+ test('a pair with an unset role drops the comparison rather than naming the default', () => {
445
+ const L = lib();
446
+ const OTB = domUtils();
447
+ const word = (d) => (OTB.roleIsUnset(d) ? '' : OTB.roleWord(d));
448
+
449
+ // The defect this replaced rendered the database's own word — "A unclassified — you are
450
+ // a engineer" — which is the example V2 names. Nothing a person did not choose may reach
451
+ // the sentence, whichever side is missing.
452
+ for (const pair of [[OTB.ROLE_UNSET, 'engineer'], ['engineer', OTB.ROLE_UNSET], [null, 'artist'], ['', '']]) {
453
+ const out = L.pairSentence(word(pair[0]), word(pair[1]));
454
+ assert.equal(out, 'Mingle pairs across roles.', `pair ${JSON.stringify(pair)}`);
455
+ assert.doesNotMatch(out, /unclassified/);
456
+ }
457
+ });
458
+
459
+ test('aOrAn is the article rule, stated once', () => {
460
+ const L = lib();
461
+ assert.equal(L.aOrAn('engineer'), 'an');
462
+ assert.equal(L.aOrAn('artist'), 'an');
463
+ assert.equal(L.aOrAn('ideator'), 'an');
464
+ assert.equal(L.aOrAn('UI'), 'a', 'the acronym is spoken "you-eye", so it takes "a"');
465
+ assert.equal(L.aOrAn('ui'), 'a');
466
+ });
467
+
424
468
  // ---- THE ANSWERABLE CLAUSE ------------------------------------------------
425
469
  test('the asker can settle in place, with both outcomes', () => {
426
470
  const js = read('modules', 'hall-ui', 'public', 'collab.js');