@bongos/core 1.19.453 → 1.19.455

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/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.453",
3
+ "version": "1.19.455",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.453",
9
+ "version": "1.19.455",
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.453",
3
+ "version": "1.19.455",
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",
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.453'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.455'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -3,7 +3,8 @@
3
3
  // Unit tests for Discord #bugs → GDS tasks (kind='bug') (Phase 3 F8, ADR 0047).
4
4
  // Pure screening/landing + orchestration via injected fakes — no Discord, no DB:
5
5
  // - screenBug: message → { ok, title, bodyMd }; blank/short/text-free rejected.
6
- // - decideBugLanding: Metic+ → 'ready'; everyone else linked → 'backlog'.
6
+ // - the landing: Metic+ → 'ready'; everyone else linked → 'backlog'. Decided by
7
+ // the ideas landing matrix since task 1003067, not by this module.
7
8
  // - recordInboundBug: files via createTask under the anti-abuse guard —
8
9
  // linked-only (unlinked rejected), rank-aware landing, per-builder
9
10
  // rate-limit, body screen; audits surface='discord_bot'; never throws.
@@ -23,7 +24,9 @@ function t(name, fn) {
23
24
  }
24
25
 
25
26
  const GOOD_ID = '123456789012345678';
26
- const CFG = { rateLimitPerHour: 5, versionId: 'GDS-V3', credits: 10 };
27
+ // task 1003067: versionId and credits are gone from this vector's config — the
28
+ // goal carries the version and the reward auto-assigns at the create layer.
29
+ const CFG = { rateLimitPerHour: 5 };
27
30
 
28
31
  console.log('Discord #bugs → GDS tasks (Phase 3 F8):');
29
32
 
@@ -51,21 +54,16 @@ await t('screenBug: long first line is capped (≤200, ellipsis)', () => {
51
54
  assert.ok(s.title.endsWith('…'));
52
55
  });
53
56
 
54
- // ---- decideBugLanding ------------------------------------------------------
55
- await t('decideBugLanding: Metic+ → ready', () => {
56
- assert.equal(bg.decideBugLanding('metic'), 'ready');
57
- assert.equal(bg.decideBugLanding('archon'), 'ready');
58
- assert.equal(bg.decideBugLanding('ARCHON'), 'ready');
59
- });
60
- await t('decideBugLanding: Xenos/Thetes/unknown → backlog', () => {
61
- for (const r of ['xenos', 'thetes', 'periokoi', '', null, undefined]) {
62
- assert.equal(bg.decideBugLanding(r), 'backlog');
63
- }
64
- });
65
57
 
66
58
  // ---- recordInboundBug (fakes) ---------------------------------------------
