@bongos/core 1.19.880 → 1.19.881

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.
@@ -23,7 +23,7 @@ const { createGoal } = require('./db-goals.js');
23
23
 
24
24
  async function listVersions() {
25
25
  const { rows } = await pool.query(
26
- `SELECT id, name, track, status, scope_doc_path, started_at, shipped_at, done_when
26
+ `SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
27
27
  FROM versions
28
28
  ORDER BY started_at`
29
29
  );
@@ -46,7 +46,7 @@ async function getVersion(id, deps = {}) {
46
46
  throw new Error('getVersion({ forUpdate }) requires deps.client — a row lock outside a transaction holds nothing');
47
47
  }
48
48
  const { rows } = await executorFor(deps, pool).query(
49
- `SELECT id, name, track, status, scope_doc_path, started_at, shipped_at, done_when
49
+ `SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
50
50
  FROM versions WHERE id = $1${deps.forUpdate ? ' FOR UPDATE' : ''}`,
51
51
  [id]
52
52
  );
@@ -167,10 +167,14 @@ async function versionsWithStatus(status, deps = {}) {
167
167
  // goal-less criteria, and ADR 0183 criteria satisfy themselves off LINKED tasks.
168
168
  // Criteria nobody can link a task to can never close. Stamping goal_id here is
169
169
  // what gives them something to hang from.
170
- async function createVersion({ id, name, status = 'planning', track = 'product', scopeDocPath = null, doneWhen = null, criteria = [], firstGoal = null, createdBy = null }, deps = {}) {
171
- // versions.track is NOT NULL with no DB default (migration 003) — omitting it
172
- // here made every create 500. An instance's first version is its product
173
- // track, so that's the default; 'internal' is the platform's own track.
170
+ async function createVersion({ id, name, status = 'planning', scopeDocPath = null, doneWhen = null, criteria = [], firstGoal = null, createdBy = null }, deps = {}) {
171
+ // No `track`: the two-track (product/internal) model was retired by task
172
+ // 1002006 and its code unwound by task 1003665. versions.track survives as a
173
+ // column, NOT NULL but carrying DEFAULT 'product' since core_205, so an INSERT
174
+ // that omits it is legal — which is what lets this path stop naming it. The
175
+ // comment that used to sit here said the column had NO default; that was true
176
+ // when it was written and stale by core_205, and believing it is why five
177
+ // insert paths kept supplying a value nobody read.
174
178
  //
175
179
  // deps.pool is the module's existing test seam (listVersions, versionProgress):
176
180
  // honoured ONLY under NODE_ENV=test, so production can never be pointed at an
@@ -179,10 +183,10 @@ async function createVersion({ id, name, status = 'planning', track = 'product',
179
183
  const txPool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
180
184
  return withTx(async (client) => {
181
185
  const { rows } = await client.query(
182
- `INSERT INTO versions (id, name, status, track, scope_doc_path, done_when)
183
- VALUES ($1, $2, $3, $4, $5, $6)
184
- RETURNING id, name, status, track, scope_doc_path, started_at, shipped_at, done_when`,
185
- [id, name, status, track, scopeDocPath, doneWhen]
186
+ `INSERT INTO versions (id, name, status, scope_doc_path, done_when)
187
+ VALUES ($1, $2, $3, $4, $5)
188
+ RETURNING id, name, status, scope_doc_path, started_at, shipped_at, done_when`,
189
+ [id, name, status, scopeDocPath, doneWhen]
186
190
  );
187
191
  const version = rows[0];
188
192
  // The first goal. Titled for the version it opens unless the caller names one —
@@ -340,7 +344,7 @@ async function promotePlanningVersion(exec) {
340
344
  SET status = 'building', started_at = COALESCE(started_at, now())
341
345
  WHERE id = (SELECT id FROM versions WHERE status = 'planning' ORDER BY id LIMIT 1)
342
346
  AND NOT EXISTS (SELECT 1 FROM versions WHERE status = 'building')
343
- RETURNING id, name, track, status, started_at`
347
+ RETURNING id, name, status, started_at`
344
348
  );
345
349
  return rows[0] ?? null;
346
350
  }
@@ -352,7 +356,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
352
356
  await client.query('BEGIN');
353
357
 
354
358
  const { rows: vRows } = await client.query(
355
- `SELECT id, name, track, status, scope_doc_path, started_at, shipped_at, done_when
359
+ `SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
356
360
  FROM versions WHERE id = $1 FOR UPDATE`,
357
361
  [versionId]
358
362
  );
@@ -576,7 +580,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
576
580
  const { rows: closed } = await client.query(
577
581
  `UPDATE versions SET status = 'shipped', shipped_at = now()
578
582
  WHERE id = $1
579
- RETURNING id, name, track, status, scope_doc_path, started_at, shipped_at, done_when`,
583
+ RETURNING id, name, status, scope_doc_path, started_at, shipped_at, done_when`,
580
584
  [versionId]
581
585
  );
582
586
 
@@ -30,10 +30,6 @@ const { asyncHandler, validateOrRespond } = api;
30
30
  // 'building'; 'shipped'/'frozen' are end states reached via the close flow.
31
31
  const VALID_VERSION_STATUSES = new Set(['planning', 'building', 'shipped', 'frozen']);
32
32
 
33
- // versions.track CHECK (migration 003) — NOT NULL with no DB default, so the
34
- // create path must always supply one. 'product' is the caller-facing default:
35
- // an instance seeding its first version is seeding its product track.
36
- const VALID_VERSION_TRACKS = new Set(['product', 'internal']);
37
33
 
38
34
  module.exports = function buildVersionsRouter() {
39
35
  const router = express.Router();
@@ -99,7 +95,6 @@ module.exports = function buildVersionsRouter() {
99
95
  id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
100
96
  name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
101
97
  status: { type: 'string', maxLength: 32 },
102
- track: { type: 'string', maxLength: 16 },
103
98
  done_when: { type: 'string', maxLength: 8000 },
104
99
  scope_doc_path: { type: 'string', maxLength: 256 },
105
100
  criteria: { type: 'array', maxItems: 100 },
@@ -116,11 +111,6 @@ module.exports = function buildVersionsRouter() {
116
111
  return res.fail('bad_status', 400, { valid: Array.from(VALID_VERSION_STATUSES) });
117
112
  }
118
113
 
119
- const track = body.track || 'product';
120
- if (!VALID_VERSION_TRACKS.has(track)) {
121
- return res.fail('bad_track', 400, { valid: Array.from(VALID_VERSION_TRACKS) });
122
- }
123
-
124
114
  if (await db.getVersion(id)) {
125
115
  return res.fail('version_exists', { status: 409, message: `Version '${id}' already exists.` });
126
116
  }
@@ -187,7 +177,7 @@ module.exports = function buildVersionsRouter() {
187
177
  let version, goal, created;
188
178
  try {
189
179
  ({ version, goal, criteria: created } = await db.createVersion({
190
- id, name, status, track,
180
+ id, name, status,
191
181
  doneWhen: body.done_when ?? null,
192
182
  scopeDocPath: body.scope_doc_path ?? null,
193
183
  criteria,
@@ -65,7 +65,7 @@ async function autoCloseVersionIfComplete(exec, versionId) {
65
65
  AND g.status = 'open'
66
66
  AND ${NOT_MAINTENANCE}
67
67
  )
68
- RETURNING v.id, v.name, v.track, v.status, v.shipped_at`,
68
+ RETURNING v.id, v.name, v.status, v.shipped_at`,
69
69
  [versionId]
70
70
  );
71
71
  return rows[0] ?? null;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.880",
3
+ "version": "1.19.881",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.880",
9
+ "version": "1.19.881",
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.880",
3
+ "version": "1.19.881",
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",
@@ -391,9 +391,6 @@ function versionPayload(spec) {
391
391
  id: v.id,
392
392
  name: v.name,
393
393
  status: v.status || 'building',
394
- // versions.track is NOT NULL (no DB default) — always send it explicitly.
395
- // An instance's first version is its product track unless the spec says otherwise.
396
- track: v.track === 'internal' ? 'internal' : 'product',
397
394
  done_when: v.done_when,
398
395
  criteria: [{ criterion_id: v.criterion_id ? slugify(v.criterion_id) : 'first-goal', criterion_md: v.done_when }],
399
396
  };
@@ -55,8 +55,9 @@ function migrateCmd(inst, { privileged = false } = {}) {
55
55
  // ONLY IF the versions table is empty: idempotent, safe to re-run, and it never
56
56
  // disturbs a project that already has versions. Mirrors seedViaApi's shape — V1
57
57
  // (status 'building') + one done-when criterion so /status has a finish line from day
58
- // one. `track` is set EXPLICITLY: versions.track is NOT NULL with no default (mig 112
59
- // dropped the CHECK), and 'product' matches the canonical V1 seed in 003_pms.sql.
58
+ // one. No `track` column: the two-track model was retired (task 1002006, unwound
59
+ // by task 1003665). The seed runs on a freshly-MIGRATED database, so core_205's
60
+ // DEFAULT 'product' is in place and an INSERT that omits the column is legal.
60
61
  //
61
62
  // The id/name/done_when come from instanceInitSpec so the starter version has ONE
62
63
  // source of truth (the same spec the greenfield `bongos init` seed uses). SQL string
@@ -68,8 +69,8 @@ function seedFirstVersionCmd(inst, { privileged = false } = {}) {
68
69
  const v = instanceInitSpec(inst).version;
69
70
  const q = (s) => `$v$${s}$v$`; // dollar-quoted literal (no value contains a single quote or $)
70
71
  const sql =
71
- `INSERT INTO versions (id, name, track, status, done_when) ` +
72
- `SELECT ${q(v.id)}, ${q(v.name)}, $v$product$v$, $v$building$v$, ${q(v.done_when)} ` +
72
+ `INSERT INTO versions (id, name, status, done_when) ` +
73
+ `SELECT ${q(v.id)}, ${q(v.name)}, $v$building$v$, ${q(v.done_when)} ` +
73
74
  `WHERE NOT EXISTS (SELECT 1 FROM versions); ` +
74
75
  `INSERT INTO done_when_criteria (version_id, criterion_id, criterion_md, sort_order) ` +
75
76
  `SELECT ${q(v.id)}, $v$first-goal$v$, ${q(v.done_when)}, 10 ` +
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.880'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.881'; // 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');
package/tests/init.mjs CHANGED
@@ -204,16 +204,18 @@ test('versionPayload: explicit criterion_id is slugified', () => {
204
204
  assert.equal(init.versionPayload(spec).criteria[0].criterion_id, 'prove-the-loop');
205
205
  });
206
206
 
207
- // versions.track is NOT NULL with no DB default — leaving it off the payload is
208
- // exactly the 1.18.18 bug (every POST /versions 500ed, blocking init outright).
209
- test('versionPayload: always sends an explicit, valid track', () => {
210
- assert.equal(init.versionPayload(fullSpec()).track, 'product'); // instance's first version = product
211
- const internal = fullSpec();
212
- internal.version.track = 'internal';
213
- assert.equal(init.versionPayload(internal).track, 'internal'); // explicit internal respected
214
- const junk = fullSpec();
215
- junk.version.track = 'bogus';
216
- assert.equal(init.versionPayload(junk).track, 'product'); // junk coerced, never forwarded
207
+ // The inverse of the case this replaces. That case pinned "always send an
208
+ // explicit track", guarding the 1.18.18 bug where omitting it 500ed every
209
+ // POST /versions against a NOT NULL column with no default. core_205 gave the
210
+ // column DEFAULT 'product' and task 1003665 retired the concept, so the payload
211
+ // must now NOT carry it — and a spec that still names one must not smuggle it
212
+ // through to a route that no longer accepts the field.
213
+ test('versionPayload: sends no track, and drops one a spec still names', () => {
214
+ assert.equal('track' in init.versionPayload(fullSpec()), false);
215
+ const legacy = fullSpec();
216
+ legacy.version.track = 'internal';
217
+ assert.equal('track' in init.versionPayload(legacy), false,
218
+ 'an old spec file cannot reintroduce the field');
217
219
  });
218
220
 
219
221
  // ---- provisioning arm (ADR 0111 §3, task 1946) ----
@@ -661,12 +663,13 @@ async function runSeedTests() {
661
663
  assert.ok(findTaskPost(api, /Production topology/), 'filed the manual fallback task on error');
662
664
  });
663
665
 
664
- await at('seedViaApi: version POST carries the explicit track (the NOT NULL column)', async () => {
666
+ await at('seedViaApi: the version POST carries NO track (task 1003665)', async () => {
665
667
  const api = fakeApi();
666
668
  await init.seedViaApi(provSpec({}), { log: () => {}, api });
667
669
  const vpost = api.calls.find((c) => c.path.endsWith('/versions'));
668
670
  assert.ok(vpost, 'POSTed the version');
669
- assert.equal(vpost.body.track, 'product');
671
+ assert.equal('track' in vpost.body, false,
672
+ 'POST /versions no longer accepts the field; the DB default supplies the column');
670
673
  });
671
674
 
672
675
  await at('seedViaApi: the ideator kickoff task POSTs kind=decision + discipline=ideator', async () => {
@@ -59,16 +59,21 @@ t('dbCreateCmd / migrateCmd use the slug db + GDS-only migrate', () => {
59
59
  assert.match(P.migrateCmd(coTenant), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 \.\/scripts\/migrate\.sh/);
60
60
  });
61
61
 
62
- t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, track explicit, one criterion — the fix for the empty-versions bug (task 2119)', () => {
62
+ t('seedFirstVersionCmd: idempotent (only-if-empty) V1 seed against the slug db, no track, one criterion — the fix for the empty-versions bug (task 2119)', () => {
63
63
  const cmd = P.seedFirstVersionCmd(coTenant);
64
64
  // targets the instance's OWN db, matching createdb/migrate
65
65
  assert.match(cmd, /psql .*-d mercury /, 'seeds the instance db');
66
66
  assert.match(cmd, /ON_ERROR_STOP=1/, 'aborts on any error (atomic, no half-seed)');
67
67
  // the version the greenfield init spec defines — ONE source of truth
68
68
  const v = P.instanceInitSpec(coTenant).version;
69
- assert.match(cmd, /INSERT INTO versions \(id, name, track, status, done_when\)/);
69
+ assert.match(cmd, /INSERT INTO versions \(id, name, status, done_when\)/);
70
70
  assert.match(cmd, new RegExp(`\\$v\\$${reEsc(v.id)}\\$v\\$`), 'seeds the spec version id (V1)');
71
- assert.match(cmd, /\$v\$product\$v\$/, 'track is set EXPLICITLY (versions.track is NOT NULL, no default since mig 112)');
71
+ // task 1003665 retired the two-track model. The seed names no track: it runs on
72
+ // a freshly-MIGRATED database, so core_205's DEFAULT 'product' fills the still-
73
+ // NOT NULL column. If that default is ever dropped, THIS seed 500s first and a
74
+ // new instance cannot stand up — which is why the assertion is stated that way.
75
+ assert.doesNotMatch(cmd, /\btrack\b/, 'the seed names no track column');
76
+ assert.doesNotMatch(cmd, /\$v\$product\$v\$/, 'nor smuggles its value in positionally');
72
77
  assert.match(cmd, /\$v\$building\$v\$/, 'status building, mirroring seedViaApi');
73
78
  assert.ok(cmd.includes(v.done_when), 'carries the spec done_when verbatim (incl. the → arrows)');
74
79
  // the IDEMPOTENT guard — only seeds when there is NO version yet, so it never disturbs an existing project
@@ -57,7 +57,9 @@ function fakeDb({ goalRow = { id: 77, title: 'Foundations' } } = {}) {
57
57
  query: async (sql, params) => {
58
58
  log.push({ sql, params });
59
59
  if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(sql)) return { rows: [] };
60
- if (/INSERT INTO versions/i.test(sql)) return { rows: [{ id: params[0], name: params[1], status: params[2], track: params[3] }] };
60
+ // params are positional: (id, name, status, scope_doc_path, done_when).
61
+ // `track` sat at index 3 until task 1003665 removed it from the INSERT.
62
+ if (/INSERT INTO versions/i.test(sql)) return { rows: [{ id: params[0], name: params[1], status: params[2] }] };
61
63
  if (/INSERT INTO goals/i.test(sql)) return { rows: [goalRow] };
62
64
  if (/INSERT INTO done_when_criteria/i.test(sql)) {
63
65
  return { rows: [{ id: log.length, version_id: params[0], criterion_id: params[1], criterion_md: params[2], sort_order: params[3], goal_id: params[4], satisfied: false }] };
@@ -83,7 +85,7 @@ const CRITERIA = [
83
85
  console.log('createVersion — the version, its first goal, and its criteria:');
84
86
 
85
87
  const out = await createVersion({
86
- id: 'V1', name: 'Foundations', status: 'building', track: 'product',
88
+ id: 'V1', name: 'Foundations', status: 'building',
87
89
  doneWhen: 'The loop closes.', criteria: CRITERIA, createdBy: 7,
88
90
  }, { pool: db.pool });
89
91
 
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { strict as assert } from 'node:assert';
12
12
  import { createRequire } from 'node:module';
13
+ import fs from 'node:fs';
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
  const { validate } = require('../src/bongos/routes/_helpers.js');
@@ -23,12 +24,14 @@ function test(name, fn) {
23
24
  }
24
25
 
25
26
  // The EXACT schema declared in versions.js → POST /versions validateOrRespond().
26
- // Keep in sync if the route's schema changes.
27
+ // Keep in sync if the route's schema changes — and note that "keep in sync" is a
28
+ // promise a copy cannot keep on its own, so the track case below reads the
29
+ // ROUTE SOURCE rather than this object. A copy that still declared `track` would
30
+ // happily prove the old contract forever.
27
31
  const VERSIONS_POST_SCHEMA = {
28
32
  id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
29
33
  name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
30
34
  status: { type: 'string', maxLength: 32 },
31
- track: { type: 'string', maxLength: 16 },
32
35
  done_when: { type: 'string', maxLength: 8000 },
33
36
  scope_doc_path: { type: 'string', maxLength: 256 },
34
37
  criteria: { type: 'array', maxItems: 100 },
@@ -75,18 +78,40 @@ test('INIT: unknown extra fields pass through (forward-compat)', () => {
75
78
  assert.equal(validate({ id: 'V1', name: 'x', future_field: 42 }, VERSIONS_POST_SCHEMA), null);
76
79
  });
77
80
 
78
- // versions.track is NOT NULL with no DB default (migration 003) — the create
79
- // path must accept it so callers can choose, and default it so omitting it can
80
- // never again 500 the route (the @bongos/core 1.18.18 `bongos init` blocker).
81
- test('INIT: explicit track passes the schema guard (both tracks)', () => {
82
- assert.equal(validate({ id: 'V1', name: 'x', track: 'product' }, VERSIONS_POST_SCHEMA), null);
83
- assert.equal(validate({ id: 'GDS-V5', name: 'x', track: 'internal' }, VERSIONS_POST_SCHEMA), null);
81
+ // The two-track (product/internal) model was retired by task 1002006 and its code
82
+ // unwound by task 1003665: role/discipline is the sole work-type axis. These read
83
+ // the ROUTE'S OWN SOURCE, not the copied schema above — the point is that the
84
+ // concept is gone from the file that enforces it.
85
+ //
86
+ // What made the removal safe, and what would make it unsafe again: versions.track
87
+ // is still NOT NULL, so an INSERT omitting it is legal ONLY because core_205 gave
88
+ // the column DEFAULT 'product'. Drop that default and every create 500s. The
89
+ // comments this task deleted asserted the opposite ("no DB default") and were
90
+ // stale from core_205 onward — which is precisely why five insert paths went on
91
+ // supplying a value nothing read.
92
+ const routeSrc = fs.readFileSync(
93
+ new URL('../modules/lifecycle/routes/versions.js', import.meta.url), 'utf8');
94
+
95
+ test('the route no longer knows what a track is', () => {
96
+ for (const gone of ['VALID_VERSION_TRACKS', 'bad_track']) {
97
+ assert.ok(!routeSrc.includes(gone), `${gone} is gone from POST /versions`);
98
+ }
99
+ assert.ok(!/\btrack\b/.test(routeSrc.replace(/tracked|tracking/g, '')),
100
+ 'no track identifier survives in the versions route');
84
101
  });
85
102
 
86
- test('BAD: non-string track → structured 400 shape, not a DB 500', () => {
87
- const r = validate({ id: 'V1', name: 'x', track: 42 }, VERSIONS_POST_SCHEMA);
88
- assert.ok(r);
89
- assert.ok(r.details.some((d) => d.field === 'track'));
103
+ test('a version is created WITHOUT a track, and that is the whole contract', () => {
104
+ // The create shape `bongos init` and provision-repo.js now send.
105
+ assert.equal(validate({ id: 'V1', name: 'x' }, VERSIONS_POST_SCHEMA), null);
106
+ assert.equal(validate({ id: 'GDS-V5', name: 'x', status: 'planning' }, VERSIONS_POST_SCHEMA), null);
107
+ });
108
+
109
+ test('the INSERT does not name the column either', () => {
110
+ const dbSrc = fs.readFileSync(
111
+ new URL('../modules/lifecycle/db-versions.js', import.meta.url), 'utf8');
112
+ const insert = dbSrc.slice(dbSrc.indexOf('INSERT INTO versions'));
113
+ assert.ok(!/^[^`]*\btrack\b/.test(insert.slice(0, insert.indexOf('`'))),
114
+ 'INSERT INTO versions names no track column — the DB default supplies it');
90
115
  });
91
116
 
92
117
  // ---- BAD shapes must return a structured 400, never throw / 500 ----