@bongos/core 1.19.621 → 1.19.622

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.621",
6
- "core_contract": "1.19.621",
7
- "source_commit": "09c7ba3955b0789f34ce6265e17cfcf52de9f146",
5
+ "core_version": "1.19.622",
6
+ "core_contract": "1.19.622",
7
+ "source_commit": "d7fca718550089ea07dc0180b88bcf356c86e06f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-09T06:59:03.131Z",
9
+ "built_at": "2026-09-09T07:05:49.899Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 464,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2086,
14
+ "functional_verbatim": 2087,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2574,
20
- "tree_sha256": "76d591f3c619056f89124f5d2abb9e8d1fc2745277375693f575438d155faf7e",
19
+ "file_count": 2575,
20
+ "tree_sha256": "b42f3efc74c1736882f7545a20d0ab217990c61535f00a1c38b916f8ee92f029",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/backlog-review/SKILL.md",
@@ -2757,7 +2757,7 @@
2757
2757
  {
2758
2758
  "path": "docs/module-api-changelog.md",
2759
2759
  "mode": "0000644",
2760
- "sha256": "37afa2a8a4f3e0d4b2689de8b45dfafa6c17569ff07575ff4b3d6b1b1393d683"
2760
+ "sha256": "502e05437441903b3c3a5388f43bfc60e1c910005a6ac0ff7ff07e5d2199a3e1"
2761
2761
  },