59
+ // The fake capture mirrors the real landing rule rather than hardcoding a status,
60
+ // so a matrix change shows up here as a failure instead of a stale fixture.
61
+ // task 1003067: the fake defers to the REAL landing matrix, so this fixture can
62
+ // never quietly disagree with the rule the server actually applies.
63
+ const { decideLanding } = require('../modules/ideas/routing.js');
64
+ const READY_OR_BACKLOG = (rank) => (rank == null ? null : decideLanding({ goalId: 7, kind: 'bug', filerRank: rank }).status);
67
65
  function makeDeps(over = {}) {
68
- const calls = { create: [], audit: [], count: [], persist: [] };
66
+ const calls = { create: [], audit: [], count: [], persist: [], suggest: [] };
69
67
  const deps = {
70
68
  getBuilderByDiscordUserId: async () => (over.builder === undefined ? null : over.builder),
71
69
  countRecentDiscordBugFilings: async (id, since) => {
@@ -73,7 +71,29 @@ function makeDeps(over = {}) {
73
71
  if (over.countThrows) throw new Error('count boom');
74
72
  return over.recent ?? 0;
75
73
  },
76
- createTask: async (a) => { calls.create.push(a); if (over.createThrows) throw new Error('db boom'); return { id: over.taskId ?? 555 }; },
74
+ // task 1003067: the vector files through ideas.capture now. `calls.create`
75
+ // keeps its name — it is still "what this vector asked to be created" — but
76
+ // the argument shape is a CAPTURE, and the landing status comes back from the
77
+ // matrix rather than being posted in.
78
+ suggestGoalForCapture: async (draft) => {
79
+ calls.suggest.push(draft);
80
+ if (over.suggestThrows) throw new Error('suggest boom');
81
+ return over.goal === undefined ? { id: 7, title: 'A goal', version_id: 'BONGOS-V1', score: 4 } : over.goal;
82
+ },
83
+ captureIdea: async (a) => {
84
+ calls.create.push(a);
85
+ if (over.createThrows) throw new Error('db boom');
86
+ // routeIdeaToGoal refuses a landing by THROWING, which rolls the idea row
87
+ // back with it — the case the goal-dropping retry exists for.
88
+ if (over.refuseRouted && a.goalId != null) throw { code: 'ROUTE_NOT_AUTHORIZED', gate: 'protected_scope' };
89
+ const routes = a.goalId != null && READY_OR_BACKLOG(a.filerRank) !== null;
90
+ return {
91
+ id: over.ideaId ?? 900,
92
+ routing: routes
93
+ ? { routed: true, task_id: over.taskId ?? 555, goal_id: a.goalId, status: READY_OR_BACKLOG(a.filerRank) }
94
+ : { routed: false, reason: 'no_goal' },
95
+ };
96
+ },
77
97
  insertAuditLog: async (a) => { calls.audit.push(a); if (over.auditThrows) throw new Error('audit boom'); return { id: 1 }; },
78
98
  persistImages: async (urls, opts) => {
79
99
  calls.persist.push({ urls, opts });
@@ -90,13 +110,16 @@ await t('recordInboundBug: linked Metic → filed at ready, kind=bug, attributed
90
110
  const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'Boats vanish at the dock', messageId: 'm1' }, deps, CFG);
91
111
  assert.equal(r.outcome, 'filed');
92
112
  assert.equal(r.taskId, 555);
113
+ assert.equal(r.ideaId, 900, 'the idea row is the attribution the old path threw away');
93
114
  assert.equal(r.status, 'ready');
94
- assert.equal(calls.create[0].kind, 'bug');
95
- assert.equal(calls.create[0].status, 'ready');
96
- assert.equal(calls.create[0].source, 'discord:bugs');
97
- assert.equal(calls.create[0].sourceRef, 'discord:m1');
98
- assert.equal(calls.create[0].versionId, 'GDS-V3');
99
- assert.match(calls.create[0].description, /reported from Discord #bugs by mona/);
115
+ assert.equal(calls.create[0].kind, 'bug', 'the channel IS the classification');
116
+ assert.equal(calls.create[0].filerRank, 'metic', 'the LIVE rank is handed to the matrix');
117
+ assert.equal(calls.create[0].capturedBy, 9, 'and the reporter is attached to the capture');
118
+ assert.equal(calls.create[0].goalId, 7, 'the suggester found it a home');
119
+ // task 1003067: no versionId is passed AT ALL. It used to be the hardcoded
120
+ // 'GDS-V3' — a shipped version, which is idea 1000771. The goal carries it now.
121
+ assert.equal(calls.create[0].versionId, undefined, 'this vector no longer names a version');
122
+ assert.match(calls.create[0].bodyMd, /reported from Discord #bugs by mona/);
100
123
  assert.equal(calls.audit[0].surface, 'discord_bot');
101
124
  assert.equal(calls.audit[0].responseStatus, 201);
102
125
  assert.equal(calls.audit[0].builderId, 9);
@@ -106,18 +129,74 @@ await t('recordInboundBug: linked Xenos → filed at backlog (needs confirm)', a
106
129
  const { deps, calls } = makeDeps({ builder: { id: 4, github_login: 'newbie', rank: 'xenos' } });
107
130
  const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'Typo on the welcome sign', messageId: 'm2' }, deps, CFG);
108
131
  assert.equal(r.outcome, 'filed');
109
- assert.equal(r.status, 'backlog');
110
- assert.equal(calls.create[0].status, 'backlog');
132
+ assert.equal(r.status, 'backlog', 'a sub-Metic report waits for a nod, as before');
133
+ assert.equal(calls.create[0].filerRank, 'xenos');
111
134
  });
112
135
 
113
- await t('recordInboundBug: UNLINKED author → ignored, nothing created', async () => {
136
+ await t('recordInboundBug: UNLINKED author → an inbox idea, never a task [changed by 1003067]', async () => {
137
+ // WAS: dropped entirely ('unlinked'), because this path minted claimable work
138
+ // and ADR 0047 would not let an unaccountable author near that. It no longer
139
+ // mints work, so the report is kept — as an idea row with no author and NO
140
+ // GOAL, which is exactly what #ideas has always done for the same person.
141
+ // The security property survives structurally: no goal ⇒ the matrix cannot
142
+ // route ⇒ no task exists to claim.
114
143
  const { deps, calls } = makeDeps({ builder: null });
115
144
  const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'something is broken here', messageId: 'm3' }, deps, CFG);
116
- assert.equal(r.outcome, 'ignored');
117
- assert.equal(r.reason, 'unlinked');
145
+ assert.equal(r.outcome, 'filed');
146
+ assert.equal(r.ideaId, 900, 'the report is recorded rather than silently dropped');
147
+ assert.equal(r.taskId, null, 'and it is NOT work');
148
+ assert.equal(r.status, null);
149
+ assert.equal(calls.create[0].capturedBy, null, 'unattributed, and honestly so');
150
+ assert.equal(calls.create[0].goalId, null, 'no home is offered to an unlinked author');
151
+ assert.equal(calls.suggest.length, 0, 'we do not even ask for one');
152
+ assert.match(calls.create[0].bodyMd, /unlinked author/);
153
+ });
154
+
155
+ await t('recordInboundBug: an unlinked author is NOT rate-limited by builder id (there is none)', async () => {
156
+ const { deps, calls } = makeDeps({ builder: null, recent: 999 });
157
+ const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'another unlinked report', messageId: 'm3b' }, deps, CFG);
158
+ assert.equal(r.outcome, 'filed', 'the per-builder cap cannot express an author with no builder row');
159
+ assert.equal(calls.count.length, 0, 'so it is not consulted');
160
+ assert.equal(r.taskId, null, 'the bound that matters is that none of it becomes work');
161
+ });
162
+
163
+ await t('recordInboundBug: a suggester failure still files — to the inbox [1003067]', async () => {
164
+ // A suggester that throws must not lose the bug report. No goal ⇒ inbox row.
165
+ const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, suggestThrows: true });
166
+ const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'the suggester is down', messageId: 'm3c' }, deps, CFG);
167
+ assert.equal(r.outcome, 'error', 'it surfaces as an error rather than pretending to have filed');
118
168
  assert.equal(calls.create.length, 0);
