@bongos/core 1.19.241 → 1.19.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.241",
6
- "core_contract": "1.19.241",
7
- "source_commit": "3be9d3e72426f99e05a9842dcb5cc9c85cfc75a0",
5
+ "core_version": "1.19.243",
6
+ "core_contract": "1.19.243",
7
+ "source_commit": "ac9bed6cf6412a5916b8b5f1c6c946f3c733cc96",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-18T20:51:17.253Z",
9
+ "built_at": "2026-08-18T22:39:28.983Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 312,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1660,
20
- "tree_sha256": "d8e9cb0898ebdea8fca5a8bc8e94fd925e4725473943002abf4cbbc68e4ae963",
20
+ "tree_sha256": "28e71bece4c2803c63b8674af865cb7cfc74626f6e768d40e3742116571bb7d4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1367,7 +1367,7 @@
1367
1367
  {
1368
1368
  "path": "docs/adr/0177-goal-membership-kind.md",
1369
1369
  "mode": "0000644",
1370
- "sha256": "1bc4a0df32f8267b5fb3b274974fbcd15d680a161de4c502334dff62da494b17"
1370
+ "sha256": "922dc12e699d3631907292e0a5550806ac88a160394c65c8223f65e58a46bc6f"
1371
1371
  },
1372
1372
  {
1373
1373
  "path": "docs/adr/README.md",
@@ -2377,7 +2377,7 @@
2377
2377
  {
2378
2378
  "path": "migrations/core_220_goal_members_membership_kind.sql",
2379
2379
  "mode": "0000644",
2380
- "sha256": "e466f4222fdbfa04386003990efabe38f97f3b3c23ff174a622642daea5c096b"
2380
+ "sha256": "e0503f677b4b30eca07badd76ba9b177f603f3740da366c1be40cb67a5537690"
2381
2381
  },
2382
2382
  {
2383
2383
  "path": "modules/autonomy/CLAUDE.md",
@@ -4347,12 +4347,12 @@
4347
4347
  {
4348
4348
  "path": "package-lock.json",
4349
4349
  "mode": "0000644",
4350
- "sha256": "8f961bb896bdf312ad73752daa2342ce9ccf7db02b21ac9d2480694db28857d8"
4350
+ "sha256": "c3f2be5aa7bbd5f5f40977d42f12485c3049df0ac811531d20bf3e7af51aed18"
4351
4351
  },
4352
4352
  {
4353
4353
  "path": "package.json",
4354
4354
  "mode": "0000644",
4355
- "sha256": "ac7bd7958b32f4eee03a29fab16260eb489d49c0e40907691bdd9c3e447d6a5d"
4355
+ "sha256": "c0b9cdaf8ef1290a7c61fc7068390a6a14b9477a0b84c17e9018c4ab832e4d7c"
4356
4356
  },
4357
4357
  {
4358
4358
  "path": "public-docs/index.html",
@@ -5772,7 +5772,7 @@
5772
5772
  {
5773
5773
  "path": "src/bongos/llm-pricing.js",
5774
5774
  "mode": "0000644",
5775
- "sha256": "f7688e75a873f9c287f6559311877081830798cb1b5f1e548ceff3fcbed9e31d"
5775
+ "sha256": "a932e617ee66dabc49b4087baf6606eeb2074f70ebb3efa0723b1d9c92add290"
5776
5776
  },
5777
5777
  {
5778
5778
  "path": "src/bongos/metrics.js",
@@ -5967,7 +5967,7 @@
5967
5967
  {
5968
5968
  "path": "src/module-api.js",
5969
5969
  "mode": "0000644",
5970
- "sha256": "d2dd5e30bb7f44cd8a37cbf559bbcdf76aec973043f3db5deec8988b62a59eb6"
5970
+ "sha256": "1f5ebdac8a94a258566b8d28fa14a3f47496e0f35e928e2d85ba85e19738134a"
5971
5971
  },
5972
5972
  {
5973
5973
  "path": "src/module-loader/catalog.js",
@@ -7332,7 +7332,7 @@
7332
7332
  {
7333
7333
  "path": "tests/llm-pricing.mjs",
7334
7334
  "mode": "0000644",
7335
- "sha256": "de3fabaeafb3eb3ad500becf565f3b6d7118a763f1e80d085f94b6ba990d5c58"
7335
+ "sha256": "64f8bd94dc461ca9b4ef82fae929d49675231afa6a24d6d65ba2dbe8721efc89"
7336
7336
  },
7337
7337
  {
7338
7338
  "path": "tests/local_preview.mjs",
@@ -7347,7 +7347,7 @@
7347
7347
  {
7348
7348
  "path": "tests/membership_kind_lockstep.mjs",
7349
7349
  "mode": "0000644",
7350
- "sha256": "7ebca67bf07f723f47a861eb530a0feee25967fe3c0f4a9e7d76703351dc8d84"
7350
+ "sha256": "e40df74d518a508417dbea2d8f39952bd8f666e866aff985a8ab72758ead873a"
7351
7351
  },
7352
7352
  {
7353
7353
  "path": "tests/memory_isolation.mjs",
@@ -128,9 +128,11 @@ cannot, if the new column has a DEFAULT: **Postgres materializes a column defaul
128
128
  statement — took the "new code wrote it" branch and overwrote `role`, turning a pending
129
129
  INVITE into an active membership. So the new column stays **DEFAULT-less** while the legacy
130
130
  one is still written (NULL then means exactly "not written"), and `DEFAULT` + `NOT NULL`
131
- arrive together in step 2. `core_220` shipped with the defective form and is corrected
132
- under task [#1003093](https://cloudbongos.com/builders#/task/1003093) — where the same
133
- statement demotes a goal's first lead to `member` on a fresh install; the invariant is
131
+ arrive together in step 2. `core_220` shipped with the defective form and was corrected
132
+ the same day under task [#1003093](https://cloudbongos.com/builders#/task/1003093) — there
133
+ the same statement demoted a goal's first lead to `member` on a fresh install
134
+ (`<redacted>.sql` runs after the core migrations), and no production
135
+ row was ever affected because the pin still served 1.19.218; the invariant is
134
136
  pinned statically by `tests/membership_kind_lockstep.mjs`, since the trigger only
135
137
  misbehaves against a live Postgres and CI has none.
136
138
 
@@ -43,8 +43,15 @@ ALTER TABLE goal_members
43
43
 
44
44
  UPDATE goal_members SET membership_kind = role WHERE membership_kind IS NULL;
45
45
 
46
- ALTER TABLE goal_members
47
- ALTER COLUMN membership_kind SET DEFAULT 'member';
46
+ -- NO DEFAULT WHILE `role` IS STILL WRITTEN — and this is load-bearing, not tidiness
47
+ -- (task 1003093, found by the grader on the sibling migration). Postgres materializes
48
+ -- a column DEFAULT into NEW *before* any BEFORE ROW trigger fires, so a default here
49
+ -- would make `NEW.membership_kind IS NULL` never true and the trigger could no longer
50
+ -- tell "the caller wrote the new column" from "the default filled it". An old-release
51
+ -- INSERT naming only `role` — lifecycle_001_goal_first_leads.sql's `'lead'`, or
52
+ -- createGoal / transferGoalOwnership on a pre-1003013 pin — would then be silently
53
+ -- rewritten to the default, DEMOTING the goal's own lead to a plain member. The
54
+ -- DEFAULT (and NOT NULL) arrive in step 2, when `role` is dropped.
48
55
 
49
56
  DO $$
50
57
  BEGIN
@@ -63,20 +70,24 @@ END $$;
63
70
  -- statement that wrote either one — no second UPDATE, no window where the two
64
71
  -- disagree, and nothing for application code to remember.
65
72
  --
66
- -- Direction is decided by WHICH SIDE CHANGED, not by a preferred column: new
67
- -- code sets membership_kind and `role` follows; the historical
73
+ -- Direction is decided by WHICH SIDE THE CALLER WROTE: new code sets
74
+ -- membership_kind and `role` follows; the historical
68
75
  -- lifecycle_001_goal_first_leads.sql seed (and any pre-upgrade code, including
69
- -- the release a rollback would restore) sets `role` and membership_kind
70
- -- follows. On INSERT the new column wins when it was supplied, because `role`
71
- -- carries a DEFAULT that makes "omitted" and "explicitly 'member'"
72
- -- indistinguishable.
76
+ -- the release a rollback would restore) sets `role` and membership_kind follows.
77
+ -- On INSERT that question is answered by `NEW.membership_kind IS NULL`, which is
78
+ -- only trustworthy because the new column has NO DEFAULT — see the note above it.
79
+ -- On UPDATE, OLD is available and the comparison is direct.
73
80
  CREATE OR REPLACE FUNCTION goal_members_membership_kind_lockstep() RETURNS trigger AS $$
74
81
  BEGIN
75
82
  IF TG_OP = 'INSERT' THEN
76
- IF NEW.membership_kind IS NOT NULL THEN
77
- NEW.role := NEW.membership_kind;
78
- ELSE
83
+ -- NULL means the caller did not write the new column (see the no-DEFAULT note
84
+ -- above): an old-release INSERT, so `role` is the source of truth. Otherwise the
85
+ -- caller is new code and `role` follows, so an older release can still read it.
86
+ IF NEW.membership_kind IS NULL THEN
79
87
  NEW.membership_kind := COALESCE(NEW.role, 'member');
88
+ NEW.role := COALESCE(NEW.role, 'member');
89
+ ELSE
90
+ NEW.role := NEW.membership_kind;
80
91
  END IF;
81
92
  ELSE
82
93
  IF NEW.membership_kind IS DISTINCT FROM OLD.membership_kind THEN
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.241",
3
+ "version": "1.19.243",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.241",
9
+ "version": "1.19.243",
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.241",
3
+ "version": "1.19.243",
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",
@@ -49,15 +49,52 @@
49
49
  // - opus = Claude Opus 5 / 4.8 / 4.7 ($5 in / $25 out) [re-verified]
50
50
  // - sonnet = Claude Sonnet 4.6 ($3 in / $15 out) [re-verified]
51
51
  // - haiku = Claude Haiku 4.5 ($1 in / $5 out) [re-verified]
52
- // NOTE (deliberately NOT changed here — its own task, per the rule above):
53
- // Claude Sonnet 5 is now $2/$10, not the $3/$15 this `sonnet` row carries. The
54
- // grader's default worker model is claude-sonnet-5, so grade costs are currently
55
- // OVER-priced ~1.5×. That is a separate price-table decision; see task 1003001.
52
+ // VERIFIED 2026-08-18 against the live models overview at platform.claude.com
53
+ // (task 1003001 — the task the note this replaces pointed at):
54
+ // - sonnet = Claude Sonnet 5 ($2 in / $10 out) [WAS $3/$15 = Sonnet 4.6's rate]
55
+ // - fable = Claude Fable 5 / Mythos 5 ($10/$50) [re-verified, unchanged]
56
+ // - opus = Claude Opus 5 / 4.8 / 4.7 ($5/$25) [re-verified, unchanged]
57
+ // - haiku = Claude Haiku 4.5 ($1/$5) [re-verified, unchanged]
58
+ // The live page states Sonnet 5 as $2/$10 flatly — NO introductory qualifier and
59
+ // no scheduled increase. (An earlier launch note describing $2/$10 as
60
+ // introductory through 2026-08-31 is superseded; a cached model table that still
61
+ // carries the qualifier is stale, which is precisely why this file re-verifies
62
+ // against the live page rather than a recalled figure.)
63
+ //
64
+ // WHY IT MATTERED, and why it was worth its own task: the grader's default worker
65
+ // model is claude-sonnet-5 (modules/grading/grader-rubric.json), so the panel is
66
+ // this platform's heaviest sonnet consumer, and ADR 0054 pays each shipped session
67
+ // round(true_cost_usd × 1.20). An over-priced sonnet row therefore OVER-PAID real
68
+ // credits — the mirror image of the fable bug recorded above, which under-paid.
69
+ //
70
+ // HISTORICAL ROWS ARE NOT RE-PRICED. cost_log rows written before today keep the
71
+ // dollars we believed at the time: the ledger is a record of what was charged and
72
+ // paid, and rewriting paid rows to a new rate would make the credit history
73
+ // disagree with the credit_log it produced. Expect a visible step change in
74
+ // cost-per-ship around 2026-08-18 for sonnet-heavy work; this note is its
75
+ // explanation.
76
+ //
77
+ // THE FAMILY↔MODEL BINDING IS NOW DATA (FAMILY_CURRENT_MODEL below) and pinned by
78
+ // tests/llm-pricing.mjs: a family whose current model is re-pinned cannot keep the
79
+ // old model's rate silently, which is exactly the failure this task fixed.
80
+ // The model each family's rate is quoted FROM. Data, not logic: nothing branches
81
+ // on it, but it makes the claim "this row is <model>'s price" checkable — the test
82
+ // asserts each family's rate against that model's published figures, so re-pinning
83
+ // a family to a newer model without re-pricing it reds the build (task 1003001).
84
+ // Legacy members of a family are knowingly mis-priced (the family-pricing trade-off
85
+ // documented above): claude-sonnet-4-6 is really $3/$15 and prices here as Sonnet 5.
86
+ const FAMILY_CURRENT_MODEL = Object.freeze({
87
+ fable: 'claude-fable-5',
88
+ opus: 'claude-opus-5',
89
+ sonnet: 'claude-sonnet-5',
90
+ haiku: 'claude-haiku-4-5',
91
+ });
92
+
56
93
  const PRICES = {
57
94
  // family : { input, output, cache_write_5m, cache_write_1h, cache_read } (USD / 1e6 tokens)
58
95
  fable: { input: 10, output: 50, cache_write_5m: 12.5, cache_write_1h: 20, cache_read: 1 },
59
96
  opus: { input: 5, output: 25, cache_write_5m: 6.25, cache_write_1h: 10, cache_read: 0.5 },
60
- sonnet: { input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 },
97
+ sonnet: { input: 2, output: 10, cache_write_5m: 2.5, cache_write_1h: 4, cache_read: 0.2 },
61
98
  haiku: { input: 1, output: 5, cache_write_5m: 1.25, cache_write_1h: 2, cache_read: 0.1 },
62
99
  };
63
100
 
@@ -246,4 +283,4 @@ function componentsFromModelUsage(modelUsage) {
246
283
  };
247
284
  }
248
285
 
249
- module.exports = { PRICES, familyFor, priceUsage, canonicalizeModelId, priceModelUsage, componentsFromModelUsage };
286
+ module.exports = { PRICES, FAMILY_CURRENT_MODEL, familyFor, priceUsage, canonicalizeModelId, priceModelUsage, componentsFromModelUsage };
package/src/module-api.js CHANGED
@@ -909,8 +909,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
909
909
  // landed since 1.19.239 with no explicit bump. run 32182178305. (task 1002620)
910
910
  // 1.19.241 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
911
911
  // landed since 1.19.240 with no explicit bump. run 32184475745. (task 1002620)
912
+ // 1.19.242 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
913
+ // landed since 1.19.241 with no explicit bump. run 32185198139. (task 1002620)
914
+ // 1.19.243 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
915
+ // landed since 1.19.242 with no explicit bump. run 32193666731. (task 1002620)
912
916
  // ---------------------------------------------------------------------------
913
- const CORE_VERSION = '1.19.241'; // CI auto-patch carrier (ADR 0161); see changelog above
917
+ const CORE_VERSION = '1.19.243'; // CI auto-patch carrier (ADR 0161); see changelog above
914
918
 
915
919
  // A namespaced logger so a module's log lines are attributable + consistent.
916
920
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -5,12 +5,17 @@
5
5
  // Re-VERIFIED + extended 2026-08-16 (task 1002883): fable (Fable 5 / Mythos 5)
6
6
  // $10/$50 — the family familyFor() used to miss, so Fable sessions priced at the
7
7
  // opus fallback and cost-plus under-paid by half.
8
+ // Re-VERIFIED 2026-08-18 (task 1003001): sonnet is CLAUDE SONNET 5 at $2/$10 —
9
+ // the row had carried Sonnet 4.6's $3/$15, over-pricing the grader panel (whose
10
+ // default worker IS claude-sonnet-5) and therefore over-paying cost-plus credits.
11
+ // The family↔model binding is now data (FAMILY_CURRENT_MODEL) and is pinned below,
12
+ // so a future re-pin cannot keep an old model's rate silently.
8
13
  // Run: node tests/llm-pricing.mjs
9
14
 
10
15
  import { strict as assert } from 'node:assert';
11
16
  import { createRequire } from 'node:module';
12
17
  const require = createRequire(import.meta.url);
13
- const { priceUsage, familyFor, PRICES, canonicalizeModelId, priceModelUsage } = require('../src/bongos/llm-pricing.js');
18
+ const { priceUsage, familyFor, PRICES, FAMILY_CURRENT_MODEL, canonicalizeModelId, priceModelUsage } = require('../src/bongos/llm-pricing.js');
14
19
 
15
20
  let passed = 0, failed = 0;
16
21
  function test(name, fn) {
@@ -101,11 +106,68 @@ test('opus 4.8: 1M input + 1M output = $5 + $25 = $30', () => {
101
106
  assert.equal(r.family, 'opus');
102
107
  });
103
108
 
104
- test('sonnet input ($3) vs haiku output ($5) use their own rates', () => {
105
- assert.ok(near(priceUsage('claude-sonnet-4-6', { input_tokens: M }).usd, 3));
109
+ test('sonnet input ($2 — Sonnet 5) vs haiku output ($5) use their own rates', () => {
110
+ assert.ok(near(priceUsage('claude-sonnet-5', { input_tokens: M }).usd, 2));
106
111
  assert.ok(near(priceUsage('claude-haiku-4-5', { output_tokens: M }).usd, 5));
107
112
  });
108
113
 
114
+ test('the grader panel prices at Sonnet 5, not Sonnet 4.6 — 1M in + 1M out = $12', () => {
115
+ // The regression this task fixed, stated as money: the panel's default worker is
116
+ // claude-sonnet-5 (modules/grading/grader-rubric.json), and the row used to say
117
+ // $3/$15 = $18 for the same tokens. ADR 0054 pays round(true_cost × 1.20), so the
118
+ // extra $6 per million became real over-paid credits.
119
+ const r = priceUsage('claude-sonnet-5', { input_tokens: M, output_tokens: M });
120
+ assert.ok(near(r.usd, 12), `got ${r.usd}`);
121
+ assert.equal(r.family, 'sonnet');
122
+ assert.equal(r.unknown_model, false);
123
+ });
124
+
125
+ test('a LEGACY family member is knowingly mis-priced — sonnet 4.6 rides Sonnet 5 rates', () => {
126
+ // Family pricing assumes the CURRENT model in each family (the file says so).
127
+ // claude-sonnet-4-6 really costs $3/$15; here it prices at $2/$10. This test
128
+ // exists so that trade-off is a recorded decision rather than a surprise — if the
129
+ // platform ever runs a legacy model in earnest, this is the test that should fail
130
+ // first and force a per-model override.
131
+ assert.ok(near(priceUsage('claude-sonnet-4-6', { input_tokens: M }).usd, 2));
132
+ });
133
+
134
+ // ── the family↔model binding (task 1003001) ─────────────────────────────────
135
+ // Each family's rate is quoted from ONE named model. These literals are the
136
+ // published per-model figures, kept separate from PRICES on purpose: if someone
137
+ // re-pins a family to a newer model, the two copies disagree and this test names
138
+ // the model in its failure — the drift that shipped Sonnet 4.6's price under a
139
+ // Sonnet 5 workload cannot recur silently.
140
+ const PUBLISHED = {
141
+ 'claude-fable-5': { input: 10, output: 50 },
142
+ 'claude-opus-5': { input: 5, output: 25 },
143
+ 'claude-sonnet-5': { input: 2, output: 10 },
144
+ 'claude-haiku-4-5': { input: 1, output: 5 },
145
+ };
146
+
147
+ test('every family rate matches the published price of the model it names', () => {
148
+ for (const [family, modelId] of Object.entries(FAMILY_CURRENT_MODEL)) {
149
+ const published = PUBLISHED[modelId];
150
+ assert.ok(published, `FAMILY_CURRENT_MODEL.${family} names ${modelId}, which this test has no published price for — add it (verified against the live models page), do not guess`);
151
+ assert.equal(PRICES[family].input, published.input, `${family} input should be ${modelId}'s $${published.input}`);
152
+ assert.equal(PRICES[family].output, published.output, `${family} output should be ${modelId}'s $${published.output}`);
153
+ }
154
+ });
155
+
156
+ test('every family names a model familyFor() actually resolves back to it', () => {
157
+ for (const [family, modelId] of Object.entries(FAMILY_CURRENT_MODEL)) {
158
+ assert.equal(familyFor(modelId), family,
159
+ `FAMILY_CURRENT_MODEL.${family} names ${modelId}, but familyFor('${modelId}') says ${familyFor(modelId)} — the binding and the matcher disagree`);
160
+ }
161
+ });
162
+
163
+ test('cache rates ride the published multipliers off each family base (1.25× / 2× / 0.1×)', () => {
164
+ for (const [family, p] of Object.entries(PRICES)) {
165
+ assert.ok(near(p.cache_write_5m, p.input * 1.25), `${family} 5m write should be 1.25× input`);
166
+ assert.ok(near(p.cache_write_1h, p.input * 2), `${family} 1h write should be 2× input`);
167
+ assert.ok(near(p.cache_read, p.input * 0.1), `${family} cache read should be 0.1× input`);
168
+ }
169
+ });
170
+
109
171
  test('cache read is 0.1× input (opus $0.50 / 1M)', () => {
110
172
  const r = priceUsage('claude-opus-4-8', { cache_read_input_tokens: M });
111
173
  assert.ok(near(r.usd, 0.5), `got ${r.usd}`);
@@ -19,6 +19,12 @@
19
19
  // A old-invite-pending | role=builder | membership_kind=member ← WRONG (before)
20
20
  // A old-invite-pending | role=pending | membership_kind=pending ← after the fix
21
21
  //
22
+ // core_220 carried the same defect (task 1003093): an old-release
23
+ // `INSERT … (goal_id, builder_id, role) VALUES (…, 'lead')` — which is exactly what
24
+ // lifecycle_001_goal_first_leads.sql issues on a FRESH install, after the core
25
+ // migrations — landed `role=member, membership_kind=member`, silently demoting every
26
+ // new goal's creator out of manager standing.
27
+ //
22
28
  // The fix is the absence of a DEFAULT while the legacy column is still written, so
23
29
  // NULL means exactly "not written". The DEFAULT (and NOT NULL) belong to step 2,
24
30
  // when the legacy column is dropped. This file pins that invariant statically,
@@ -34,11 +40,17 @@ import { fileURLToPath } from 'node:url';
34
40
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
35
41
  const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
36
42
 
37
- // core_220 (goal membership) carries the SAME defect and is fixed under task
38
- // 1003093 — filed, not forgotten. It is deliberately absent from this list rather
39
- // than silently passing: a class guard that quietly excludes a known-broken member
40
- // of the class is worse than no guard.
43
+ // BOTH expand-phase migrations, since task 1003093 fixed core_220's identical
44
+ // INSERT branch. core_220's version of the bug demoted a goal's own LEAD: on a fresh
45
+ // install, lifecycle_001_goal_first_leads.sql inserts the creator with `role='lead'`
46
+ // AFTER core migrations run, so every new goal's creator landed as a plain member and
47
+ // lost manager standing. Verified against a real Postgres both ways round.
41
48
  const EXPAND_MIGRATIONS = [
49
+ {
50
+ rel: 'migrations/core_220_goal_members_membership_kind.sql',
51
+ table: 'goal_members',
52
+ legacy: 'role',
53
+ },
42
54
  {
43
55
  rel: 'modules/platform-identity/migrations/platform_identity_010_membership_kind.sql',
44
56
  table: 'platform_identity_project_memberships',