@bongos/core 1.20.51 → 1.20.53

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.
Files changed (42) hide show
  1. package/.bongos-core.json +144 -24
  2. package/docs/copy-inventory.md +19 -18
  3. package/docs/copy-registry.json +28 -19
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/page-readings.json +6 -6
  6. package/migrations/core_267_page_tweak_author_of_record.sql +41 -0
  7. package/modules/hall-ui/public/builders.js +3 -1
  8. package/modules/lifecycle/db-analytics.js +6 -1
  9. package/modules/lifecycle/db-ship.js +10 -3
  10. package/modules/lifecycle/page-tweak-artist.js +86 -0
  11. package/modules/lifecycle/page-tweak-reads.js +7 -5
  12. package/modules/lifecycle/routes/tasks.js +3 -0
  13. package/modules/provisioning/soft-limits.js +17 -0
  14. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea-cut.webp +0 -0
  15. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea-cut.webp.json +16 -0
  16. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  17. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +16 -0
  18. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  19. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +16 -0
  20. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  21. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +16 -0
  22. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  23. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +16 -0
  24. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  25. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +16 -0
  26. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  27. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +16 -0
  28. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  29. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +16 -0
  30. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  31. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +16 -0
  32. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  33. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +16 -0
  34. package/modules/sessions/db.js +8 -0
  35. package/modules/status-ui/public/status.js +3 -1
  36. package/package-lock.json +2 -2
  37. package/package.json +1 -1
  38. package/release-notes.json +16 -0
  39. package/src/module-api.js +1 -1
  40. package/tests/genesis_life_plates.mjs +63 -0
  41. package/tests/page_tweak_artist.mjs +236 -0
  42. package/tests/provisioning_planet_physics.mjs +14 -0
@@ -2759,5 +2759,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2759
2759
  landed since 1.20.49 with no explicit bump. run 36815399738. (task 1002620)
2760
2760
  1.20.51 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2761
2761
  landed since 1.20.50 with no explicit bump. run 36816320939. (task 1002620)
2762
+ 1.20.52 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2763
+ landed since 1.20.51 with no explicit bump. run 36817286385. (task 1002620)
2764
+ 1.20.53 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2765
+ landed since 1.20.52 with no explicit bump. run 36818406554. (task 1002620)
2762
2766
  ---------------------------------------------------------------------------