2762
2762
  {
2763
2763
  "path": "docs/modules-contract.md",
@@ -5597,7 +5597,7 @@
5597
5597
  {
5598
5598
  "path": "modules/lifecycle/db-tasks.js",
5599
5599
  "mode": "0000644",
5600
- "sha256": "15c9efea1fb5a8187fe5bae756777ebeb1a214e47e0434d7a8adeb156bd1e093"
5600
+ "sha256": "65558689ccfcea8f71b98cd51eaf1fb45d39267f7e6f6cf8fc35814da2752a34"
5601
5601
  },
5602
5602
  {
5603
5603
  "path": "modules/lifecycle/db-versions.js",
@@ -7662,12 +7662,12 @@
7662
7662
  {
7663
7663
  "path": "package-lock.json",
7664
7664
  "mode": "0000644",
7665
- "sha256": "cc6fce6807181651ec0ec230ad34473092623805b54a1ac9fbaa84e3092b3f34"
7665
+ "sha256": "f56943090b50301c55cc7bb459c1d2b805aaaee4dc57953071c80268ea3bcac0"
7666
7666
  },
7667
7667
  {
7668
7668
  "path": "package.json",
7669
7669
  "mode": "0000644",
7670
- "sha256": "aa932c27317fc8f47d1939478cd55134c96eb1265357cc37de7765e98fe146fc"
7670
+ "sha256": "0bf4bc86183612ce855831f54233cec5ff5f2f9cbf56925dcc63d28a50338636"
7671
7671
  },
7672
7672
  {
7673
7673
  "path": "public-docs/index.html",
@@ -8922,7 +8922,7 @@
8922
8922
  {
8923
8923
  "path": "scripts/gds/start.js",
8924
8924
  "mode": "0000644",
8925
- "sha256": "061a477c618ca76aba4f3abfb2d22ad89ceb0bee46bac65b139ad8ee2f30413a"
8925
+ "sha256": "d0adb1267fc2d75482a37ba252ebffce15cdd32ef2af178579668776a8dfa3a4"
8926
8926
  },
8927
8927
  {
8928
8928
  "path": "scripts/gds/status.js",
@@ -9397,7 +9397,7 @@
9397
9397
  {
9398
9398
  "path": "src/module-api.js",
9399
9399
  "mode": "0000644",
9400
- "sha256": "155f6fd854a43d20569e168af8238b3a082a03d74360d9e6248650ec453568a8"
9400
+ "sha256": "aa73b7f2356cde88eb58d423517adc364cf6fb7e8c9fd5dc177ae4d73b83e3e2"
9401
9401
  },
9402
9402
  {
9403
9403
  "path": "src/module-loader/catalog.js",
@@ -10159,6 +10159,11 @@
10159
10159
  "mode": "0000644",
10160
10160
  "sha256": "1ab482fea7d56cccd811c760e21bf564c7952cd5c11d5831faa500b6c2eaa331"
10161
10161
  },
10162
+ {
10163
+ "path": "tests/cross_discipline_priority_direction.mjs",
10164
+ "mode": "0000644",
10165
+ "sha256": "d7e67ada5978b56e271439233c4c8cd7bcb2abfada593c4591dee4904a8a6233"
10166
+ },
10162
10167
  {
10163
10168
  "path": "tests/currency_label.mjs",
10164
10169
  "mode": "0000644",
@@ -1691,5 +1691,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1691
1691
  landed since 1.19.619 with no explicit bump. run 34321089991. (task 1002620)
1692
1692
  1.19.621 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1693
1693
  landed since 1.19.620 with no explicit bump. run 34321546662. (task 1002620)
1694
+ 1.19.622 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1695
+ landed since 1.19.621 with no explicit bump. run 34322091367. (task 1002620)
1694
1696
  ---------------------------------------------------------------------------
1695
1697
  ```
@@ -855,11 +855,22 @@ function taskTouchesProtectedPath(task) {
855
855
  // and rank them.
856
856
  //
857
857
  // Ranking key (most relevant first):
858
- // 1. priority DESC a P5 in another lane still beats a P1
858
+ // 1. priority ASC, unset last 1 is the MOST urgent. This is the DB's own
859
+ // convention: CHECK (priority BETWEEN 1 AND 5), and every version-progress
860
+ // view weights work as 6 - COALESCE(priority, 5), so a lower number earns
861
+ // more weight. It is the same order the SQL feeds use — `priority ASC
862
+ // NULLS LAST` in the optimizer, `priority NULLS LAST` in listTasks and the
863
+ // goal rollup. A task with no priority set ranks after an explicit P5,
864
+ // never ahead of a P1.
859
865
  // 2. credits-per-minute DESC — best bang-for-effort among equal priority
860
866
  // 3. est_minutes ASC — shorter wins the tiebreak (easier to try)
861
867
  // 4. id ASC — stable final tiebreak
862
868
  //
869
+ // scripts/gds/start.js keeps a hand-copied mirror of this function (it is a
870
+ // pure HTTP client and cannot import this module). The two are a documented
871
+ // mirror pair: tests/cross_discipline_priority_direction.mjs executes BOTH and
872
+ // fails if either drifts. Change one, change the other.
873
+ //
863
874
  // 'unclassified' tasks are excluded: they're un-triaged, not a real lane to
864
875
  // recommend exploring. If preferred_disciplines is empty (no preference), the
865
876
  // builder already sees everything in the main list, so there is nothing
@@ -876,13 +887,22 @@ function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, lim
876
887
  const mins = Number(t.est_minutes_calibrated ?? t.est_minutes) || 0;
877
888
  return mins > 0 ? credits / mins : 0;
878
889
  };
890
+ // `ASC NULLS LAST` expressed in JS: an absent, blank or non-numeric priority
891
+ // sorts AFTER every explicit 1..5. Deliberately not `Number(t.priority) || 0`
892
+ // — that maps NULL to 0, which under an ascending sort is more urgent than a
893
+ // P1. Infinity is the only value that keeps unset work at the back.
894
+ const prio = (t) => {
895
+ if (t.priority === null || t.priority === undefined || t.priority === '') return Infinity;
896
+ const n = Number(t.priority);
897
+ return Number.isFinite(n) ? n : Infinity;
898
+ };
879
899
  const outside = rows.filter(
880
900
  (t) => t.discipline && t.discipline !== 'unclassified' && !prefSet.has(t.discipline)
881
901
  );
882
902
  outside.sort((a, b) => {
883
- const pa = Number(a.priority) || 0;
884
- const pb = Number(b.priority) || 0;
885
- if (pb !== pa) return pb - pa; // higher priority first
903
+ const pa = prio(a);
904
+ const pb = prio(b);
905
+ if (pa !== pb) return pa - pb; // lower number = more urgent; unset last
886
906
  const ca = cpm(a);
887
907
  const cb = cpm(b);
888
908
  if (cb !== ca) return cb - ca; // higher credits/min first
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.621",
3
+ "version": "1.19.622",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.621",
9
+ "version": "1.19.622",
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.621",
3
+ "version": "1.19.622",
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",
@@ -45,9 +45,9 @@
45
45
  //
46
46
  // Below the main list, a "You might enjoy" section surfaces up to 3 claimable
47
47
  // tasks OUTSIDE the builder's disciplines, ranked by relevance. The ranking
48
- // mirrors db.rankCrossDisciplineRecommendations (the canonical server-side
49
- // home for this logic — kept in sync deliberately; start.js is a pure HTTP
50
- // client and cannot import the server db module).
48
+ // mirrors rankCrossDisciplineRecommendations in modules/lifecycle/db-tasks.js
49
+ // (the canonical server-side home for this logic — kept in sync deliberately;
50
+ // start.js is a pure HTTP client and cannot import the server db module).
51
51
  //
52
52
  //
53
53
  // Returns JSON if --json is passed (for the slash command to parse cleanly).
@@ -470,12 +470,19 @@ function renderWidget({ builder, claims, claimable, total, streak, recommendatio
470
470
  }
471
471
 
472
472
  // Cross-discipline recommendations — mirror of
473
- // db.rankCrossDisciplineRecommendations (src/bongos/db.js). Given the full
474
- // claimable list and the builder's preferred_disciplines, return up to `limit`
475
- // tasks whose discipline is OUTSIDE the builder's preferences, ranked:
476
- // priority DESC, credits/min DESC, est_minutes ASC, id ASC.
473
+ // rankCrossDisciplineRecommendations in modules/lifecycle/db-tasks.js (the
474
+ // canonical copy; it moved there from src/bongos/db.js in the twelve-unit
475
+ // carve). Given the full claimable list and the builder's
476
+ // preferred_disciplines, return up to `limit` tasks whose discipline is OUTSIDE
477
+ // the builder's preferences, ranked:
478
+ // priority ASC (unset last), credits/min DESC, est_minutes ASC, id ASC.
479
+ // Priority 1 is the MOST urgent — the DB's own convention (CHECK (priority
480
+ // BETWEEN 1 AND 5), weighted 6 - COALESCE(priority, 5)); an unset priority
481
+ // sorts after an explicit P5, never ahead of a P1.
477
482
  // 'unclassified' tasks are excluded (un-triaged, not a real lane). Empty
478
483
  // preferences → [] (everything is already in the main list; nothing outside).
484
+ // This copy and the canonical one are pinned together by
485
+ // tests/cross_discipline_priority_direction.mjs — change one, change the other.
479
486
  function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, limit = 3) {
480
487
  const rows = Array.isArray(claimable) ? claimable : [];
481
488
  const prefs = Array.isArray(preferredDisciplines) ? preferredDisciplines : [];
@@ -486,13 +493,21 @@ function rankCrossDisciplineRecommendations(claimable, preferredDisciplines, lim
486
493
  const mins = Number(t.est_minutes_calibrated ?? t.est_minutes) || 0;
487
494
  return mins > 0 ? credits / mins : 0;
488
495
  };
496
+ // `ASC NULLS LAST` in JS — an absent/blank/non-numeric priority sorts after
497
+ // every explicit 1..5. Not `Number(t.priority) || 0`: that maps NULL to 0,
498
+ // which under an ascending sort outranks a P1.
499
+ const prio = (t) => {
500
+ if (t.priority === null || t.priority === undefined || t.priority === '') return Infinity;
501
+ const n = Number(t.priority);
502
+ return Number.isFinite(n) ? n : Infinity;
503
+ };
489
504
  const outside = rows.filter(
490
505
  (t) => t.discipline && t.discipline !== 'unclassified' && !prefSet.has(t.discipline)
491
506
  );
492
507
  outside.sort((a, b) => {
493
- const pa = Number(a.priority) || 0;
494
- const pb = Number(b.priority) || 0;
495
- if (pb !== pa) return pb - pa;
508
+ const pa = prio(a);
509
+ const pb = prio(b);
510
+ if (pa !== pb) return pa - pb;
496
511
  const ca = cpm(a);
497
512
  const cb = cpm(b);
498
513
  if (cb !== ca) return cb - ca;
@@ -973,7 +988,9 @@ async function main() {
973
988
  // Only auto-run when invoked directly (`node start.js …`); a `require()` (the
974
989
  // unit test) imports the pure render helpers without executing the fetch flow —
975
990
  // the claim.js precedent. The owed-rebase warning helpers (ADR 0120 part 5) are
976
- // exported so they can be unit-tested without a live server.
991
+ // exported so they can be unit-tested without a live server, and so is
992
+ // rankCrossDisciplineRecommendations — the mirror test executes THIS copy
993
+ // alongside the canonical one rather than pattern-matching the source.
977
994
  if (require.main === module) {
978
995
  main().catch((err) => {
979
996
  console.error('fatal:', err);
@@ -982,6 +999,7 @@ if (require.main === module) {
982
999
  }
983
1000
 
984
1001
  module.exports = {
1002
+ rankCrossDisciplineRecommendations,
985
1003
  rebaseWarningModel,
986
1004
  rebaseWarningMarkdown,
987
1005
  rebaseWarningWidget,
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.621'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.622'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,196 @@
1
+ // tests/cross_discipline_priority_direction.mjs — priority sorts ONE way
2
+ // (task 1003756).
3
+ //
4
+ // tasks.priority is 1..5 with 1 the MOST urgent. The DB says so in two places
5
+ // that cannot drift (the CHECK constraint and the 6 - COALESCE(priority, 5)
6
+ // weighting every version-progress view uses), and every SQL feed orders
7
+ // `priority ASC NULLS LAST`. rankCrossDisciplineRecommendations used to sort
8
+ // the other way and its header comment asserted the inverse as the global rule,
9
+ // so re-ranking a task moved it the wrong direction in /builder-start's
10
+ // "You might enjoy" section.
11
+ //
12
+ // The function has TWO hand-copied implementations — the canonical one in
13
+ // modules/lifecycle/db-tasks.js and a mirror in scripts/gds/start.js, which is
14
+ // a pure HTTP client and cannot import the server module. This test EXECUTES
15
+ // both against the same fixtures and asserts they agree, so the pair cannot
16
+ // drift apart again.
17
+ //
18
+ // Run: node tests/cross_discipline_priority_direction.mjs
19
+ import assert from 'node:assert/strict';
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { createRequire } from 'node:module';
24
+ import { makeRunner } from './helpers.mjs';
25
+
26
+ const require = createRequire(import.meta.url);
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
29
+
30
+ const CANONICAL = require(path.join(ROOT, 'modules/lifecycle/db-tasks.js'))
31
+ .rankCrossDisciplineRecommendations;
32
+ const MIRROR = require(path.join(ROOT, 'scripts/gds/start.js'))
33
+ .rankCrossDisciplineRecommendations;
34
+
35
+ // Both copies, run over the same input. Every behavioural assertion below goes
36
+ // through this so a fix applied to only one file fails the suite.
37
+ const IMPLS = [['db-tasks.js (canonical)', CANONICAL], ['start.js (mirror)', MIRROR]];
38
+ const bothRank = (rows, prefs, limit) => IMPLS.map(([name, fn]) => [name, fn(rows, prefs, limit)]);
39
+ const eachImpl = (rows, prefs, limit, check) => {
40
+ for (const [name, out] of bothRank(rows, prefs, limit)) check(out, name);
41
+ };
42
+
43
+ // A claimable row. discipline defaults OUTSIDE the caller's prefs (['engineer'])
44
+ // so the partition step never eats the fixture; credits/minutes are equal by
45
+ // default so priority is the only live sort key unless a test says otherwise.
46
+ const row = (id, priority, extra = {}) => ({
47
+ id, priority, discipline: 'design', credits_reward: 10, est_minutes: 10, ...extra,
48
+ });
49
+ const PREFS = ['engineer'];
50
+ const ids = (out) => out.map((t) => t.id);
51
+
52
+ const { test, summary } = makeRunner();
53
+
54
+ // ---- the direction itself ---------------------------------------------------
55
+
56
+ await test('P1 outranks P5 — 1 is the most urgent, in BOTH copies', () => {
57
+ // Input deliberately in the wrong order so a no-op sort cannot pass.
58
+ const rows = [row('p5', 5), row('p3', 3), row('p1', 1)];
59
+ eachImpl(rows, PREFS, 10, (out, name) => {
60
+ assert.deepEqual(ids(out), ['p1', 'p3', 'p5'],
61
+ `${name}: ascending — the DB weights work 6 - COALESCE(priority, 5), so P1 earns the most`);
62
+ });
63
+ });
64
+
65
+ await test('the limit keeps the MOST urgent, not the least', () => {
66
+ const rows = [row('p5', 5), row('p4', 4), row('p1', 1), row('p2', 2)];
67
+ eachImpl(rows, PREFS, 2, (out, name) => {
68
+ assert.deepEqual(ids(out), ['p1', 'p2'],
69
+ `${name}: slicing after a backwards sort would surface the two LEAST urgent tasks`);
70
+ });
71
+ });
72
+
73
+ // ---- the NULL mapping -------------------------------------------------------
74
+ // The old code read priority as `Number(t.priority) || 0`. Under the old DESC
75
+ // sort that put unset work last by accident; under the corrected ASC sort the
76
+ // same expression would rank it 0 — ahead of every P1. These pin the explicit
77
+ // handling that replaced it.
78
+
79
+ await test('an unset priority sorts LAST, not first (the || 0 trap)', () => {
80
+ for (const blank of [null, undefined, '']) {
81
+ const rows = [row('blank', blank), row('p1', 1), row('p5', 5)];
82
+ eachImpl(rows, PREFS, 10, (out, name) => {
83
+ assert.deepEqual(ids(out), ['p1', 'p5', 'blank'],
84
+ `${name}: priority=${JSON.stringify(blank)} must rank after an explicit P5`);
85
+ });
86
+ }
87
+ });
88
+
89
+ await test('a non-numeric priority is treated as unset, not as 0', () => {
90
+ const rows = [row('junk', 'urgent'), row('p5', 5)];
91
+ eachImpl(rows, PREFS, 10, (out, name) => {
92
+ assert.deepEqual(ids(out), ['p5', 'junk'], `${name}: NaN must not outrank a real priority`);
93
+ });
94
+ });
95
+
96
+ await test('a numeric string priority still sorts as its number', () => {
97
+ const rows = [row('five', '5'), row('one', '1')];
98
+ eachImpl(rows, PREFS, 10, (out, name) => {
99
+ assert.deepEqual(ids(out), ['one', 'five'], `${name}: the API can hand back stringified integers`);
100
+ });
101
+ });
102
+
103
+ // ---- the rest of the ranking key, so fixing direction did not disturb it ----
104
+
105
+ await test('credits-per-minute DESC breaks a priority tie', () => {
106
+ const rows = [
107
+ row('cheap', 2, { credits_reward: 10, est_minutes: 100 }),
108
+ row('rich', 2, { credits_reward: 100, est_minutes: 10 }),
109
+ ];
110
+ eachImpl(rows, PREFS, 10, (out, name) => {
111
+ assert.deepEqual(ids(out), ['rich', 'cheap'], `${name}: best bang-for-effort first`);
112
+ });
113
+ });
114
+
115
+ await test('shorter est_minutes, then id, break the remaining ties', () => {
116
+ const rows = [
117
+ row('long', 2, { credits_reward: 10, est_minutes: 10 }),
118
+ row('short', 2, { credits_reward: 5, est_minutes: 5 }),
119
+ ];
120
+ // Equal credits/min (1.0), so est_minutes decides.
121
+ eachImpl(rows, PREFS, 10, (out, name) => {
122
+ assert.deepEqual(ids(out), ['short', 'long'], `${name}: shorter is easier to try`);
123
+ });
124
+ const tied = [row(9, 2), row(3, 2)];
125
+ eachImpl(tied, PREFS, 10, (out, name) => {
126
+ assert.deepEqual(ids(out), [3, 9], `${name}: id ASC is the stable final tiebreak`);
127
+ });
128
+ });
129
+
130
+ await test('partitioning is unchanged: outside-prefs only, no unclassified, empty prefs → []', () => {
131
+ const rows = [
132
+ row('mine', 1, { discipline: 'engineer' }),
133
+ row('untriaged', 1, { discipline: 'unclassified' }),
134
+ row('theirs', 4, { discipline: 'design' }),
135
+ ];
136
+ eachImpl(rows, PREFS, 10, (out, name) => {
137
+ assert.deepEqual(ids(out), ['theirs'], `${name}: only lanes outside the builder's prefs`);
138
+ });
139
+ eachImpl(rows, [], 10, (out, name) => {
140
+ assert.deepEqual(out, [], `${name}: no stated preference → nothing is "outside"`);
141
+ });
142
+ });
143
+
144
+ await test('the two copies agree row-for-row on a mixed corpus', () => {
145
+ const disciplines = ['design', 'ops', 'research', 'engineer', 'unclassified'];
146
+ const priorities = [1, 2, 3, 4, 5, null, undefined, '', '2', 'junk'];
147
+ const rows = [];
148
+ for (let i = 0; i < 60; i++) {
149
+ rows.push(row(i, priorities[i % priorities.length], {
150
+ discipline: disciplines[i % disciplines.length],
151
+ credits_reward: (i % 7) * 5,
152
+ est_minutes: (i % 4) * 15,
153
+ }));
154
+ }
155
+ const [[, fromCanonical], [, fromMirror]] = bothRank(rows, ['engineer'], 25);
156
+ assert.ok(fromCanonical.length > 5, 'fixture must actually exercise the ranking');
157
+ assert.deepEqual(ids(fromMirror), ids(fromCanonical),
158
+ 'start.js is a hand-copied mirror — it must produce the identical order');
159
+ });
160
+
161
+ // ---- the convention this direction is anchored to ---------------------------
162
+ // If someone ever inverts the DB's meaning of priority, these fail and force the
163
+ // behavioural assertions above to be reconsidered rather than silently re-flipped.
164
+
165
+ await test('the DB still says 1 is most urgent (CHECK + the 6 - COALESCE weighting)', () => {
166
+ const schema = read('migrations/003_pms.sql');
167
+ assert.match(schema, /priority\s+integer\s+CHECK \(priority BETWEEN 1 AND 5\)/,
168
+ 'priority is a 1..5 integer');
169
+ assert.match(schema, /6 - COALESCE\(t\.priority, 5\)/,
170
+ 'progress weighting subtracts from 6, so a LOWER priority number earns MORE weight');
171
+ assert.match(read('src/bongos/optimizer.js'), /priority ASC NULLS LAST/,
172
+ 'the optimizer — the closest analogue to this recommender — orders ascending');
173
+ });
174
+
175
+ await test('neither copy claims priority DESC, and the mirror pointer resolves', () => {
176
+ const canonicalSrc = read('modules/lifecycle/db-tasks.js');
177
+ const mirrorSrc = read('scripts/gds/start.js');
178
+ for (const [name, src] of [['db-tasks.js', canonicalSrc], ['start.js', mirrorSrc]]) {
179
+ assert.ok(!/priority DESC/.test(src), `${name}: no comment may assert priority DESC`);
180
+ }
181
+ // The canonical home moved out of src/bongos/db.js in the twelve-unit carve;
182
+ // start.js pointed at the old address long after the function had left it.
183
+ assert.ok(
184
+ !/rankCrossDisciplineRecommendations \(src\/bongos\/db\.js\)/.test(mirrorSrc)
185
+ && !/db\.rankCrossDisciplineRecommendations/.test(mirrorSrc),
186
+ 'start.js must name modules/lifecycle/db-tasks.js as the canonical home'
187
+ );
188
+ assert.match(mirrorSrc, /modules\/lifecycle\/db-tasks\.js/,
189
+ 'the mirror names where the canonical copy actually lives');
190
+ assert.ok(
191
+ !/rankCrossDisciplineRecommendations/.test(read('src/bongos/db.js')),
192
+ 'src/bongos/db.js really does not define it — the old pointer was dangling'
193
+ );
194
+ });
195
+
196
+ summary();