@bongos/core 1.19.337 → 1.19.338
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 +29 -29
- package/modules/builder-settings/routes/settings.js +18 -16
- package/modules/dev-box/routes/box.js +25 -23
- package/modules/discord/discord-bot.js +37 -35
- package/modules/discord/discord-role-sync.js +14 -12
- package/modules/government/routes/government.js +15 -13
- package/modules/ideas/routes/inbox.js +16 -14
- package/modules/lifecycle/routes/claims.js +11 -9
- package/modules/lifecycle/routes/goals.js +25 -23
- package/modules/lifecycle/routes/task-write-routes.js +17 -15
- package/modules/lifecycle/routes/tasks.js +21 -19
- package/modules/memory/routes/memory.js +18 -16
- package/modules/platform-identity/routes/sso.js +13 -11
- package/modules/provisioning/routes/provisioning.js +17 -15
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -0
- package/src/bongos/auth-admission.js +9 -6
- package/src/bongos/routes/auth.js +20 -17
- package/src/bongos/routes/backup.js +7 -4
- package/src/bongos/routes/builders.js +17 -14
- package/src/bongos/routes/public.js +9 -6
- package/src/bongos/uptime-poller.js +8 -5
- package/src/module-api.js +42 -6
- package/tests/logger.mjs +67 -0
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
const auth = require('./auth');
|
|
28
28
|
const db = require('./db');
|
|
29
29
|
const ic = require('../../src/module-api');
|
|
30
|
+
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
31
|
+
const log = ic.logger('discord');
|
|
30
32
|
|
|
31
33
|
let client = null; // the live discord.js Client (or null when disabled/down)
|
|
32
34
|
let starting = false; // guard against double-start
|
|
@@ -165,7 +167,7 @@ function makeInboundChannelHandler({ channelId, aggregator, logLabel }) {
|
|
|
165
167
|
messageId: msg.id,
|
|
166
168
|
});
|
|
167
169
|
} catch (err) {
|
|
168
|
-
|
|
170
|
+
log.error(`[discord-bot] ${logLabel} error:`, err.message);
|
|
169
171
|
}
|
|
170
172
|
};
|
|
171
173
|
}
|
|
@@ -192,11 +194,11 @@ function getClient() {
|
|
|
192
194
|
// boot) can't be taken down by Discord.
|
|
193
195
|
async function start() {
|
|
194
196
|
if (!botEnabled()) {
|
|
195
|
-
|
|
197
|
+
log.info('[discord-bot] disabled (DISCORD_BOT_ENABLED is not "true") — not connecting.');
|
|
196
198
|
return null;
|
|
197
199
|
}
|
|
198
200
|
if (client || starting) {
|
|
199
|
-
|
|
201
|
+
log.info('[discord-bot] start() called but a client is already active — skipping.');
|
|
200
202
|
return client;
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -204,7 +206,7 @@ async function start() {
|
|
|
204
206
|
if (!token) {
|
|
205
207
|
// Enabled but no token: a misconfiguration, not a crash. Log loudly and
|
|
206
208
|
// bail so the rest of the server still boots.
|
|
207
|
-
|
|
209
|
+
log.error('[discord-bot] DISCORD_BOT_ENABLED is on but no bot token is configured '
|
|
208
210
|
+ '(set DISCORD_BOT_TOKEN or bot_token in ~/.config/otb/discord-bot.json) — not connecting.');
|
|
209
211
|
return null;
|
|
210
212
|
}
|
|
@@ -240,10 +242,10 @@ async function start() {
|
|
|
240
242
|
c.once(Events.ClientReady, (ready) => {
|
|
241
243
|
const guilds = ready.guilds?.cache?.size ?? 0;
|
|
242
244
|
// ready.user.tag is the bot's name#id — safe to log; the TOKEN never is.
|
|
243
|
-
|
|
245
|
+
log.info(`[discord-bot] connected as ${ready.user.tag} — in ${guilds} guild(s).`);
|
|
244
246
|
if (guilds === 0) {
|
|
245
247
|
const worldName = (() => { try { return ic.branding().identity?.worldName || 'project'; } catch { return 'project'; } })();
|
|
246
|
-
|
|
248
|
+
log.info(`[discord-bot] note: bot is in 0 guilds. Invite it to the ${worldName} `
|
|
247
249
|
+ 'server (F5/role-sync needs it there with Manage Roles) before role/reaction features can act.');
|
|
248
250
|
}
|
|
249
251
|
// Ensure all managed rank roles exist in the guild — creates any missing
|
|
@@ -254,7 +256,7 @@ async function start() {
|
|
|
254
256
|
? ready.guilds?.cache?.get(roleSync.loadGuildId()) || null
|
|
255
257
|
: ready.guilds?.cache?.first() || null;
|
|
256
258
|
roleSync.ensureRankRoles(guild).catch((e) =>
|
|
257
|
-
|
|
259
|
+
log.error('[discord-bot] ensureRankRoles failed:', e.message));
|
|
258
260
|
|
|
259
261
|
// F5 role-sync (ADR 0033 §4): an initial reconcile once connected, then a
|
|
260
262
|
// periodic sweep. Both best-effort — a Discord perms/hierarchy problem
|
|
@@ -278,21 +280,21 @@ async function start() {
|
|
|
278
280
|
// discord.js auto-reconnects transient gateway drops on its own; we only
|
|
279
281
|
// LOG the lifecycle so an operator can see the connection's health in the
|
|
280
282
|
// journal. No custom reconnect loop — that would fight the library.
|
|
281
|
-
c.on(Events.Error, (err) =>
|
|
282
|
-
c.on(Events.Warn, (msg) =>
|
|
283
|
-
c.on(Events.ShardError, (err) =>
|
|
283
|
+
c.on(Events.Error, (err) => log.error('[discord-bot] client error:', err.message));
|
|
284
|
+
c.on(Events.Warn, (msg) => log.warn('[discord-bot] warning:', msg));
|
|
285
|
+
c.on(Events.ShardError, (err) => log.error('[discord-bot] shard error:', err.message));
|
|
284
286
|
c.on(Events.ShardDisconnect, (event, id) =>
|
|
285
|
-
|
|
287
|
+
log.warn(`[discord-bot] shard ${id} disconnected (code ${event?.code}) — discord.js will attempt to reconnect.`));
|
|
286
288
|
c.on(Events.ShardReconnecting, (id) =>
|
|
287
|
-
|
|
289
|
+
log.info(`[discord-bot] shard ${id} reconnecting…`));
|
|
288
290
|
c.on(Events.ShardResume, (id) =>
|
|
289
|
-
|
|
291
|
+
log.info(`[discord-bot] shard ${id} resumed.`));
|
|
290
292
|
// 'invalidated' = the session can't be recovered (e.g. token revoked).
|
|
291
293
|
// discord.js will NOT auto-reconnect; reconnecting would hammer the API.
|
|
292
294
|
// Log loudly, tear down, and leave it for a human — a revoked token is an
|
|
293
295
|
// operator action, not a transient blip.
|
|
294
296
|
c.on(Events.Invalidated, () => {
|
|
295
|
-
|
|
297
|
+
log.error('[discord-bot] session invalidated (token revoked or auth failed) — '
|
|
296
298
|
+ 'destroying client; a human must re-check the token and restart.');
|
|
297
299
|
stop().catch(() => {});
|
|
298
300
|
});
|
|
@@ -303,7 +305,7 @@ async function start() {
|
|
|
303
305
|
// event loop on every 👍 guild-wide (F4 efficiency finding). Config is
|
|
304
306
|
// boot-time anyway (changing it needs a restart, like the kill-switch).
|
|
305
307
|
const shipFeedChannelId = loadShipFeedChannelId();
|
|
306
|
-
|
|
308
|
+
log.info(shipFeedChannelId
|
|
307
309
|
? `[discord-bot] watching #ship-feed (${shipFeedChannelId}) for 👍 upvotes.`
|
|
308
310
|
: '[discord-bot] no DISCORD_SHIP_FEED_CHANNEL_ID set — upvoting is inert (no channel pinned).');
|
|
309
311
|
|
|
@@ -311,14 +313,14 @@ async function start() {
|
|
|
311
313
|
// builder). Resolved once at startup; null → the approval surface is inert
|
|
312
314
|
// (fail-closed — the bot won't act on reactions in arbitrary channels).
|
|
313
315
|
const memberApprovalsChannelId = loadMemberApprovalsChannelId();
|
|
314
|
-
|
|
316
|
+
log.info(memberApprovalsChannelId
|
|
315
317
|
? `[discord-bot] watching #member-approvals (${memberApprovalsChannelId}) for Archon 👍/👎.`
|
|
316
318
|
: '[discord-bot] no DISCORD_MEMBER_APPROVALS_CHANNEL_ID set — member approvals are inert (no channel pinned).');
|
|
317
319
|
|
|
318
320
|
// F6 inbound: the #ideas channel whose messages become idea_inbox entries.
|
|
319
321
|
// Resolved once at startup (config is boot-time). Null → inbound is inert.
|
|
320
322
|
const ideasChannelId = loadIdeasChannelId();
|
|
321
|
-
|
|
323
|
+
log.info(ideasChannelId
|
|
322
324
|
? `[discord-bot] watching #ideas (${ideasChannelId}) for inbound ideas.`
|
|
323
325
|
: '[discord-bot] no DISCORD_IDEAS_CHANNEL_ID set — inbound idea filing is inert (no channel pinned).');
|
|
324
326
|
|
|
@@ -328,7 +330,7 @@ async function start() {
|
|
|
328
330
|
// startup; onFlush is the real filing path (recordInboundIdea).
|
|
329
331
|
const ideaAggregator = inbound.createIdeaAggregator({
|
|
330
332
|
onFlush: (combined) => inbound.recordInboundIdea(combined).then((result) => {
|
|
331
|
-
|
|
333
|
+
log.info(`[discord-bot] #ideas burst → ${result.outcome}${result.ideaId ? ` (idea #${result.ideaId}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
|
|
332
334
|
return result;
|
|
333
335
|
}),
|
|
334
336
|
});
|
|
@@ -337,7 +339,7 @@ async function start() {
|
|
|
337
339
|
// tasks (kind='bug') straight-through, under the anti-abuse guard in
|
|
338
340
|
// discord-bugs.js. Resolved once at startup. Null → bug intake is inert.
|
|
339
341
|
const bugsChannelId = loadBugsChannelId();
|
|
340
|
-
|
|
342
|
+
log.info(bugsChannelId
|
|
341
343
|
? `[discord-bot] watching #bugs (${bugsChannelId}) for inbound bug-tasks.`
|
|
342
344
|
: '[discord-bot] no DISCORD_BUGS_CHANNEL_ID set — inbound bug filing is inert (no channel pinned).');
|
|
343
345
|
|
|
@@ -346,7 +348,7 @@ async function start() {
|
|
|
346
348
|
// real filing path (recordInboundBug). Built once at startup.
|
|
347
349
|
const bugAggregator = bugs.createBugAggregator({
|
|
348
350
|
onFlush: (combined) => bugs.recordInboundBug(combined).then((result) => {
|
|
349
|
-
|
|
351
|
+
log.info(`[discord-bot] #bugs burst → ${result.outcome}${result.taskId ? ` (task #${result.taskId}, ${result.status}, ${result.images || 0} img)` : (result.reason ? ` (${result.reason})` : '')}`);
|
|
350
352
|
return result;
|
|
351
353
|
}),
|
|
352
354
|
});
|
|
@@ -385,9 +387,9 @@ async function start() {
|
|
|
385
387
|
// systemd journal is less access-controlled than audit_log (Archon-only),
|
|
386
388
|
// which already records the id with surface='discord_bot'. Log only the
|
|
387
389
|
// task + outcome here (Hacker finding).
|
|
388
|
-
|
|
390
|
+
log.info(`[discord-bot] 👍 on task #${taskId} → ${result.outcome}`);
|
|
389
391
|
} catch (err) {
|
|
390
|
-
|
|
392
|
+
log.error('[discord-bot] reaction handler error:', err.message);
|
|
391
393
|
}
|
|
392
394
|
});
|
|
393
395
|
|
|
@@ -438,9 +440,9 @@ async function start() {
|
|
|
438
440
|
}
|
|
439
441
|
// Don't log the reacting Discord id (audit_log already records it,
|
|
440
442
|
// Archon-only). Log only the record + outcome.
|
|
441
|
-
|
|
443
|
+
log.info(`[discord-bot] approval ${kind} #${marker.id} (${decision}) → ${result.outcome}`);
|
|
442
444
|
} catch (err) {
|
|
443
|
-
|
|
445
|
+
log.error('[discord-bot] approval handler error:', err.message);
|
|
444
446
|
}
|
|
445
447
|
});
|
|
446
448
|
|
|
@@ -478,9 +480,9 @@ async function start() {
|
|
|
478
480
|
if (!builder) return; // unlinked arrival — nothing to sync
|
|
479
481
|
const roleSync = require('./discord-role-sync');
|
|
480
482
|
const r = await roleSync.syncBuilderRole(builder.id);
|
|
481
|
-
|
|
483
|
+
log.info(`[discord-bot] member joined → role-sync for builder ${builder.id}: ${r.ok ? (r.noop ? 'noop' : 'synced') : r.reason}`);
|
|
482
484
|
} catch (err) {
|
|
483
|
-
|
|
485
|
+
log.error('[discord-bot] member-add handler error:', err.message);
|
|
484
486
|
}
|
|
485
487
|
});
|
|
486
488
|
|
|
@@ -488,14 +490,14 @@ async function start() {
|
|
|
488
490
|
// login() rejects on a bad token / network failure — catch it so a Discord
|
|
489
491
|
// problem never propagates into the HTTP boot path.
|
|
490
492
|
await c.login(token).catch((err) => {
|
|
491
|
-
|
|
493
|
+
log.error('[discord-bot] login failed:', err.message);
|
|
492
494
|
// Tear the half-built client down so a later start() can retry cleanly.
|
|
493
495
|
try { c.destroy(); } catch (_) { /* ignore */ }
|
|
494
496
|
client = null;
|
|
495
497
|
});
|
|
496
498
|
return client;
|
|
497
499
|
} catch (err) {
|
|
498
|
-
|
|
500
|
+
log.error('[discord-bot] failed to start:', err.message);
|
|
499
501
|
client = null;
|
|
500
502
|
return null;
|
|
501
503
|
} finally {
|
|
@@ -508,8 +510,8 @@ async function start() {
|
|
|
508
510
|
function runRoleReconcile(trigger) {
|
|
509
511
|
const roleSync = require('./discord-role-sync');
|
|
510
512
|
roleSync.reconcileAll()
|
|
511
|
-
.then((r) =>
|
|
512
|
-
.catch((err) =>
|
|
513
|
+
.then((r) => log.info(`[discord-bot] role-sync reconcile (${trigger}): ${r.synced}/${r.total} linked builders synced.`))
|
|
514
|
+
.catch((err) => log.error(`[discord-bot] role-sync reconcile (${trigger}) failed:`, err.message));
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
// Run the ADR 0037 channel reconcile sweep (best-effort, lazy-required). Makes
|
|
@@ -518,10 +520,10 @@ function runChannelReconcile(trigger) {
|
|
|
518
520
|
const channels = require('./discord-channels');
|
|
519
521
|
channels.reconcileChannels()
|
|
520
522
|
.then((r) => {
|
|
521
|
-
if (!r.ok)
|
|
522
|
-
else
|
|
523
|
+
if (!r.ok) log.info(`[discord-bot] channel reconcile (${trigger}): skipped (${r.reason}).`);
|
|
524
|
+
else log.info(`[discord-bot] channel reconcile (${trigger}): ${r.applied} applied, ${r.failed} failed of ${r.total}.`);
|
|
523
525
|
})
|
|
524
|
-
.catch((err) =>
|
|
526
|
+
.catch((err) => log.error(`[discord-bot] channel reconcile (${trigger}) failed:`, err.message));
|
|
525
527
|
}
|
|
526
528
|
|
|
527
529
|
// Graceful shutdown — close the gateway connection. Safe to call when never
|
|
@@ -536,9 +538,9 @@ async function stop() {
|
|
|
536
538
|
client = null;
|
|
537
539
|
try {
|
|
538
540
|
await c.destroy();
|
|
539
|
-
|
|
541
|
+
log.info('[discord-bot] disconnected.');
|
|
540
542
|
} catch (err) {
|
|
541
|
-
|
|
543
|
+
log.error('[discord-bot] error during shutdown:', err.message);
|
|
542
544
|
}
|
|
543
545
|
}
|
|
544
546
|
|
|
@@ -27,6 +27,8 @@ const path = require('node:path');
|
|
|
27
27
|
const os = require('node:os');
|
|
28
28
|
const db = require('./db');
|
|
29
29
|
const ic = require('../../src/module-api');
|
|
30
|
+
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
31
|
+
const log = ic.logger('discord');
|
|
30
32
|
|
|
31
33
|
// The live ranks that map to a Discord role badge = the full rank ladder, in
|
|
32
34
|
// order low→high. Sourced from db.LIVE_RANK_LADDER — the server-side single
|
|
@@ -134,7 +136,7 @@ async function syncBuilderRole(builderId, deps = realDeps) {
|
|
|
134
136
|
return { ok: true, add, removed: remove };
|
|
135
137
|
} catch (err) {
|
|
136
138
|
// Most likely a Discord permission/hierarchy problem — log, don't throw.
|
|
137
|
-
|
|
139
|
+
log.error(`[discord-bot] role-sync failed for builder ${builderId}:`, err.message);
|
|
138
140
|
return { ok: false, reason: 'error', error: err.message };
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -177,14 +179,14 @@ async function ensureRankRoles(guild) {
|
|
|
177
179
|
});
|
|
178
180
|
if (!res.ok) {
|
|
179
181
|
const text = await res.text();
|
|
180
|
-
|
|
182
|
+
log.error(`[discord-bot] ensureRankRoles: failed to create ${rank} role (${res.status}): ${text}`);
|
|
181
183
|
continue;
|
|
182
184
|
}
|
|
183
185
|
const role = await res.json();
|
|
184
186
|
created[rank] = role.id;
|
|
185
|
-
|
|
187
|
+
log.info(`[discord-bot] ensureRankRoles: created ${rank} role (id=${role.id})`);
|
|
186
188
|
} catch (e) {
|
|
187
|
-
|
|
189
|
+
log.error(`[discord-bot] ensureRankRoles: error creating ${rank} role:`, e.message);
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -199,9 +201,9 @@ async function ensureRankRoles(guild) {
|
|
|
199
201
|
const cfg = JSON.parse(ic.readConfigFileSync('discord-bot.json') || '{}');
|
|
200
202
|
cfg.role_ids = Object.assign({}, cfg.role_ids || {}, created);
|
|
201
203
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 4), 'utf8');
|
|
202
|
-
|
|
204
|
+
log.info(`[discord-bot] ensureRankRoles: saved ${Object.keys(created).join(', ')} to config.`);
|
|
203
205
|
} catch (e) {
|
|
204
|
-
|
|
206
|
+
log.error('[discord-bot] ensureRankRoles: could not save role ids to config:', e.message);
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
|
|
@@ -241,13 +243,13 @@ async function ensureRankRoles(guild) {
|
|
|
241
243
|
body: JSON.stringify({ color: desired }),
|
|
242
244
|
});
|
|
243
245
|
if (recolorRes.ok) {
|
|
244
|
-
|
|
246
|
+
log.info(`[discord-bot] ensureRankRoles: recolored ${rank} role to #${desired.toString(16).padStart(6, '0')}`);
|
|
245
247
|
} else {
|
|
246
248
|
const text = await recolorRes.text();
|
|
247
|
-
|
|
249
|
+
log.error(`[discord-bot] ensureRankRoles: recolor ${rank} failed (${recolorRes.status}): ${text}`);
|
|
248
250
|
}
|
|
249
251
|
} catch (e) {
|
|
250
|
-
|
|
252
|
+
log.error(`[discord-bot] ensureRankRoles: error recoloring ${rank} role:`, e.message);
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -273,15 +275,15 @@ async function ensureRankRoles(guild) {
|
|
|
273
275
|
});
|
|
274
276
|
if (patchRes.ok) {
|
|
275
277
|
const order = highToLow.filter((r) => finalMap[r]).join(' > ');
|
|
276
|
-
|
|
278
|
+
log.info(`[discord-bot] ensureRankRoles: reordered roles (${order})`);
|
|
277
279
|
} else {
|
|
278
280
|
const text = await patchRes.text();
|
|
279
|
-
|
|
281
|
+
log.error(`[discord-bot] ensureRankRoles: role reorder failed (${patchRes.status}): ${text}`);
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
285
|
} catch (e) {
|
|
284
|
-
|
|
286
|
+
log.error('[discord-bot] ensureRankRoles: error reordering roles:', e.message);
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -25,6 +25,8 @@ const db = require('../db');
|
|
|
25
25
|
const sessionScopes = require('../session-scopes');
|
|
26
26
|
const boardMembership = require('../board-membership');
|
|
27
27
|
const board = require('../board');
|
|
28
|
+
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
29
|
+
const log = api.logger('government');
|
|
28
30
|
|
|
29
31
|
// The port surface. Deliberately narrow: resolution + the catalog reads a gate or
|
|
30
32
|
// the Archon tab needs. It grows with R98-R101 (principals, session-scopes,
|
|
@@ -208,7 +210,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
208
210
|
const ranks = await db.listRanksWithGrants();
|
|
209
211
|
res.json({ ranks });
|
|
210
212
|
} catch (err) {
|
|
211
|
-
|
|
213
|
+
log.error('[gds] GET /government/ranks', err);
|
|
212
214
|
res.fail('ranks_list_failed', { status: 500, message: 'internal error' });
|
|
213
215
|
}
|
|
214
216
|
});
|
|
@@ -242,7 +244,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
242
244
|
const assignments = await db.listAssignments();
|
|
243
245
|
res.json({ assignments });
|
|
244
246
|
} catch (err) {
|
|
245
|
-
|
|
247
|
+
log.error('[gds] GET /government/assignments', err);
|
|
246
248
|
res.fail('assignments_list_failed', { status: 500, message: 'internal error' });
|
|
247
249
|
}
|
|
248
250
|
});
|
|
@@ -272,7 +274,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
272
274
|
if (err && err.code === '23505') {
|
|
273
275
|
return res.fail('rank_exists', 409, { rank_key: rankKey });
|
|
274
276
|
}
|
|
275
|
-
|
|
277
|
+
log.error('[gds] POST /government/ranks', err);
|
|
276
278
|
res.fail('rank_create_failed', { status: 500, message: 'internal error' });
|
|
277
279
|
}
|
|
278
280
|
});
|
|
@@ -307,7 +309,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
307
309
|
const granted = await db.grantRankPermission({ rankKey, permissionKey });
|
|
308
310
|
res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, granted });
|
|
309
311
|
} catch (err) {
|
|
310
|
-
|
|
312
|
+
log.error('[gds] PUT /government/ranks/:rankKey/permissions/:permissionKey', err);
|
|
311
313
|
res.fail('grant_failed', { status: 500, message: 'internal error' });
|
|
312
314
|
}
|
|
313
315
|
});
|
|
@@ -326,7 +328,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
326
328
|
const removed = await db.revokeRankPermission({ rankKey, permissionKey });
|
|
327
329
|
res.json({ ok: true, rank_key: rankKey, permission_key: permissionKey, removed });
|
|
328
330
|
} catch (err) {
|
|
329
|
-
|
|
331
|
+
log.error('[gds] DELETE /government/ranks/:rankKey/permissions/:permissionKey', err);
|
|
330
332
|
res.fail('revoke_failed', { status: 500, message: 'internal error' });
|
|
331
333
|
}
|
|
332
334
|
});
|
|
@@ -361,7 +363,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
361
363
|
const count = await db.resetRankGrants({ rankKey, permissionKeys: keys });
|
|
362
364
|
res.json({ ok: true, rank_key: rankKey, seeded_rank: rank.seeded_rank, permission_count: count });
|
|
363
365
|
} catch (err) {
|
|
364
|
-
|
|
366
|
+
log.error('[gds] POST /government/ranks/:rankKey/reset', err);
|
|
365
367
|
res.fail('reset_failed', { status: 500, message: 'internal error' });
|
|
366
368
|
}
|
|
367
369
|
});
|
|
@@ -391,7 +393,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
391
393
|
if (err && err.code === '23503') {
|
|
392
394
|
return res.fail('builder_not_found', 404, { builder_id: req.body.builder_id });
|
|
393
395
|
}
|
|
394
|
-
|
|
396
|
+
log.error('[gds] POST /government/assignments', err);
|
|
395
397
|
res.fail('assign_failed', { status: 500, message: 'internal error' });
|
|
396
398
|
}
|
|
397
399
|
});
|
|
@@ -409,7 +411,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
409
411
|
const removed = await db.unassignRank({ builderId, rankKey });
|
|
410
412
|
res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
|
|
411
413
|
} catch (err) {
|
|
412
|
-
|
|
414
|
+
log.error('[gds] DELETE /government/assignments/:builderId/:rankKey', err);
|
|
413
415
|
res.fail('unassign_failed', { status: 500, message: 'internal error' });
|
|
414
416
|
}
|
|
415
417
|
});
|
|
@@ -427,7 +429,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
427
429
|
const view = await board.boardRoomView({ closedLimit: 20 });
|
|
428
430
|
res.json({ ...view, viewer_id: String(req.builder.id) });
|
|
429
431
|
} catch (err) {
|
|
430
|
-
|
|
432
|
+
log.error('[gds] GET /government/board/items', err);
|
|
431
433
|
res.fail('board_read_failed', { status: 500, message: 'internal error' });
|
|
432
434
|
}
|
|
433
435
|
});
|
|
@@ -442,7 +444,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
442
444
|
try {
|
|
443
445
|
res.json(await board.constitutionView({ historyLimit: 50 }));
|
|
444
446
|
} catch (err) {
|
|
445
|
-
|
|
447
|
+
log.error('[gds] GET /government/constitution', err);
|
|
446
448
|
res.fail('constitution_read_failed', { status: 500, message: 'internal error' });
|
|
447
449
|
}
|
|
448
450
|
});
|
|
@@ -498,7 +500,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
498
500
|
}
|
|
499
501
|
res.status(201).json({ ok: true, amendment: out.amendment, item: out.item });
|
|
500
502
|
} catch (err) {
|
|
501
|
-
|
|
503
|
+
log.error('[gds] POST /government/board/items', err);
|
|
502
504
|
res.fail('propose_failed', { status: 500, message: 'internal error' });
|
|
503
505
|
}
|
|
504
506
|
});
|
|
@@ -548,7 +550,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
548
550
|
try {
|
|
549
551
|
closure = await board.maybeCloseAfterVote(itemId);
|
|
550
552
|
} catch (closeErr) {
|
|
551
|
-
|
|
553
|
+
log.error('[gds] post-vote close failed (vote recorded, close left to the expiry sweep):', closeErr);
|
|
552
554
|
}
|
|
553
555
|
}
|
|
554
556
|
// Idempotent re-cast reads back the standing vote — 200, not 201, and no
|
|
@@ -561,7 +563,7 @@ module.exports = function buildGovernmentRouter() {
|
|
|
561
563
|
...(closure && closure.closed ? { item_closed: true, outcome: closure.outcome } : {}),
|
|
562
564
|
});
|
|
563
565
|
} catch (err) {
|
|
564
|
-
|
|
566
|
+
log.error('[gds] POST /government/board/items/:itemId/votes', err);
|
|
565
567
|
res.fail('vote_failed', { status: 500, message: 'internal error' });
|
|
566
568
|
}
|
|
567
569
|
});
|
|
@@ -8,6 +8,8 @@ const inbox = require('../inbox');
|
|
|
8
8
|
// and the deterministic scorer (R142) — both pure, both walk the declaration.
|
|
9
9
|
const { validateFieldValues, templateFor, fieldOrder } = require('../templates');
|
|
10
10
|
const { completenessScore, weakestSectionPayload, scoreFieldValues } = require('../completeness');
|
|
11
|
+
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
12
|
+
const log = api.logger('ideas');
|
|
11
13
|
const { validateOrRespond, LIMITS, parseId } = api;
|
|
12
14
|
|
|
13
15
|
const INBOX_STATUS_VALUES = ['open', 'promoted', 'discarded', 'merged'];
|
|
@@ -94,7 +96,7 @@ module.exports = function buildInboxRouter() {
|
|
|
94
96
|
try {
|
|
95
97
|
v = validateFieldValues(body.field_values ?? {}, templateKey ? { templateKey } : undefined);
|
|
96
98
|
} catch (validationErr) {
|
|
97
|
-
|
|
99
|
+
log.error('[gds] POST /inbox field-values validation threw:', validationErr);
|
|
98
100
|
return res.fail('invalid_field_values', 400, { errors: [{ field: null, reason: 'unvalidatable' }], template_key: templateKey ?? null });
|
|
99
101
|
}
|
|
100
102
|
if (!v.ok) return res.fail('invalid_field_values', 400, { errors: v.errors, template_key: templateKey ?? null });
|
|
@@ -155,7 +157,7 @@ module.exports = function buildInboxRouter() {
|
|
|
155
157
|
if (r && r.credited > 0) idea.ideator_credit = r.credited;
|
|
156
158
|
}
|
|
157
159
|
} catch (creditErr) {
|
|
158
|
-
|
|
160
|
+
log.error('[gds] POST /inbox full-idea pass-credit failed (idea captured + graded, credit skipped):', creditErr);
|
|
159
161
|
// no warning surfaced — the idea and its grade are intact; the credit
|
|
160
162
|
// is the only thing missed, and a reconcile/re-grade path can pay it.
|
|
161
163
|
}
|
|
@@ -193,12 +195,12 @@ module.exports = function buildInboxRouter() {
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
} catch (boardErr) {
|
|
196
|
-
|
|
198
|
+
log.error('[gds] POST /inbox board-window open failed (idea captured + graded, window skipped):', boardErr);
|
|
197
199
|
// R11's expiry poller sweeps open items, not missed opens; a missed
|
|
198
200
|
// open is recoverable by resubmission (R13) — capture stays intact.
|
|
199
201
|
}
|
|
200
202
|
} catch (gradeErr) {
|
|
201
|
-
|
|
203
|
+
log.error('[gds] POST /inbox full-grade write failed (idea captured, landing as quick):', gradeErr);
|
|
202
204
|
grade_warning = 'captured_but_ungraded';
|
|
203
205
|
}
|
|
204
206
|
}
|
|
@@ -242,7 +244,7 @@ module.exports = function buildInboxRouter() {
|
|
|
242
244
|
500;
|
|
243
245
|
return res.fail(err.code, status, { ...err });
|
|
244
246
|
}
|
|
245
|
-
|
|
247
|
+
log.error('[gds] POST /inbox', err);
|
|
246
248
|
res.fail('capture_failed', { status: 500, message: 'internal error' });
|
|
247
249
|
}
|
|
248
250
|
});
|
|
@@ -272,7 +274,7 @@ module.exports = function buildInboxRouter() {
|
|
|
272
274
|
const open_count = await inbox.countOpenIdeas({ viewerRank });
|
|
273
275
|
res.json({ ideas, open_count });
|
|
274
276
|
} catch (err) {
|
|
275
|
-
|
|
277
|
+
log.error('[gds] GET /inbox', err);
|
|
276
278
|
res.fail('list_failed', { status: 500, message: 'internal error' });
|
|
277
279
|
}
|
|
278
280
|
});
|
|
@@ -451,7 +453,7 @@ module.exports = function buildInboxRouter() {
|
|
|
451
453
|
if (!idea) return res.fail('idea_not_found', 404);
|
|
452
454
|
res.json({ idea });
|
|
453
455
|
} catch (err) {
|
|
454
|
-
|
|
456
|
+
log.error('[gds] GET /inbox/:id', err);
|
|
455
457
|
res.fail('idea_read_failed', { status: 500, message: 'internal error' });
|
|
456
458
|
}
|
|
457
459
|
});
|
|
@@ -494,7 +496,7 @@ module.exports = function buildInboxRouter() {
|
|
|
494
496
|
try {
|
|
495
497
|
v = validateFieldValues(req.body.field_values, templateKey ? { templateKey } : undefined);
|
|
496
498
|
} catch (validationErr) {
|
|
497
|
-
|
|
499
|
+
log.error('[gds] POST /inbox/:id/resubmit validation threw:', validationErr);
|
|
498
500
|
return res.fail('invalid_field_values', 400, { errors: [{ field: null, reason: 'unvalidatable' }] });
|
|
499
501
|
}
|
|
500
502
|
if (!v.ok) return res.fail('invalid_field_values', 400, { errors: v.errors, template_key: templateKey ?? null });
|
|
@@ -507,7 +509,7 @@ module.exports = function buildInboxRouter() {
|
|
|
507
509
|
let rejection = null;
|
|
508
510
|
if (government && government.latestRejectionForIdea) {
|
|
509
511
|
try { rejection = await government.latestRejectionForIdea(id); }
|
|
510
|
-
catch (e) {
|
|
512
|
+
catch (e) { log.error('[gds] resubmit: rejection lookup failed (continuing without):', e); }
|
|
511
513
|
}
|
|
512
514
|
if (!rejection || !rejection.section_key) {
|
|
513
515
|
const flag = weakestSectionPayload(idea.field_values, templateKey ? { templateKey } : undefined);
|
|
@@ -539,7 +541,7 @@ module.exports = function buildInboxRouter() {
|
|
|
539
541
|
if (r && r.credited > 0) out.ideator_credit = r.credited;
|
|
540
542
|
}
|
|
541
543
|
} catch (creditErr) {
|
|
542
|
-
|
|
544
|
+
log.error('[gds] resubmit: pass-credit failed (regrade intact, credit skipped):', creditErr);
|
|
543
545
|
}
|
|
544
546
|
|
|
545
547
|
let board_window;
|
|
@@ -553,7 +555,7 @@ module.exports = function buildInboxRouter() {
|
|
|
553
555
|
}
|
|
554
556
|
}
|
|
555
557
|
} catch (boardErr) {
|
|
556
|
-
|
|
558
|
+
log.error('[gds] resubmit: board-window open failed (regrade intact, window skipped):', boardErr);
|
|
557
559
|
}
|
|
558
560
|
|
|
559
561
|
res.json({
|
|
@@ -563,7 +565,7 @@ module.exports = function buildInboxRouter() {
|
|
|
563
565
|
...(board_window ? { board_window } : {}),
|
|
564
566
|
});
|
|
565
567
|
} catch (err) {
|
|
566
|
-
|
|
568
|
+
log.error('[gds] POST /inbox/:id/resubmit', err);
|
|
567
569
|
res.fail('resubmit_failed', { status: 500, message: 'internal error' });
|
|
568
570
|
}
|
|
569
571
|
});
|
|
@@ -994,7 +996,7 @@ module.exports = function buildInboxRouter() {
|
|
|
994
996
|
500;
|
|
995
997
|
return res.fail(err.code, status, { ...err });
|
|
996
998
|
}
|
|
997
|
-
|
|
999
|
+
log.error('[gds] PATCH /inbox/:id', err);
|
|
998
1000
|
res.fail('update_failed', { status: 500, message: 'internal error' });
|
|
999
1001
|
}
|
|
1000
1002
|
});
|
|
@@ -1050,7 +1052,7 @@ module.exports = function buildInboxRouter() {
|
|
|
1050
1052
|
if (err && err.code === '23505') {
|
|
1051
1053
|
return res.fail('CRITERION_ALREADY_RATIFIED', 409, { detail: 'A criterion with this proposal ref already exists on the target version.' });
|
|
1052
1054
|
}
|
|
1053
|
-
|
|
1055
|
+
log.error('[gds] POST /inbox/:id/ratify', err);
|
|
1054
1056
|
res.fail('ratify_failed', { status: 500, message: 'internal error' });
|
|
1055
1057
|
}
|
|
1056
1058
|
}
|
|
@@ -12,6 +12,8 @@ const { validateOrRespond, LIMITS, parseId } = api;
|
|
|
12
12
|
// task 1003153: the DEAD dependency edge (a dep that was abandoned, so can never
|
|
13
13
|
// satisfy) — one pure definition shared with the CLI + the detector.
|
|
14
14
|
const { deadDepHint, splitDeadDeps } = require('../dead-deps.js');
|
|
15
|
+
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
16
|
+
const log = api.logger('lifecycle');
|
|
15
17
|
|
|
16
18
|
// V3.R33 (#254): input validation guards. Length caps + shape checks at the
|
|
17
19
|
// top of every write handler. Schemas describe KNOWN-field constraints only —
|
|
@@ -155,7 +157,7 @@ async function gateOneActiveClaimPerSession(req, res, next) {
|
|
|
155
157
|
activeClaimsForSession = await db.getActiveClaimsForSession(creatorSessionId);
|
|
156
158
|
} catch (err) {
|
|
157
159
|
// FAIL-CLOSED. A lookup failure must not let a commingling second claim land.
|
|
158
|
-
|
|
160
|
+
log.error('[gds] gateOneActiveClaimPerSession lookup failed (failing closed):', err);
|
|
159
161
|
return res.fail('session_claim_check_failed', { status: 409, message: 'Could not verify this worktree has no other claim in flight. Try again, or start a fresh worktree for this task.' });
|
|
160
162
|
}
|
|
161
163
|
const decision = shouldRefuseSessionClaim({ activeClaimsForSession, creatorSessionId, worktreeName });
|
|
@@ -188,7 +190,7 @@ async function gateNewcomerClaim(req, res, next) {
|
|
|
188
190
|
}
|
|
189
191
|
next();
|
|
190
192
|
} catch (err) {
|
|
191
|
-
|
|
193
|
+
log.error('[gds] gateNewcomerClaim failed:', err);
|
|
192
194
|
// Fail open — onboarding plumbing must never brick a claim attempt.
|
|
193
195
|
next();
|
|
194
196
|
}
|
|
@@ -249,7 +251,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
249
251
|
try {
|
|
250
252
|
await onboarding.restockForClaim(task);
|
|
251
253
|
} catch (e) {
|
|
252
|
-
|
|
254
|
+
log.info('[gds] newcomer chore restock after claim failed (non-blocking):', e && e.message);
|
|
253
255
|
}
|
|
254
256
|
}
|
|
255
257
|
// V3.R51 (#270): claim.worktree_name is the SERVER-DECIDED name. If the
|
|
@@ -344,7 +346,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
344
346
|
}
|
|
345
347
|
return res.fail(err.code, status, { ...err });
|
|
346
348
|
}
|
|
347
|
-
|
|
349
|
+
log.error('[gds] POST /claims', err);
|
|
348
350
|
res.fail('claim_failed', { status: 500, message: 'internal error' });
|
|
349
351
|
}
|
|
350
352
|
});
|
|
@@ -407,7 +409,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
407
409
|
if (err && err.code === 'BUILDER_INACTIVE') {
|
|
408
410
|
return res.fail('BUILDER_INACTIVE', 403);
|
|
409
411
|
}
|
|
410
|
-
|
|
412
|
+
log.error('[gds] POST /claims/batch', err);
|
|
411
413
|
res.fail('batch_claim_failed', { status: 500, message: 'internal error' });
|
|
412
414
|
}
|
|
413
415
|
});
|
|
@@ -445,7 +447,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
445
447
|
if (err && err.code === 'BATCH_EMPTY') {
|
|
446
448
|
return res.fail('claim_ids_required', 400);
|
|
447
449
|
}
|
|
448
|
-
|
|
450
|
+
log.error('[gds] POST /claims/batch/release', err);
|
|
449
451
|
res.fail('batch_release_failed', { status: 500, message: 'internal error' });
|
|
450
452
|
}
|
|
451
453
|
});
|
|
@@ -479,7 +481,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
479
481
|
if (err && err.code === 'BATCH_EMPTY') {
|
|
480
482
|
return res.fail('task_ids_required', 400);
|
|
481
483
|
}
|
|
482
|
-
|
|
484
|
+
log.error('[gds] POST /claims/batch/validate', err);
|
|
483
485
|
res.fail('batch_validate_failed', { status: 500, message: 'internal error' });
|
|
484
486
|
}
|
|
485
487
|
});
|
|
@@ -543,7 +545,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
543
545
|
500;
|
|
544
546
|
return res.fail(err.code, status, { ...err });
|
|
545
547
|
}
|
|
546
|
-
|
|
548
|
+
log.error('[gds] POST /claims/:id/resolve', err);
|
|
547
549
|
res.fail('resolve_failed', { status: 500, message: 'internal error' });
|
|
548
550
|
}
|
|
549
551
|
});
|
|
@@ -596,7 +598,7 @@ module.exports = function buildClaimsRouter() {
|
|
|
596
598
|
if (err && err.code === 'BAD_CLAIM_ID') {
|
|
597
599
|
return res.fail('bad_claim_id', 400);
|
|
598
600
|
}
|
|
599
|
-
|
|
601
|
+
log.error('[gds] POST /claims/:id/release-on-behalf', err);
|
|
600
602
|
res.fail('release_on_behalf_failed', { status: 500, message: 'internal error' });
|
|
601
603
|
}
|
|
602
604
|
}
|