2763
2767
  ```
@@ -1353,7 +1353,7 @@
1353
1353
  "surface": "builders",
1354
1354
  "title": "Builders' Hall",
1355
1355
  "states_read": ["home","standing","standing-karma","out"],
1356
- "files_hash": "2a773a6907072f93",
1356
+ "files_hash": "959a729d1d7eba2b",
1357
1357
  "reading_hash": "1debe0f979ce3b59",
1358
1358
  "counts": {"lines":204,"placed":68,"shared":7,"unplaced":129},
1359
1359
  "omitted": {"data":182},
@@ -1398,9 +1398,9 @@
1398
1398
  {"key":"L0037","section":"From the system","text":"From the system","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":707,"string_id":"d32c5e058d1e"},
1399
1399
  {"key":"L0038","section":"From the system","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":2},
1400
1400
  {"key":"L0039","section":"From the system","text":"Open {…}","placement":"placed","file":"modules/hall-ui/public/goals.js","line":67,"string_id":"f6f68ba322be"},
1401
- {"key":"L0040","section":"Ideas awaiting triage 151","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":895,"string_id":"e181637c5a5e"},
1401
+ {"key":"L0040","section":"Ideas awaiting triage 151","text":"Ideas awaiting triage","placement":"placed","file":"modules/hall-ui/public/builders.js","line":897,"string_id":"e181637c5a5e"},
1402
1402
  {"key":"L0041","section":"Ideas awaiting triage 151","text":"Triage","placement":"unplaced","occurrences":5},
1403
- {"key":"L0042","section":"Ideas awaiting triage 151","text":"…{…} more open ideas — triage clears them one at a time.","placement":"placed","file":"modules/hall-ui/public/builders.js","line":893,"string_id":"e35f11bdc5d7"},
1403
+ {"key":"L0042","section":"Ideas awaiting triage 151","text":"…{…} more open ideas — triage clears them one at a time.","placement":"placed","file":"modules/hall-ui/public/builders.js","line":895,"string_id":"e35f11bdc5d7"},
1404
1404
  {"key":"L0043","section":"Was this removed? 2","text":"Was this removed?","placement":"unplaced"},
1405
1405
  {"key":"L0044","section":"Was this removed? 2","text":"{…}{…} — open it to see what went","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":374,"string_id":"7f79bedecf60","occurrences":2},
1406
1406
  {"key":"L0045","section":"Was this removed? 2","text":"Confirm","placement":"unplaced","occurrences":2},
@@ -4318,7 +4318,7 @@
4318
4318
  "surface": "status",
4319
4319
  "title": "{{worldName}} — the public ledger",
4320
4320
  "states_read": ["default"],
4321
- "files_hash": "49c104486ef09ecf",
4321
+ "files_hash": "35acac6a3253f014",
4322
4322
  "reading_hash": "2853e52b66ba076c",
4323
4323
  "counts": {"lines":87,"placed":49,"shared":14,"unplaced":24},
4324
4324
  "omitted": {"data":89},
@@ -4327,7 +4327,7 @@
4327
4327
  {"key":"L0001","section":"top bar","text":"The Public Ledger of the City's Making","placement":"shared","file":"modules/status-ui/public/index.html","line":125,"string_id":"acdf97805931"},
4328
4328
  {"key":"L0002","section":"top bar","text":"A shipping container has washed ashore upon the coast south of Athens. About it a new civilization rises. Set out below: the city's affairs, openly recorded.","placement":"shared","file":"modules/status-ui/public/index.html","line":126,"string_id":"f4a664041da5"},
4329
4329
  {"key":"L0003","section":"top bar","text":"Cross into the city →","placement":"shared","file":"modules/status-ui/public/index.html","line":128,"string_id":"3aa071ef1142"},
4330
- {"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1167,"string_id":"c1e3c2212207"},
4330
+ {"key":"L0004","section":"top bar","text":"tablet last consulted at {…}","placement":"shared","file":"modules/status-ui/public/status.js","line":1169,"string_id":"c1e3c2212207"},
4331
4331
  {"key":"L0005","section":"navigation","text":"Pulse","placement":"shared"},
4332
4332
  {"key":"L0006","section":"navigation","text":"Watch","placement":"shared"},
4333
4333
  {"key":"L0007","section":"navigation","text":"Architects","placement":"shared"},
@@ -4403,7 +4403,7 @@
4403
4403
  {"key":"L0077","section":"Lately Inscribed","text":"inscribed by {…} · {…}","placement":"unplaced","occurrences":6},
4404
4404
  {"key":"L0078","section":"Lately Inscribed","text":"The site now has the three pages every real website needs — privacy, terms and a contact page — linked from the footer of the front page and the projects hub. The privacy page says plainly what is kept about a GitHub account and a project, who else sees it, and how long it stays; the contact page points at the two doors that actually exist, the repository and the builders' hall. Every project built on the platform gets the same pages under its own name, with the claims that are only true of the platform itself removed.","placement":"unplaced"},
4405
4405
  {"key":"L0079","section":"Lately Inscribed","text":"‹ earlier page","placement":"placed","file":"modules/status-ui/public/index.html","line":321,"string_id":"6d0d64f10bd9"},
4406
- {"key":"L0080","section":"Lately Inscribed","text":"page {…} of {…}","placement":"placed","file":"modules/status-ui/public/status.js","line":1010,"string_id":"4a00bdbfd9be"},
4406
+ {"key":"L0080","section":"Lately Inscribed","text":"page {…} of {…}","placement":"placed","file":"modules/status-ui/public/status.js","line":1012,"string_id":"4a00bdbfd9be"},
4407
4407
  {"key":"L0081","section":"Lately Inscribed","text":"later page ›","placement":"placed","file":"modules/status-ui/public/index.html","line":323,"string_id":"9b019d632d3e"},
4408
4408
  {"key":"L0082","section":"The World","text":"The World","placement":"placed","file":"modules/status-ui/public/index.html","line":331,"string_id":"3e465b62cea8"},
4409
4409
  {"key":"L0083","section":"The World","text":"A true-to-life portrait of {{worldName}} as she stands today, rendered from the city's own stones.","placement":"placed","file":"modules/status-ui/public/index.html","line":332,"string_id":"623d6b7ae6b7"},
@@ -0,0 +1,41 @@
1
+ -- core_267 — an approved page tweak already shipped names its artist (task 1004432,
2
+ -- WA6.RP02, goal 1000095)
3
+ --
4
+ -- From this release, shipTask (modules/lifecycle/db-ship.js) writes tasks.shipped_by
5
+ -- as the round's ARTIST when a page tweak ships, instead of the session that applied
6
+ -- it. Everything that counts or lists shipped work reads shipped_by (the profile,
7
+ -- the shipped count, the activity log, the status page's recent list, streaks,
8
+ -- achievements, the hub rollup), so that one write moves every one of them.
9
+ --
10
+ -- The rounds that shipped BEFORE this release still name their applier, so on every
11
+ -- one of those surfaces the artist who wrote the page is absent and the applier is
12
+ -- credited with work that was not theirs. This brings them in line, once.
13
+ --
14
+ -- The artist is page-tweak-artist.js's rule, spelled out here because a migration
15
+ -- cannot call it: the builder of the round's LATEST WEB claim (worktree_name and
16
+ -- creator_session_id both NULL), the same person economy's awardArtistPageCredit
17
+ -- already paid. A round with no web claim keeps its applier, exactly as the ship
18
+ -- path does. tests/page_tweak_artist.mjs holds this predicate to the helper's.
19
+ --
20
+ -- Data only, no column or constraint: forward-safe against a live database, and an
21
+ -- older release reading the rows just sees a different builder id. Idempotent: a
22
+ -- second run matches nothing, because the IS DISTINCT FROM guard skips rows that
23
+ -- already name their artist. Money is untouched: credit_log is not read or written,
24
+ -- and the artist was paid at the land.
25
+
26
+ UPDATE tasks t
27
+ SET shipped_by = w.builder_id
28
+ FROM (
29
+ SELECT DISTINCT ON (c.task_id) c.task_id, c.builder_id
30
+ FROM claims c
31
+ JOIN tasks pt ON pt.id = c.task_id
32
+ WHERE pt.source = 'page-tweak'
33
+ AND pt.status = 'shipped'
34
+ AND c.worktree_name IS NULL AND c.creator_session_id IS NULL
35
+ ORDER BY c.task_id, c.claimed_at DESC, c.id DESC
36
+ ) w
37
+ WHERE t.id = w.task_id
38
+ AND t.shipped_by IS DISTINCT FROM w.builder_id;
39
+
40
+ INSERT INTO schema_migrations (version) VALUES ('core_267_page_tweak_author_of_record')
41
+ ON CONFLICT DO NOTHING;
@@ -762,7 +762,9 @@
762
762
  if (!tbody) return;
763
763
  const slice = window.OTBKit.paginate(acclaimState.tasks, acclaimPager);
764
764
  tbody.innerHTML = slice.map((t) => {
765
- const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`));
765
+ // task 1004432: an approved page is its artist's; the applying session rides along as a helper.
766
+ const helperName = t.helper_display_name || (t.helper_login ? `@${t.helper_login}` : '');
767
+ const by = escapeHtml(t.display_name || (t.github_login ? `@${t.github_login}` : `builder #${t.shipped_by}`)) + (helperName ? ` <span class="acclaim__evidence">with help from ${escapeHtml(helperName)}</span>` : '');
766
768
  const net = Number(t.net_votes) || 0;
