@bongos/core 1.24.16 → 1.24.18

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,9 +3,9 @@
3
3
  "generator": "scripts/bongos/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 3278,
7
- "unique_ids": 2595,
8
- "certain": 3278,
6
+ "strings": 3276,
7
+ "unique_ids": 2593,
8
+ "certain": 3276,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 275
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 172,
16
- "strings": 2165
16
+ "strings": 2163
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -7694,7 +7694,7 @@
7694
7694
  "surface": "builders-hall",
7695
7695
  "text": "Free here. {…} bills you for it directly.",
7696
7696
  "file": "modules/hall-ui/public/ai-studio.js",
7697
- "line": 334,
7697
+ "line": 336,
7698
7698
  "origin": "js-markup",
7699
7699
  "confidence": "certain"
7700
7700
  },
@@ -10200,15 +10200,6 @@
10200
10200
  "origin": "js-markup",
10201
10201
  "confidence": "certain"
10202
10202
  },
10203
- {
10204
- "id": "b7d4eeb3f762",
10205
- "surface": "builders-hall",
10206
- "text": "My own key",
10207
- "file": "modules/hall-ui/public/ai-studio.js",
10208
- "line": 334,
10209
- "origin": "js-markup",
10210
- "confidence": "certain"
10211
- },
10212
10203
  {
10213
10204
  "id": "3d0d554cd684",
10214
10205
  "surface": "builders-hall",
@@ -21693,15 +21684,6 @@
21693
21684
  "origin": "js-markup",
21694
21685
  "confidence": "certain"
21695
21686
  },