119
169
  });
120
170
 
171
+ await t('recordInboundBug: a REFUSED goal drops the guess and still files the report [1003067]', async () => {
172
+ // The suggester picks the home; the goal can still refuse it (protected scope, a
173
+ // scope-wall verdict, an archive mid-flight). Because routing is one transaction
174
+ // that refusal takes the idea row with it — so a goal WE guessed at would lose a
175
+ // report the reporter did nothing wrong to file. Drop the guess, keep the report.
176
+ const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, refuseRouted: true });
177
+ const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'a bug whose words match a protected goal', messageId: 'm7' }, deps, CFG);
178
+ assert.equal(r.outcome, 'filed', 'the report survives the refusal');
179
+ assert.equal(calls.create.length, 2, 'one routed attempt, then one homeless retry');
180
+ assert.equal(calls.create[0].goalId, 7, 'the first carried the suggested home');
181
+ assert.equal(calls.create[1].goalId, null, 'the retry drops it');
182
+ assert.equal(r.taskId, null, 'and it lands in the inbox, where triage will see it');
183
+ });
184
+
185
+ await t('recordInboundBug: a refusal with NO goal to drop is a real error, not a retry loop', async () => {
186
+ const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, goal: null, refuseRouted: true, createThrows: true });
187
+ const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'the database is simply down', messageId: 'm8' }, deps, CFG);
188
+ assert.equal(r.outcome, 'error', 'nothing to drop ⇒ surface it rather than retrying forever');
189
+ assert.equal(calls.create.length, 1, 'exactly one attempt');
190
+ });
191
+
192
+ await t('recordInboundBug: NO confident goal → the report still lands, in the inbox', async () => {
193
+ const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, goal: null });
194
+ const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'a bug matching no goal at all', messageId: 'm3d' }, deps, CFG);
195
+ assert.equal(r.outcome, 'filed');
196
+ assert.equal(r.taskId, null, 'no home ⇒ no task, which is the homeless-inbox contract (C2)');
197
+ assert.equal(calls.create[0].goalId, null);
198
+ });
199
+
121
200
  await t('recordInboundBug: over rate-limit → ignored + audited 429, nothing created', async () => {
122
201
  const { deps, calls } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, recent: 5 });