767
769
  const netCls = signClasses(net, 'acclaim__net');
768
770
  const netText = net > 0 ? `+${net}` : `${net}`;
@@ -14,6 +14,7 @@ const api = require('../../src/module-api');
14
14
  const { pool, withTx } = api;
15
15
  const { SMOKE_NOT_LIKE, gradePassCount, gradeQualityWhere, median } = require('./db-shared.js');
16
16
  const { CREDITS_AWARDED_SUBQUERY } = require('./db-tasks.js');
17
+ const { helperJoinSql } = require('./page-tweak-artist.js');
17
18
 
18
19
  // -------------------------------------------------------------------------
19
20
  // Cost log + summary carved into modules/economy/cost.js (BV1.R77 / ADR 0093
@@ -47,9 +48,13 @@ async function recentShippedTasks(limit = 30, { days = null } = {}) {
47
48
  CASE WHEN t.security_sensitive THEN NULL ELSE t.visual_url END AS visual_url, CASE WHEN t.security_sensitive THEN NULL ELSE t.visual_alt END AS visual_alt,
48
49
  ${CREDITS_AWARDED_SUBQUERY} AS credits_awarded,
49
50
  t.est_minutes, t.actual_minutes,
50
- b.github_login, b.display_name
51
+ b.github_login, b.display_name,
52
+ -- task 1004432: an approved page is its artist's (shipped_by); the session
53
+ -- that applied it rides along as a helper, so a reader still sees both.
54
+ helper.helper_login, helper.helper_display_name
51
55
  FROM tasks t
52
56
  LEFT JOIN builders b ON b.id = t.shipped_by
57
+ ${helperJoinSql('t')}
53
58
  WHERE ${whereClause}
54
59
  ORDER BY t.shipped_at DESC NULLS LAST
55
60
  LIMIT $1`,
@@ -19,6 +19,7 @@ const { THETES_GRADUATION_THRESHOLD, maybeAutoGraduateToThetes, foldWorktreeName
19
19
  const { mostRecentClaimHolderId, sealSessionLog, staleClaimHourPredicate } = require('./db-shared.js');
20
20
  const { LIVE_STATUSES } = require('./dead-deps.js');
21
21
  const { isPageTweak } = require('./page-tweak-hold.js');
22
+ const pageTweakArtist = require('./page-tweak-artist.js');
22
23
 
23
24
  // shouldRefuseClaimResolve — may THIS caller resolve THIS claim? (task 1003767)
24
25
  //
@@ -512,6 +513,9 @@ async function needsRebaseTasksForBuilder(builderId, client = pool) {
512
513
  // The ship gate (task 1002483, ADR 0184) — see ./ship-assurance for the why.
513
514
  const { normalizeShipAssurance, assertShipAssurance, recordShipAssurance, SHIP_ASSURANCE_KINDS } = require('./ship-assurance');
514
515
 
516
+ // The author of record (task 1004432): a page tweak names its ARTIST at this flip only.
517
+ const SHIPPED_BY_SQL = pageTweakArtist.shippedBySql('$2');
518
+
515
519
  async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
516
520
  const shipAssuranceValue = assertShipAssurance(landAssurance); // FAIL-CLOSED, ADR 0184
517
521
  // #538 test seam (mirrors confirmTask): a caller-supplied pool is honored ONLY
@@ -523,7 +527,7 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
523
527
  `UPDATE tasks
524
528
  SET status = 'shipped',
525
529
  shipped_at = now(),
526
- shipped_by = COALESCE(shipped_by, $2),
530
+ shipped_by = ${SHIPPED_BY_SQL},
527
531
  needs_rebase = false,
528
532
  needs_rebase_reason = NULL,
529
533
  needs_rebase_at = NULL,
@@ -642,10 +646,13 @@ async function shipTask({ taskId, builderId, landAssurance }, deps = {}) {
642
646
  // credit it keys off (breaking-the-100c-bar etc.) now lands here.
643
647
  let justUnlocked = [];
644
648
  const rewardPort = seams.resolveOptional('reward');
645
- if (rewardPort && rewardPort.reconcileBuilderAchievements && creditedBuilderId != null) {
649
+ // A page tweak's ship count and credit moved to its artist (above), so the
650
+ // artist is whose achievements this ship can unlock; the applier's did not move.
651
+ const achievementBuilderId = isPageTweak(rows[0]) ? actualShipperId : creditedBuilderId;
652
+ if (rewardPort && rewardPort.reconcileBuilderAchievements && achievementBuilderId != null) {
646
653
  await client.query('SAVEPOINT ach_reconcile_ship');
647
654
  try {
648
- justUnlocked = await rewardPort.reconcileBuilderAchievements(client, creditedBuilderId, { taskId });
655
+ justUnlocked = await rewardPort.reconcileBuilderAchievements(client, achievementBuilderId, { taskId });
649
656
  await client.query('RELEASE SAVEPOINT ach_reconcile_ship');
650
657
  } catch (_) {
651
658
  await client.query('ROLLBACK TO SAVEPOINT ach_reconcile_ship');
@@ -0,0 +1,86 @@
1
+ // modules/lifecycle/page-tweak-artist.js — WHO a page-tweak round belongs to,
2
+ // written once (task 1004432 / WA6.RP02, ADR 0341 D10).
3
+ //
4
+ // A round has two people. The ARTIST wrote the words in the studio and holds the
5
+ // round's WEB claim (no worktree, no creating session: the claim the submit
6
+ // released). The APPLIER is the session that put the words into the page, on an
7
+ // ordinary CLI claim in a worktree. The artist is the author of record: they are
8
+ // the one paid (economy's awardArtistPageCredit), the one the changelog names
9
+ // (page-tweak-reads.js), and, since this task, the one `tasks.shipped_by` names
10
+ // once the round ships. The applier is a helper.
11
+ //
12
+ // Pure: SQL fragments and nothing else, so every reader takes the rule from here
13
+ // instead of re-typing it. Economy cannot require this file (a module reaches
14
+ // another only through a port), so credits.js keeps its own copy of the
15
+ // predicate and tests/page_tweak_artist.mjs holds the two strings together.
16
+
17
+ 'use strict';
18
+
19
+ const PAGE_TWEAK_SOURCE = 'page-tweak';
20
+
21
+ // A WEB claim: the artist's. Neither a worktree (a CLI applier) nor a creating
22
+ // session (an agent-made claim) is on it.
23
+ function webClaimPredicate(alias = 'c') {
24
+ return `${alias}.worktree_name IS NULL AND ${alias}.creator_session_id IS NULL`;
25
+ }
26
+
27
+ // The newest claim first; the id breaks a same-instant tie the same way in
28
+ // every reader.
29
+ function latestFirst(alias = 'c') {
30
+ return `${alias}.claimed_at DESC, ${alias}.id DESC`;
31
+ }
32
+
33
+ // The round's artist: the builder of its latest web claim, as a scalar
34
+ // subquery over `taskIdExpr` (a column or a bind parameter). NULL when the
35
+ // round never had a web claim, which a caller treats as "no artist".
36
+ function artistIdSql(taskIdExpr) {
37
+ return `(SELECT c.builder_id FROM claims c
38
+ WHERE c.task_id = ${taskIdExpr} AND ${webClaimPredicate('c')}
39
+ ORDER BY ${latestFirst('c')}
40
+ LIMIT 1)`;
41
+ }
42
+
43
+ // THE AUTHOR OF RECORD, as shipTask writes it (db-ship.js). Everywhere shipped
44
+ // work is counted or listed reads tasks.shipped_by: the profile, the shipped
45
+ // count, the activity log, the status page's recent list, streaks,
46
+ // achievements, the hub rollup. For an ordinary task that is the claim holder,
47
+ // set at confirm, and this keeps it (COALESCE with `fallbackParam`, the caller's
48
+ // builder). For a page tweak it is the ARTIST, written at the SHIPPED flip and
49
+ // nowhere earlier: until a round ships, shipped_by still names the applier,
50
+ // because the rebase gate (needsRebaseTasksForBuilder, status='confirmed') must
51
+ // refuse the session that can actually rebase, never an artist with no
52
+ // worktree. A round with no web claim keeps its applier. Used inside
53
+ // `UPDATE tasks`, so the row is `tasks`.
54
+ function shippedBySql(fallbackParam) {
55
+ return `CASE WHEN source = '${PAGE_TWEAK_SOURCE}'
56
+ THEN COALESCE(${artistIdSql('tasks.id')}, shipped_by, ${fallbackParam})
57
+ ELSE COALESCE(shipped_by, ${fallbackParam}) END`;
58
+ }
59
+
60
+ // The helper beside a shipped row, for a list that shows people. The helper is
61
+ // the round's APPLIER, the builder of its latest WORKTREE claim: display only
62
+ // ("with help from"), never attribution or pay. A LEFT JOIN
63
+ // LATERAL yielding helper_login and helper_display_name on a page tweak whose
64
+ // applier is someone other than the author of record, and nothing otherwise
65
+ // (an ordinary task, a self-applied round, a round with no worktree claim).
66
+ // `alias` is the tasks alias of the surrounding query.
67
+ //
68
+ // THE COST, since it runs once per row of a feed. The source test sits inside
69
+ // the claims scan and names only the outer row, so Postgres plans it as a
70
+ // one-time filter: an ordinary task reads no claim at all. A page tweak's lookup
71
+ // is idx_claims_task_all (core_263, claims (task_id)) plus a sort of that one
72
+ // task's few claims, and the feeds are LIMITed.
73
+ function helperJoinSql(alias = 't') {
74
+ return `LEFT JOIN LATERAL (
75
+ SELECT hb.github_login AS helper_login, hb.display_name AS helper_display_name
76
+ FROM (SELECT c.builder_id FROM claims c
77
+ WHERE ${alias}.source = '${PAGE_TWEAK_SOURCE}'
78
+ AND c.task_id = ${alias}.id AND c.worktree_name IS NOT NULL
79
+ ORDER BY ${latestFirst('c')}
80
+ LIMIT 1) ap
81
+ JOIN builders hb ON hb.id = ap.builder_id
82
+ WHERE ap.builder_id IS DISTINCT FROM ${alias}.shipped_by
83
+ ) helper ON true`;
84
+ }
85
+
86
+ module.exports = { PAGE_TWEAK_SOURCE, webClaimPredicate, latestFirst, artistIdSql, shippedBySql, helperJoinSql };
@@ -13,6 +13,7 @@
13
13
 
14
14
  const api = require('../../src/module-api');
15
15
  const hold = require('./page-tweak-hold');
16
+ const artistRule = require('./page-tweak-artist');
16
17
 
17
18
  // Every page-tweak task, oldest first, with what the round state (D7) and the
18
19
  // changelog (D8) need beside the task itself:
@@ -35,9 +36,10 @@ const hold = require('./page-tweak-hold');
35
36
  // * the active claim is a plain join: uniq_active_claim_per_task allows at
36
37
  // most one row per task, and its partial index serves it.
37
38
  // * the latest web claim is ONE pass over claims (the `web` CTE, DISTINCT ON
38
- // per task), joined once, rather than a correlated subquery per task:
39
- // claims has no full index on task_id, so a per-row LATERAL would scan it
40
- // once per round.
39
+ // per task), joined once, rather than a correlated subquery per task.
40
+ // (When this was written claims had no full index on task_id; core_263 has
41
+ // since added idx_claims_task_all, so a per-task lookup is an index probe.
42
+ // The single pass stays: it is still the cheaper plan for every round at once.)
41
43
  //
42
44
  // `pageId` narrows it to ONE page's rounds (task 1004319 / TW08): the draft
43
45
  // autosave runs this on every debounced save, and a page's rounds are a handful
@@ -52,8 +54,8 @@ async function listPageTweakTasks({ pageId = null } = {}) {
52
54
  ), web AS (
53
55
  SELECT DISTINCT ON (c.task_id) c.task_id, c.builder_id
54
56
  FROM claims c JOIN pt ON pt.id = c.task_id
55
- WHERE c.worktree_name IS NULL AND c.creator_session_id IS NULL
56
- ORDER BY c.task_id, c.claimed_at DESC, c.id DESC
57
+ WHERE ${artistRule.webClaimPredicate('c')}
58
+ ORDER BY c.task_id, ${artistRule.latestFirst('c')}
57
59
  )
58
60
  SELECT t.id, t.status, t.title, t.description, t.source_ref, t.touches,
59
61
  t.shipped_at, t.created_at, t.updated_at,
@@ -33,6 +33,7 @@ const { depRefusalRow } = require('../dead-deps.js');
33
33
  // quality verdict rather than an outage or an errored worker (task 1002652).
34
34
  // The acclaim feed reads it so a grader outage never reaches a voter as 0.0.
35
35
  const { gradeQualityWhere } = require('../db-shared');
36
+ const { helperJoinSql } = require('../page-tweak-artist'); // task 1004432: a page tweak's applier as a helper
36
37
  const auth = api;
37
38
  const db = require('../db');
38
39
  const pageTweakReads = require('../page-tweak-reads'); const { HELD_NOT_ARMED } = require('../page-tweak-hold'); // ADR 0341 D7 (tests stub the read)
@@ -783,6 +784,7 @@ module.exports = function buildTasksRouter() {
783
784
  `SELECT t.id, t.title, t.value_summary, t.version_id, t.status,
784
785
  t.shipped_at, t.shipped_by,
785
786
  b.github_login, b.display_name,
787
+ helper.helper_login, helper.helper_display_name,
786
788
  COALESCE(pv.net, 0)::int AS net_votes,
787
789
  mine.vote AS my_vote,
788
790
  -- task 1002835: a karma vote is uninformed without the two facts
@@ -801,6 +803,7 @@ module.exports = function buildTasksRouter() {
801
803
  cost.total_usd AS cost_usd
802
804
  FROM tasks t
803
805
  LEFT JOIN builders b ON b.id = t.shipped_by
806
+ ${helperJoinSql('t')}
804
807
  LEFT JOIN task_grades g ON g.task_id = t.id
805
808
  LEFT JOIN LATERAL (
806
809
  SELECT SUM(amount_usd) AS total_usd
@@ -56,6 +56,23 @@ const SOFT_LIMIT_RULES = Object.freeze([
56
56
  effect: 'grey',
57
57
  reason: 'A nonprofit has no owners, so it has no shares to give.',
58
58
  }),
59
+ // The owner's call (task 1004475, 2026-09-30): a single-owner company and a research
60
+ // entity are greyed too, each with the reason that fits it. Only a for-profit company
61
+ // or a division of a corporation is offered equity.
62
+ Object.freeze({
63
+ id: 'equity-not-in-a-single-owner-llc',
64
+ when: { entity: { in: ['single-owner-llc'] } },
65
+ options: ['reward_intents:equity'],
66
+ effect: 'grey',
67
+ reason: 'A single-owner company has one owner. Giving builders a stake means taking on more owners first.',
68
+ }),
69
+ Object.freeze({
70
+ id: 'equity-not-in-a-research-entity',
71
+ when: { entity: { in: ['research'] } },
72
+ options: ['reward_intents:equity'],
73
+ effect: 'grey',
74
+ reason: "A research entity usually has no shares to give, so equity is off unless it's set up as a company.",
75
+ }),
59
76
  Object.freeze({
60
77
  id: 'no-cash-payouts-to-eu-builders-yet',
61
78
  when: { builder_geography: { in: ['us-eu', 'global'] } },
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "cutout",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "scope",
9
+ "label": "primordial sea",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. A chrome planet whose whole surface is a slow, liquid chrome ocean with faint swirling currents, calm and empty, nothing living yet.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "1024x1024 from the same panel",
13
+ "source": "r5-life-1-primordial-sea.jpg with the black ground removed: flood-fill from the edges on near-black pixels, luminance-feathered",
14
+ "encoded": "canvas WebP quality 0.88 with alpha",
15
+ "use": "light mode: the genesis life strip, stage scope (primordial sea)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "scope",
9
+ "label": "primordial sea",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. A chrome planet whose whole surface is a slow, liquid chrome ocean with faint swirling currents, calm and empty, nothing living yet.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "source panel about 220px across the object in a 1024x559 sheet, squared and upscaled to 1024px; soft if shown large, sized for the genesis life strip",
13
+ "source": "the owner's Gemini sheet, split by the ui-design kit browser (panel detection on the white dividers)",
14
+ "encoded": "canvas JPEG quality 0.92, 1024x1024, pure black ground",
15
+ "use": "dark mode: the genesis life strip, stage scope (primordial sea)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "cutout",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "strategize",
9
+ "label": "first cells",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet with a cluster of small glossy chrome beads, like the first cells, gathering on one side of its surface.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "1024x1024 from the same panel",
13
+ "source": "r5-life-2-first-cells.jpg with the black ground removed: flood-fill from the edges on near-black pixels, luminance-feathered",
14
+ "encoded": "canvas WebP quality 0.88 with alpha",
15
+ "use": "light mode: the genesis life strip, stage strategize (first cells)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "strategize",
9
+ "label": "first cells",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet with a cluster of small glossy chrome beads, like the first cells, gathering on one side of its surface.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "source panel about 220px across the object in a 1024x559 sheet, squared and upscaled to 1024px; soft if shown large, sized for the genesis life strip",
13
+ "source": "the owner's Gemini sheet, split by the ui-design kit browser (panel detection on the white dividers)",
14
+ "encoded": "canvas JPEG quality 0.92, 1024x1024, pure black ground",
15
+ "use": "dark mode: the genesis life strip, stage strategize (first cells)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "cutout",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "charter",
9
+ "label": "the genome",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet wrapped loosely by a single double-helix ribbon of polished chrome, like DNA, circling it at an angle.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "1024x1024 from the same panel",
13
+ "source": "r5-life-3-genome.jpg with the black ground removed: flood-fill from the edges on near-black pixels, luminance-feathered",
14
+ "encoded": "canvas WebP quality 0.88 with alpha",
15
+ "use": "light mode: the genesis life strip, stage charter (the genome)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "charter",
9
+ "label": "the genome",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet wrapped loosely by a single double-helix ribbon of polished chrome, like DNA, circling it at an angle.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "source panel about 220px across the object in a 1024x559 sheet, squared and upscaled to 1024px; soft if shown large, sized for the genesis life strip",
13
+ "source": "the owner's Gemini sheet, split by the ui-design kit browser (panel detection on the white dividers)",
14
+ "encoded": "canvas JPEG quality 0.92, 1024x1024, pure black ground",
15
+ "use": "dark mode: the genesis life strip, stage charter (the genome)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "cutout",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "legalize",
9
+ "label": "the membrane",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet sealed inside a thin, translucent frosted-glass shell, a soft membrane with a gentle inner glow.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "1024x1024 from the same panel",
13
+ "source": "r5-life-4-membrane.jpg with the black ground removed: flood-fill from the edges on near-black pixels, luminance-feathered; the halo outside the shell rim un-multiplied from black so the glow fades on light paper instead of darkening",
14
+ "encoded": "canvas WebP quality 0.88 with alpha",
15
+ "use": "light mode: the genesis life strip, stage legalize (the membrane)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "legalize",
9
+ "label": "the membrane",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet sealed inside a thin, translucent frosted-glass shell, a soft membrane with a gentle inner glow.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "source panel about 220px across the object in a 1024x559 sheet, squared and upscaled to 1024px; soft if shown large, sized for the genesis life strip",
13
+ "source": "the owner's Gemini sheet, split by the ui-design kit browser (panel detection on the white dividers)",
14
+ "encoded": "canvas JPEG quality 0.92, 1024x1024, pure black ground",
15
+ "use": "dark mode: the genesis life strip, stage legalize (the membrane)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "cutout",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "legislation",
9
+ "label": "the nervous system",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet with fine branching chrome filaments spreading across its surface like neurons, a faint warm glow where they meet.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "1024x1024 from the same panel",
13
+ "source": "r5-life-5-nervous-system.jpg with the black ground removed: flood-fill from the edges on near-black pixels, luminance-feathered",
14
+ "encoded": "canvas WebP quality 0.88 with alpha",
15
+ "use": "light mode: the genesis life strip, stage legislation (the nervous system)"
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "kind": "plate",
3
+ "model": "Gemini image model (generated by the owner in the Gemini app, 2026-09-30; one 5-panel sheet)",
4
+ "aspect_ratio": "1:1",
5
+ "generated": "2026-09-30",
6
+ "round": 5,
7
+ "task": 1004425,
8
+ "stage": "legislation",
9
+ "label": "the nervous system",
10
+ "prompt": "Mirror-polished liquid chrome, soft key light from the top left, a slightly long lens, the object whole and centred, pure black background everywhere the object is not. No text, no letters, no logo, no watermark, no UI. The same chrome planet with fine branching chrome filaments spreading across its surface like neurons, a faint warm glow where they meet.",
11
+ "reference": "modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg, uploaded as the material/light/ground reference",
12
+ "resolution": "source panel about 220px across the object in a 1024x559 sheet, squared and upscaled to 1024px; soft if shown large, sized for the genesis life strip",
13
+ "source": "the owner's Gemini sheet, split by the ui-design kit browser (panel detection on the white dividers)",
14
+ "encoded": "canvas JPEG quality 0.92, 1024x1024, pure black ground",
15
+ "use": "dark mode: the genesis life strip, stage legislation (the nervous system)"
16
+ }
@@ -741,6 +741,10 @@ async function sessionUploadCoverage({ days, windowHours } = {}) {
741
741
  AND t.shipped_by IS NOT NULL
742
742
  AND t.shipped_at IS NOT NULL
743
743
  AND t.shipped_at >= now() - ($1 * interval '1 day')
744
+ -- task 1004432: a shipped page tweak names its ARTIST, who writes in the
745
+ -- studio and runs no session, so it can never be "covered"; the session
746
+ -- that applied it is the applier's, on an ordinary task of its own kind.
747
+ AND t.source IS DISTINCT FROM 'page-tweak'
744
748
  GROUP BY t.shipped_by, b.github_login
745
749
  ORDER BY 4 DESC`,
746
750
  [d, w]
@@ -765,6 +769,10 @@ async function shipsMissingSessionRecord({ days, windowHours, limit, builderId }
765
769
  AND t.shipped_by IS NOT NULL
766
770
  AND t.shipped_at IS NOT NULL
767
771
  AND t.shipped_at >= now() - ($1 * interval '1 day')
772
+ -- task 1004432: a shipped page tweak names its ARTIST, who writes in the
773
+ -- studio and runs no session, so it can never be "covered"; the session
774
+ -- that applied it is the applier's, on an ordinary task of its own kind.
775
+ AND t.source IS DISTINCT FROM 'page-tweak'
768
776
  AND ($3::bigint IS NULL OR t.shipped_by = $3)
769
777
  AND NOT EXISTS (
770
778
  SELECT 1
@@ -984,7 +984,9 @@ function renderShippedPage() {
984
984
  const meta = document.createElement('p');
985
985
  meta.className = 'shipped__meta';
986
986
  const by = t.display_name || t.github_login;
987
- meta.textContent = `${by ? `inscribed by ${by} · ` : ''}${fmtRelativeTime(t.shipped_at)}`;
987
+ // task 1004432: an approved page is the artist's; the session that applied it is a helper.
988
+ const helper = t.helper_display_name || t.helper_login;
989
+ meta.textContent = `${by ? `inscribed by ${by} · ` : ''}${helper ? `with help from ${helper} · ` : ''}${fmtRelativeTime(t.shipped_at)}`;
988
990
  li.appendChild(title);
989
991
  if (thumb) li.appendChild(thumb);
990
992
  li.appendChild(meta);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.51",
3
+ "version": "1.20.53",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.51",
9
+ "version": "1.20.53",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",