21696
- {
21697
- "id": "c80c3434ae36",
21698
- "surface": "builders-hall",
21699
- "text": "{…} key ending {…}",
21700
- "file": "modules/hall-ui/public/ai-studio.js",
21701
- "line": 334,
21702
- "origin": "js-markup",
21703
- "confidence": "certain"
21704
- },
21705
21687
  {
21706
21688
  "id": "5ad81ff61920",
21707
21689
  "surface": "builders-hall",
@@ -3288,5 +3288,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3288
3288
  landed since 1.24.13 with no explicit bump. run 41110951037. (task 1002620)
3289
3289
  1.24.16 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3290
3290
  landed since 1.24.15 with no explicit bump. run 41124515103. (task 1002620)
3291
+ 1.24.17 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3292
+ landed since 1.24.16 with no explicit bump. run 41129472650. (task 1002620)
3293
+ 1.24.18 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3294
+ landed since 1.24.17 with no explicit bump. run 41131020363. (task 1002620)
3291
3295
  ---------------------------------------------------------------------------
3292
3296
  ```
@@ -5,16 +5,16 @@
5
5
  "adr": "0341 D3",
6
6
  "reread": "node scripts/bongos/page-reader.js --stale",
7
7
  "how": ["Generated by node scripts/bongos/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
8
- "counts": {"pages":52,"lines":5612,"placed":1755,"shared":461,"unplaced":3396,"omitted_data":3766},
8
+ "counts": {"pages":52,"lines":5612,"placed":1753,"shared":461,"unplaced":3398,"omitted_data":3766},
9
9
  "pages": [
10
10
  {
11
11
  "id": "builders:ai-studio",
12
12
  "surface": "builders",
13
13
  "title": "Studio",
14
14
  "states_read": ["pick","describe","make","waiting","result","result-own","failed","balance","balance-all","out"],
15
- "files_hash": "b4dc755790ac158e",
16
- "reading_hash": "7fa800b814783e81",
17
- "counts": {"lines":102,"placed":40,"shared":8,"unplaced":54},
15
+ "files_hash": "48c0f2c2c104a63b",
16
+ "reading_hash": "475acccd2681bd7f",
17
+ "counts": {"lines":102,"placed":38,"shared":8,"unplaced":56},
18
18
  "omitted": {"data":15},
19
19
  "notes": [],
20
20
  "lines": [
@@ -84,9 +84,9 @@
84
84
  {"key":"L0064","section":"How do you want to pay?","text":"PDF","placement":"unplaced","state":"make"},
85
85
  {"key":"L0065","section":"How do you want to pay?","text":"“Turn the sketch into a finished hero picture for the landing pa…”","placement":"unplaced","state":"make"},
86
86
  {"key":"L0066","section":"How do you want to pay?","text":"change","placement":"unplaced","state":"make"},
87
- {"key":"L0067","section":"How do you want to pay?","text":"My own key","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"b7d4eeb3f762","state":"make"},
88
- {"key":"L0068","section":"How do you want to pay?","text":"Free here. {…} bills you for it directly.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"e9a1af5dc515","state":"make"},
89
- {"key":"L0069","section":"How do you want to pay?","text":"{…} key ending {…}","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":334,"string_id":"c80c3434ae36","state":"make"},
87
+ {"key":"L0067","section":"How do you want to pay?","text":"My own key","placement":"unplaced","state":"make"},
88
+ {"key":"L0068","section":"How do you want to pay?","text":"Free here. {…} bills you for it directly.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":336,"string_id":"e9a1af5dc515","state":"make"},
89
+ {"key":"L0069","section":"How do you want to pay?","text":"Gemini key ending {…}","placement":"unplaced","state":"make"},
90
90
  {"key":"L0070","section":"How do you want to pay?","text":"My balance","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":340,"string_id":"39afdc03594a","state":"make"},
91
91
  {"key":"L0071","section":"How do you want to pay?","text":"Usually a few cents. Up to {…} is set aside, and the rest comes back.","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":340,"string_id":"664555b65658","state":"make"},
92
92
  {"key":"L0072","section":"How do you want to pay?","text":"{…} left","placement":"placed","file":"modules/hall-ui/public/market-watch.js","line":73,"string_id":"92b79edf21f6","state":"make"},
@@ -4216,7 +4216,7 @@
4216
4216
  "surface": "builders",
4217
4217
  "title": "Studio",
4218
4218
  "states_read": ["home","out","flow","flow-pick","status","words-offer","words-edit","reviews","full","full-back","approvals","approvals-before","approvals-phone","approvals-note","approved","sent-back","approvals-page"],
4219
- "files_hash": "9eda81ed0ffd5014",
4219
+ "files_hash": "6c591072e20de9f0",
4220
4220
  "reading_hash": "bcba11e6cef56218",
4221
4221
  "counts": {"lines":31,"placed":16,"shared":6,"unplaced":9},
4222
4222
  "omitted": {"data":4},
@@ -0,0 +1,86 @@
1
+ -- core_290_builder_payouts.sql — a builder is paid for a task, exactly once, and a payout
2
+ -- that did not go through still says it is owed (task 1003183, goal 1000092 M04;
3
+ -- ADR 0385 decisions 1 and 4, ADR 0397 decisions 2, 5 and 7).
4
+ --
5
+ -- TWO TABLES, BOTH APPEND-ONLY.
6
+ --
7
+ -- builder_payouts_owed one row per (builder, task): what is owed, and the
8
+ -- personal approval that authorises it. Written BEFORE
9
+ -- the processor is asked to move anything, in its own
10
+ -- committed step, so a crash, a refusal or a timeout
11
+ -- afterwards still leaves the debt on record. The failure
12
+ -- this exists to prevent is a builder who was silently
13
+ -- never paid, with no trace anything was due.
14
+ -- builder_payout_attempts every try at paying one owed row: sent (with the
15
+ -- processor's transfer reference), failed (with the
16
+ -- reason), or held (not sent, with the reason: a gate,
17
+ -- no processor connected, a missing approval).
18
+ --
19
+ -- EXACTLY ONCE is three rules, each enforced here, not only by the code:
20
+ -- * one owed row per (builder, task), and one per approval (an approval pays one
21
+ -- thing, never two tasks);
22
+ -- * at most one `sent` attempt per owed row;
23
+ -- * the processor is called with an idempotency key derived from the owed row's id,
24
+ -- so a retry after an unknown outcome is answered by the processor with the same
25
+ -- transfer, not a second one.
26
+ --
27
+ -- Amounts are integer cents, as the approval that authorises them is. The money
28
+ -- itself is held by the processor (ADR 0397); the matching `payout` movement is booked
29
+ -- through the economy module's `economy.account` port, in micro-dollars, in the same step as `sent`.
30
+
31
+ CREATE TABLE IF NOT EXISTS builder_payouts_owed (
32
+ id BIGSERIAL PRIMARY KEY,
33
+ builder_id BIGINT NOT NULL REFERENCES builders(id),
34
+ task_id BIGINT NOT NULL REFERENCES tasks(id),
35
+ approval_id BIGINT NOT NULL REFERENCES payout_approvals(id),
36
+ amount_cents BIGINT NOT NULL,
37
+ currency TEXT NOT NULL,
38
+ recorded_by BIGINT NOT NULL REFERENCES builders(id),
39
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
40
+ CONSTRAINT builder_payouts_owed_amount_chk CHECK (amount_cents > 0),
41
+ CONSTRAINT builder_payouts_owed_currency_chk CHECK (currency ~ '^[A-Z]{3}$')
42
+ );
43
+
44
+ -- one payout per builder per task, and one task per approval
45
+ CREATE UNIQUE INDEX IF NOT EXISTS builder_payouts_owed_one_per_task
46
+ ON builder_payouts_owed (builder_id, task_id);
47
+ CREATE UNIQUE INDEX IF NOT EXISTS builder_payouts_owed_one_per_approval
48
+ ON builder_payouts_owed (approval_id);
49
+ CREATE INDEX IF NOT EXISTS builder_payouts_owed_by_time ON builder_payouts_owed (created_at DESC, id DESC);
50
+
51
+ CREATE TABLE IF NOT EXISTS builder_payout_attempts (
52
+ id BIGSERIAL PRIMARY KEY,
53
+ owed_id BIGINT NOT NULL REFERENCES builder_payouts_owed(id),
54
+ outcome TEXT NOT NULL,
55
+ processor_ref TEXT,
56
+ reason TEXT,
57
+ actor_id BIGINT REFERENCES builders(id),
58
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
59
+ CONSTRAINT builder_payout_attempts_outcome_chk CHECK (outcome IN ('sent', 'failed', 'held')),
60
+ CONSTRAINT builder_payout_attempts_reason_len_chk CHECK (reason IS NULL OR length(reason) <= 300),
61
+ -- a sent attempt names the processor transfer; a failed or held one says why
62
+ CONSTRAINT builder_payout_attempts_cause_chk CHECK (
63
+ (outcome = 'sent' AND processor_ref IS NOT NULL) OR
64
+ (outcome IN ('failed', 'held') AND reason IS NOT NULL AND length(reason) > 0))
65
+ );
66
+
67
+ -- paid at most once
68
+ CREATE UNIQUE INDEX IF NOT EXISTS builder_payout_attempts_one_sent
69
+ ON builder_payout_attempts (owed_id) WHERE outcome = 'sent';
70
+ CREATE INDEX IF NOT EXISTS builder_payout_attempts_by_owed ON builder_payout_attempts (owed_id, id DESC);
71
+
72
+ CREATE OR REPLACE FUNCTION builder_payouts_append_only() RETURNS trigger AS $$
73
+ BEGIN
74
+ RAISE EXCEPTION '% is append-only: a payout is never edited or deleted, a new attempt is recorded instead', TG_TABLE_NAME;
75
+ END;
76
+ $$ LANGUAGE plpgsql;
77
+
78
+ DROP TRIGGER IF EXISTS builder_payouts_owed_no_rewrite ON builder_payouts_owed;
79
+ CREATE TRIGGER builder_payouts_owed_no_rewrite BEFORE UPDATE OR DELETE ON builder_payouts_owed
80
+ FOR EACH ROW EXECUTE FUNCTION builder_payouts_append_only();
81
+ DROP TRIGGER IF EXISTS builder_payout_attempts_no_rewrite ON builder_payout_attempts;
82
+ CREATE TRIGGER builder_payout_attempts_no_rewrite BEFORE UPDATE OR DELETE ON builder_payout_attempts
83
+ FOR EACH ROW EXECUTE FUNCTION builder_payouts_append_only();
84
+
85
+ INSERT INTO schema_migrations (version) VALUES ('core_290_builder_payouts')
86
+ ON CONFLICT DO NOTHING;
@@ -65,6 +65,10 @@ function listPublic(rows) {
65
65
  function chooseKey(stored, payWith) {
66
66
  const own = ['openrouter', 'gemini'].find((p) => (stored || []).includes(p));
67
67
  if (payWith === 'ours') return { source: 'ours' };
68
+ // A named provider picks that key when both are saved.
69
+ if (isProvider(payWith)) {
70
+ return (stored || []).includes(payWith) ? { source: 'own', provider: payWith } : { error: 'no_own_key' };
71
+ }
68
72
  if (payWith === 'own' && !own) return { error: 'no_own_key' };
69
73
  if (payWith !== undefined && payWith !== null && payWith !== 'own') return { error: 'bad_pay_with' };
70
74
  return own ? { source: 'own', provider: own } : { source: 'ours' };
@@ -173,7 +173,7 @@ module.exports = function buildAiStudioRouter() {
173
173
  if (api.validateOrRespond(req, res, {
174
174
  prompt: { required: true, type: 'string', maxLength: S.MAX_PROMPT + 200 },
175
175
  file_ids: { required: true, type: 'array', maxItems: S.MAX_FILES },
176
- pay_with: { type: 'string', enum: ['own', 'ours'] },
176
+ pay_with: { type: 'string', enum: ['own', 'ours', 'openrouter', 'gemini'] },
177
177
  })) return;
178
178
  if (!S.isAdult(await db.latestAgeTier(req.builder.id))) return res.fail('adults_only', { status: 403, message: ADULTS_ONLY });
179
179
  const checked = S.checkRequest(req.body);
@@ -8,6 +8,6 @@
8
8
  "default": true,
9
9
  "maintenance": { "status": "core-maintained" },
10
10
  "contributes": { "migrations": true, "routes": ["cost", "credits", "earnings", "leaderboard", "project-account", "reward"] },
11
- "provides": ["reward"],
11
+ "provides": ["reward", "economy.account"],
12
12
  "consumes": []
13
13
  }
@@ -15,11 +15,20 @@
15
15
 
16
16
  const express = require('express');
17
17
  const api = require('../../../src/module-api');
18
- const { readAccount } = require('../project-account');
18
+ const { readAccount, recordMovement } = require('../project-account');
19
19
 
20
20
  const log = api.logger('economy');
21
21
 
22
+ // The `economy.account` PORT (task 1003183): core's payout path (src/bongos/builder-payouts.js)
23
+ // books its `payout` movement through this, inside its own transaction, without importing the
24
+ // module. Registered here for the reason routes/reward.js gives: the loader runs this factory in
25
+ // the live server and whenever a test rebuilds the router. hasProvider-guarded for re-runs.
26
+ function registerSeams() {
27
+ if (!api.hasProvider('economy.account')) api.registerProvider('economy.account', { recordMovement });
28
+ }
29
+
22
30
  module.exports = function buildProjectAccountRouter({ processor = null } = {}) {
31
+ registerSeams();
23
32
  const router = express.Router();
24
33
 
25
34
  // rank: payout.approve (archon floor) — the owner. The account's balance and who was paid what
@@ -107,25 +107,31 @@
107
107
 
108
108
  // The own key the server will run on: OpenRouter first, as keys.chooseKey
109
109
  // prefers it (it reports its cost). null when none is saved.
110
- function ownKey(keys) {
110
+ function ownKey(keys) { return ownKeys(keys)[0] || null; }
111
+
112
+ // Every own key that is saved, OpenRouter first: step 3 offers one card each,
113
+ // so a builder with both picks which one runs.
114
+ function ownKeys(keys) {
111
115
  const list = (keys && keys.keys) || [];
112
- const set = (p) => list.find((k) => k && k.provider === p && k.set);
113
- const k = set('openrouter') || set('gemini');
114
- if (!k) return null;
115
- return k.provider === 'openrouter'
116
- ? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
117
- : { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 };
116
+ return ['openrouter', 'gemini']
117
+ .map((p) => list.find((k) => k && k.provider === p && k.set))
118
+ .filter(Boolean)
119
+ .map((k) => (k.provider === 'openrouter'
120
+ ? { provider: 'openrouter', name: 'OpenRouter', billedBy: 'OpenRouter', last4: k.last4 }
121
+ : { provider: 'gemini', name: 'Gemini', billedBy: 'Google', last4: k.last4 }));
118
122
  }
119
123
 
120
- // Step 3's two cards. balance is GET /ai-studio/balance's body.
124
+ // Step 3's cards. balance is GET /ai-studio/balance's body.
121
125
  function payChoices(keys, balance) {
122
- const own = ownKey(keys);
126
+ const owns = ownKeys(keys);
127
+ const own = owns[0] || null;
123
128
  const b = balance || {};
124
129
  const avail = b.balance ? b.balance.available_micro : null;
125
130
  const max = isMicro(b.max_cost_micro) ? b.max_cost_micro : null;
126
131
  const payments = !!b.balance;
127
132
  return {
128
133
  own,
134
+ owns,
129
135
  payments,
130
136
  setAside: max === null ? '' : usd(max),
131
137
  left: payments ? `${balanceUsd(avail)} left` : '',
@@ -260,7 +266,7 @@
260
266
 
261
267
  const lib = {
262
268
  MAX_FILES, MAX_IMAGE_BYTES, MAX_PDF_BYTES, IMAGE_TYPES, PDF_TYPE, STEPS, TOPUP_CHOICES, ANSWER_AFTER_SEC,
263
- usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, payChoices,
269
+ usd, balanceUsd, sizeText, routeOf, checkFile, isImage, stepMarks, quoteLine, ownKey, ownKeys, payChoices,
264
270
  waitSteps, clock, isFinished, receipt, againLine, monthSummary, ledgerLine, replyHtml, homeLine,
265
271
  };
266
272
  if (typeof module !== 'undefined' && module.exports) module.exports = lib;
@@ -331,12 +331,12 @@
331
331
  const files = readyFiles();
332
332
  const c = lib.payChoices(state.keys, state.balance);
333
333
  const thumbs = files.slice(0, 3).map((f) => thumb(f)).join('');
334
- const ownCard = c.own ? `
335
- <button type="button" class="st-action ais-card ais-card--own" data-pay="own">
336
- <span class="st-action__label">My own key</span>
337
- <span class="ais-card__line">Free here. ${esc(c.own.billedBy)} bills you for it directly.</span>
338
- <span class="ais-card__foot">${esc(c.own.name)} key ending ${esc(c.own.last4 || '')}</span>
339
- </button>` : '';
334
+ const ownCard = c.owns.map((k) => `
335
+ <button type="button" class="st-action ais-card ais-card--own" data-pay="${esc(k.provider)}">
336
+ <span class="st-action__label">${c.owns.length > 1 ? `My ${esc(k.name)} key` : 'My own key'}</span>
337
+ <span class="ais-card__line">Free here. ${esc(k.billedBy)} bills you for it directly.</span>
338
+ <span class="ais-card__foot">${c.owns.length > 1 ? '' : `${esc(k.name)} `}key ending ${esc(k.last4 || '')}</span>
339
+ </button>`).join('');
340
340
  const balanceCard = c.payments ? `
341
341
  <button type="button" class="st-action ais-card ais-card--ours" data-pay="ours" ${c.short ? 'disabled' : ''}>
342
342
  <span class="st-action__label">My balance</span>
@@ -346,8 +346,8 @@
346
346
  let foot = 'You only pay for what it really cost. If it fails, nothing is taken.';
347
347
  let extra = '';
348
348
  if (c.payments && c.short) extra = `<p class="ais-faint">Your balance is under ${esc(c.setAside)}. <a href="/builders/ai-studio?view=balance">Add money</a> to pay this way.</p>`;
349
- if (!c.own) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
350
- if (!c.own && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
349
+ if (!c.owns.length) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
350
+ if (!c.owns.length && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
351
351
  page.innerHTML = `${stepHead('make')}
352
352
  <h1 class="ais-ask" data-head tabindex="-1">How do you want to pay?</h1>
353
353
  <div class="ais-row ais-row--summary">${thumbs}<span class="ais-grow ais-soft">${esc(lib.quoteLine(state.draft.prompt))}</span><a class="ais-link" href="/builders/ai-studio?step=describe" data-nav="step=describe">change</a></div>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.16",
3
+ "version": "1.24.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.16",
9
+ "version": "1.24.18",
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.24.16",
3
+ "version": "1.24.18",
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",
@@ -10981,5 +10981,17 @@
10981
10981
  "id": "1005028",
10982
10982
  "text": "Settings now has a Your age section where you record your age group, which switches on AI Studio for adults."
10983
10983
  }
10984
+ ],
10985
+ "1.24.17": [
10986
+ {
10987
+ "id": "1005029",
10988
+ "text": "Artists with both an OpenRouter and a Gemini key saved can now choose which one runs their AI request."
10989
+ }
10990
+ ],
10991
+ "1.24.18": [
10992
+ {
10993
+ "id": "1003183",
10994
+ "text": "Builders can now be paid for a task, and never twice. Each payment is written down as owed before anything is sent, so a failed payment still leaves a record. No money moves yet: the Stripe connection comes next."
10995
+ }
10984
10996
  ]
10985
10997
  }
@@ -285,6 +285,10 @@ const INTEGRATION = new Set([
285
285
  // causeless or wrongly-directed movement, any UPDATE/DELETE/TRUNCATE, and a second row
286
286
  // for one processor reference.
287
287
  'project_account_db',
288
+ // task 1003183: builder payouts from the real migrations — Postgres refuses a second debt
289
+ // for one task or approval, a second sent attempt, a causeless attempt, and any rewrite;
290
+ // the real payForTask pays once and books one payout movement.
291
+ 'builder_payouts_db',
288
292
  // task 1004408: migration platform_identity_028 deletes the old hub invite's
289
293
  // pending rows before a cutoff it reads from the core_upgrades ledger. Only a real
290
294
  // engine runs the DO block, the numeric version compare and the one-shot guard.