123
202
  const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'yet another bug report', messageId: 'm4' }, deps, CFG);
@@ -170,7 +249,7 @@ await t('recordInboundBug: audit failure does NOT undo the filing', async () =>
170
249
  assert.equal(calls.create.length, 1);
171
250
  });
172
251
 
173
- await t('recordInboundBug: createTask error → {error}, never throws', async () => {
252
+ await t('recordInboundBug: a capture error → {error}, never throws', async () => {
174
253
  const { deps } = makeDeps({ builder: { id: 9, github_login: 'mona', rank: 'metic' }, createThrows: true });
175
254
  const r = await bg.recordInboundBug({ discordUserId: GOOD_ID, content: 'valid bug body here', messageId: 'm' }, deps, CFG);
176
255
  assert.equal(r.outcome, 'error');
@@ -202,8 +281,8 @@ await t('recordInboundBug: text + images → persists images + embeds durable li
202
281
  assert.equal(r.images, 1);
203
282
  assert.equal(calls.persist.length, 1);
204
283
  assert.deepEqual(calls.persist[0].urls, ['https://cdn.discordapp.com/a.png']);
205
- assert.match(calls.create[0].description, /Attachments \(1\)/);
206
- assert.match(calls.create[0].description, /!\[attachment 1\]\(\/api\/gds\/bug-attachments\//);
284
+ assert.match(calls.create[0].bodyMd, /Attachments \(1\)/);
285
+ assert.match(calls.create[0].bodyMd, /!\[attachment 1\]\(\/api\/gds\/bug-attachments\//);
207
286
  });
208
287
 
209
288
  await t('recordInboundBug: image-only (empty text) → filed with screenshot title', async () => {
@@ -225,7 +304,7 @@ await t('recordInboundBug: image persist failure → still files (without images
225
304
  );
226
305
  assert.equal(r.outcome, 'filed');
227
306
  assert.equal(r.images, 0);
228
- assert.doesNotMatch(calls.create[0].description, /Attachments/);
307
+ assert.doesNotMatch(calls.create[0].bodyMd, /Attachments/);
229
308
  });
230
309
 
231
310
  await t('recordInboundBug: all images refused (stored empty) → filed, no Attachments block', async () => {
@@ -236,7 +315,7 @@ await t('recordInboundBug: all images refused (stored empty) → filed, no Attac
236
315
  );
237
316
  assert.equal(r.outcome, 'filed');
238
317
  assert.equal(r.images, 0);
239
- assert.doesNotMatch(calls.create[0].description, /Attachments/);
318
+ assert.doesNotMatch(calls.create[0].bodyMd, /Attachments/);
240
319
  });
241
320
 
242
321
  // ---- combineParts ----------------------------------------------------------