@bongos/core 1.19.673 → 1.19.675
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 +35 -25
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +6 -3
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +71 -12
- package/docs/api-reference.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/discord/discord-channels.js +107 -0
- package/modules/discord/routes/discord.js +26 -2
- package/modules/lifecycle/db-tasks.js +22 -0
- package/modules/lifecycle/goal-edits.js +34 -0
- package/modules/lifecycle/routes/done-when.js +114 -17
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/discord-channels.js +14 -3
- package/src/module-api.js +1 -1
- package/tests/criterion_goal_attach.mjs +347 -0
- package/tests/discord_channels_snapshot.mjs +198 -0
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
const express = require('express');
|
|
16
16
|
const api = require('../../../src/module-api');
|
|
17
|
+
// Structured + attributable, and the console ratchet counts raw console calls —
|
|
18
|
+
// the api.logger(<module key>) precedent every other module's routes follow.
|
|
19
|
+
const log = api.logger('discord');
|
|
17
20
|
const auth = require('../auth');
|
|
18
21
|
const db = require('../db');
|
|
19
22
|
const shipBroadcast = require('../ship-broadcast');
|
|
@@ -93,7 +96,28 @@ module.exports = function buildDiscordRouter() {
|
|
|
93
96
|
}
|
|
94
97
|
return res.json({ ok: true, guild_id: plan.guildId, op_count: plan.ops.length, ops: plan.ops, warnings: plan.warnings });
|
|
95
98
|
} catch (err) {
|
|
96
|
-
|
|
99
|
+
log.error({ err: err && err.message }, 'GET /discord/channels/plan failed');
|
|
100
|
+
return res.fail('internal_error', 500);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// GET /api/bongos/discord/channels/snapshot — what docs/discord/channels.json
|
|
105
|
+
// WOULD say if it described the guild exactly as it is right now (task 1003510).
|
|
106
|
+
// Read-only in both directions: it writes nothing to Discord and nothing to the
|
|
107
|
+
// repo — the operator reviews the output and commits it, so the layout stays
|
|
108
|
+
// Archon-gated by merge, which is the property ADR 0037 §1 rests on.
|
|
109
|
+
// 503 when the bot is disabled / not connected to a guild.
|
|
110
|
+
// rank: archon — channel administration command.
|
|
111
|
+
router.get('/discord/channels/snapshot', auth.requireBuilder, auth.requirePermission('discord.channels.manage'), async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const channels = require('../discord-channels');
|
|
114
|
+
const snap = channels.computeSnapshotConfig();
|
|
115
|
+
if (!snap.ok) {
|
|
116
|
+
return res.fail('bot_unavailable', 503, { reason: snap.reason });
|
|
117
|
+
}
|
|
118
|
+
return res.json({ ok: true, guild_id: snap.guildId, config: snap.config, warnings: snap.warnings });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
log.error({ err: err && err.message }, 'GET /discord/channels/snapshot failed');
|
|
97
121
|
return res.fail('internal_error', 500);
|
|
98
122
|
}
|
|
99
123
|
});
|
|
@@ -111,7 +135,7 @@ module.exports = function buildDiscordRouter() {
|
|
|
111
135
|
}
|
|
112
136
|
return res.json({ ok: true, applied: result.applied, failed: result.failed, total: result.total, warnings: result.warnings });
|
|
113
137
|
} catch (err) {
|
|
114
|
-
|
|
138
|
+
log.error({ err: err && err.message }, 'POST /discord/channels/reconcile failed');
|
|
115
139
|
return res.fail('internal_error', 500);
|
|
116
140
|
}
|
|
117
141
|
});
|
|
@@ -352,6 +352,27 @@ async function createCriterion({ versionId, criterionId, criterionMd, goalId = n
|
|
|
352
352
|
return rows[0];
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// updateCriterionGoal — move an EXISTING criterion into a goal, or out of one
|
|
356
|
+
// (task 1003509). Criteria used to be born linked and stay that way: goal_id was
|
|
357
|
+
// writable only in createCriterion's INSERT, so the runner-seeded V1 criterion a
|
|
358
|
+
// fresh instance starts with could never join the goal it obviously belongs to,
|
|
359
|
+
// and /progress showed it "(ungrouped)" forever beside a goal reporting
|
|
360
|
+
// criteria: []. Amending it needed SQL, which a hub-provisioned owner has no
|
|
361
|
+
// access to at all (walk finding W5).
|
|
362
|
+
//
|
|
363
|
+
// goalId null DETACHES (back to a version-level criterion). The caller owns every
|
|
364
|
+
// question this raises — that the goal exists and sits on the criterion's own
|
|
365
|
+
// version, who may write it, and whether the move completes the goal; this is the
|
|
366
|
+
// UPDATE and nothing else, exactly as updateTaskGoal below is for tasks.
|
|
367
|
+
async function updateCriterionGoal(id, goalId) {
|
|
368
|
+
const { rows } = await pool.query(
|
|
369
|
+
`UPDATE done_when_criteria SET goal_id = $1 WHERE id = $2
|
|
370
|
+
RETURNING id, version_id, criterion_id, criterion_md, goal_id, sort_order, satisfied`,
|
|
371
|
+
[goalId, id]
|
|
372
|
+
);
|
|
373
|
+
return rows[0] ?? null;
|
|
374
|
+
}
|
|
375
|
+
|
|
355
376
|
async function updateTaskGoal(id, goalId) {
|
|
356
377
|
const { rows } = await pool.query(
|
|
357
378
|
`UPDATE tasks SET goal_id = $1, updated_at = now() WHERE id = $2 RETURNING *`,
|
|
@@ -1034,6 +1055,7 @@ module.exports = {
|
|
|
1034
1055
|
setCreditsReward,
|
|
1035
1056
|
suggestCreditsReward,
|
|
1036
1057
|
taskTouchesProtectedPath,
|
|
1058
|
+
updateCriterionGoal,
|
|
1037
1059
|
updateTaskAutomationTag,
|
|
1038
1060
|
updateTaskDescription,
|
|
1039
1061
|
updateTaskTitle,
|
|
@@ -187,6 +187,39 @@ async function recordProseEdit(kind, id, req, deltas, deps = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// recordCriterionGoalMove — the audit row for moving a criterion between goals
|
|
191
|
+
// (task 1003509). Same reason recordProseEdit exists: the global write-audit
|
|
192
|
+
// middleware records the request body, which carries the DESTINATION and nothing
|
|
193
|
+
// else, so on its own the trail cannot say which goal the criterion left. For a
|
|
194
|
+
// regrouping that is the half that matters — "criterion 12 joined goal 5" is not
|
|
195
|
+
// recoverable without "…and left goal 3".
|
|
196
|
+
//
|
|
197
|
+
// A no-op move (same goal_id in and out) writes nothing, mirroring the empty-delta
|
|
198
|
+
// short-circuit above. And an audit failure is logged, never thrown: the move has
|
|
199
|
+
// already committed, so throwing here would report a failure that did not happen.
|
|
200
|
+
async function recordCriterionGoalMove({ criterionId, fromGoalId, toGoalId }, req, deps = {}) {
|
|
201
|
+
const log = deps.log || ((m) => proseLog().info(m));
|
|
202
|
+
const from = fromGoalId ?? null;
|
|
203
|
+
const to = toGoalId ?? null;
|
|
204
|
+
if (from === to) return;
|
|
205
|
+
const builderId = req.builder && req.builder.id;
|
|
206
|
+
log(`criterion ${criterionId} moved by builder ${builderId}: goal ${from ?? '(none)'} -> ${to ?? '(none)'}`);
|
|
207
|
+
const insert = deps.insertAuditLog || api.insertAuditLog;
|
|
208
|
+
try {
|
|
209
|
+
await insert({
|
|
210
|
+
builderId,
|
|
211
|
+
route: `criterion:${criterionId}:goal-move`,
|
|
212
|
+
method: 'PATCH',
|
|
213
|
+
requestBodyRedacted: JSON.stringify({ from_goal_id: from, to_goal_id: to }),
|
|
214
|
+
responseStatus: 200,
|
|
215
|
+
ip: req.ip || null,
|
|
216
|
+
userAgent: (req.get && req.get('user-agent')) || null,
|
|
217
|
+
});
|
|
218
|
+
} catch (e) {
|
|
219
|
+
log(`[gds] AUDIT GAP: criterion ${criterionId} goal move landed but its audit row failed: ${e && e.message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
// ---------------------------------------------------------------------------
|
|
191
224
|
// WHY PATCH /goals/:id looks the way it does (kept here, not in the route:
|
|
192
225
|
// routes/goals.js is over the fitness oversize threshold, so prose there is
|
|
@@ -332,6 +365,7 @@ module.exports = {
|
|
|
332
365
|
criterionProseRefusal,
|
|
333
366
|
goalAcceptingRequestsHandler,
|
|
334
367
|
logProseEdit,
|
|
368
|
+
recordCriterionGoalMove,
|
|
335
369
|
recordProseEdit,
|
|
336
370
|
goalProseHandler,
|
|
337
371
|
goalVisibilityHandler,
|
|
@@ -224,7 +224,7 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
224
224
|
});
|
|
225
225
|
}, { errorCode: 'satisfy_failed' }));
|
|
226
226
|
|
|
227
|
-
// PATCH /done-when/:criterionId —
|
|
227
|
+
// PATCH /done-when/:criterionId — amend a criterion's TEXT (task 1003102) and/or MOVE it between goals (task 1003509).
|
|
228
228
|
// Was write-once: this file exposed create, satisfy and unsatisfy and nothing
|
|
229
229
|
// else, so wrong text had no API repair path. The incident that forced it: a
|
|
230
230
|
// shell-quoting error stripped backtick-wrapped literals out of three criteria
|
|
@@ -246,31 +246,128 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
246
246
|
// claim "gated like creation" is true. (Caught by the grader on the first cut of
|
|
247
247
|
// task 1003102: the comment said it, the code did half of it.)
|
|
248
248
|
// rank: metic+archon — matches POST /versions/:id/done-when.
|
|
249
|
+
//
|
|
250
|
+
// THE GOAL MOVE (task 1003509 — walk finding W5). `goal_id` used to be writable
|
|
251
|
+
// only by createCriterion's INSERT, so a criterion was born linked and stayed
|
|
252
|
+
// that way for life. The runner seeds a fresh instance's V1 criterion with NO
|
|
253
|
+
// goal — at standup there is no goal yet to name (provision-repo.js
|
|
254
|
+
// seedFirstVersionCmd) — so the owner's first goal could never adopt it:
|
|
255
|
+
// /progress showed the criterion "(ungrouped)" forever beside the goal it
|
|
256
|
+
// obviously belonged to, reporting `criteria: []`. Amending it took SQL, and a
|
|
257
|
+
// hub-provisioned owner has no SQL access at all.
|
|
258
|
+
//
|
|
259
|
+
// BOTH ENDS OF A MOVE ARE GATED, not just the goal it leaves. A move is a
|
|
260
|
+
// criterion write on the DESTINATION every bit as much as on the source, and
|
|
261
|
+
// gating only the source would let a goal's own owner push their criterion into
|
|
262
|
+
// a goal they have no authority over — a criterion being, per the CREATE route
|
|
263
|
+
// above, a thing that permanently blocks version close and goal auto-achieve
|
|
264
|
+
// with no delete route to undo it. So authorizeCriterionCreate runs against both
|
|
265
|
+
// and both must pass. DETACH (`goal_id: null`) has no destination, so it lands
|
|
266
|
+
// on that gate's goal-less branch and is Archon-only — the same wall that makes
|
|
267
|
+
// version-level criteria Archon-only at creation (ADR 0154), reached without a
|
|
268
|
+
// rule of its own.
|
|
269
|
+
//
|
|
270
|
+
// A SATISFIED criterion may still MOVE. criterionProseRefusal freezes the text
|
|
271
|
+
// of a verified claim, and that reasoning is about re-wording: moving one
|
|
272
|
+
// between goals changes how it is grouped, never what was checked. It is
|
|
273
|
+
// refused here only when the prose is what is changing.
|
|
274
|
+
//
|
|
275
|
+
// WHO CAN ADOPT THE SEEDED CRITERION. Because a goal-LESS criterion is
|
|
276
|
+
// version-level, the source gate's own goal-less branch makes ANY write to one
|
|
277
|
+
// Archon-only — which predates this task (a non-Archon could not edit an
|
|
278
|
+
// ungrouped criterion's prose either). That is not a wall in front of W5: the
|
|
279
|
+
// founder of a wizard-born instance IS its Archon, so the one criterion this
|
|
280
|
+
// was filed for is theirs to place. A Metic is unaffected where it matters —
|
|
281
|
+
// regrouping between two goals they already hold needs no escalation.
|
|
249
282
|
router.patch('/done-when/:criterionId', auth.requireBuilder, auth.requirePermission('criterion.create'), asyncHandler('PATCH /done-when/:criterionId', async (req, res) => {
|
|
250
283
|
if (validateOrRespond(req, res, {
|
|
251
|
-
criterion_md: {
|
|
284
|
+
criterion_md: { type: 'string', minLength: 1, maxLength: LIMITS.NOTES },
|
|
285
|
+
goal_id: { type: 'integer', min: 1 },
|
|
252
286
|
})) return;
|
|
287
|
+
// `goal_id: null` is a DETACH and has to be told apart from an absent field, so
|
|
288
|
+
// the presence of the KEY decides, never its value. (The validator treats an
|
|
289
|
+
// explicit null as absent, which is why it validates at all.)
|
|
290
|
+
const movingGoal = Object.prototype.hasOwnProperty.call(req.body || {}, 'goal_id');
|
|
291
|
+
const changingProse = req.body.criterion_md != null;
|
|
292
|
+
if (!movingGoal && !changingProse) {
|
|
293
|
+
return res.fail('nothing_to_update', { status: 400, message: 'Send criterion_md, goal_id, or both.' });
|
|
294
|
+
}
|
|
253
295
|
const id = parseId(req, res, { param: 'criterionId', code: 'bad_criterion_id' });
|
|
254
296
|
if (id === null) return;
|
|
255
297
|
const existing = await doneWhen.getCriterion(id);
|
|
256
298
|
if (!existing) return res.fail('criterion_not_found', 404);
|
|
257
|
-
// The ownership wall, resolved exactly as the CREATE route resolves it: the
|
|
258
|
-
// criterion's OWN goal + version, and membership read only for a non-Archon.
|
|
259
|
-
const goal = existing.goal_id != null ? await db.getGoal(existing.goal_id) : null;
|
|
260
299
|
const version = await db.getVersion(existing.version_id);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
300
|
+
// The ownership wall, resolved exactly as the CREATE route resolves it: the
|
|
301
|
+
// goal + version, and membership read only for a non-Archon. One closure, so
|
|
302
|
+
// both ends of a move are decided by the same code rather than a copy.
|
|
303
|
+
const authorizeFor = async (goal) => {
|
|
304
|
+
const actorMembership = (goal && req.builder.rank !== 'archon')
|
|
305
|
+
? await db.getGoalMember({ goalId: goal.id, builderId: req.builder.id })
|
|
306
|
+
: null;
|
|
307
|
+
return authorizeCriterionCreate({
|
|
308
|
+
goal, version, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
const currentGoal = existing.goal_id != null ? await db.getGoal(existing.goal_id) : null;
|
|
312
|
+
const authority = await authorizeFor(currentGoal);
|
|
267
313
|
if (!authority.ok) return res.status(authority.status).json(authority.body);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
314
|
+
|
|
315
|
+
const targetGoalId = movingGoal ? (req.body.goal_id ?? null) : null;
|
|
316
|
+
if (movingGoal) {
|
|
317
|
+
let targetGoal = null;
|
|
318
|
+
if (targetGoalId !== null) {
|
|
319
|
+
targetGoal = await db.getGoal(targetGoalId);
|
|
320
|
+
// Same refusal the CREATE route gives, for the same reason: a criterion may
|
|
321
|
+
// never cross versions, which would put it in one version's rollup and the
|
|
322
|
+
// other's goal at once.
|
|
323
|
+
if (!targetGoal || String(targetGoal.version_id) !== String(existing.version_id)) {
|
|
324
|
+
return res.fail('bad_goal_id', { status: 400, message: `goal_id ${targetGoalId} does not exist on version '${existing.version_id}'.` });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const destination = await authorizeFor(targetGoal);
|
|
328
|
+
if (!destination.ok) return res.status(destination.status).json(destination.body);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let criterion = existing;
|
|
332
|
+
let deltas = [];
|
|
333
|
+
let unchanged = false;
|
|
334
|
+
if (changingProse) {
|
|
335
|
+
const refusal = goalEdits.criterionProseRefusal(existing);
|
|
336
|
+
if (refusal) return res.fail(refusal.code, refusal.status, refusal.message ? { message: refusal.message } : undefined);
|
|
337
|
+
const result = await goalEdits.updateCriterionProse({ criterionId: id, criterionMd: req.body.criterion_md });
|
|
338
|
+
if (!result) return res.fail('criterion_not_found', 404);
|
|
339
|
+
await goalEdits.recordProseEdit('criterion', id, req, result.deltas);
|
|
340
|
+
criterion = result.criterion;
|
|
341
|
+
deltas = result.deltas;
|
|
342
|
+
unchanged = !!result.unchanged;
|
|
343
|
+
}
|
|
344
|
+
const achievedGoals = [];
|
|
345
|
+
const fromGoalId = existing.goal_id ?? null;
|
|
346
|
+
// A move to where it already is writes nothing at all — not the row, not the
|
|
347
|
+
// audit, not a re-derive. Nothing about the world changed.
|
|
348
|
+
if (movingGoal && fromGoalId !== targetGoalId) {
|
|
349
|
+
const moved = await db.updateCriterionGoal(id, targetGoalId);
|
|
350
|
+
if (!moved) return res.fail('criterion_not_found', 404);
|
|
351
|
+
criterion = moved;
|
|
352
|
+
await goalEdits.recordCriterionGoalMove(
|
|
353
|
+
{ criterionId: id, fromGoalId, toGoalId: targetGoalId }, req
|
|
354
|
+
);
|
|
355
|
+
// BOTH ENDS are re-derived, because a move changes what each of them still
|
|
356
|
+
// needs. The destination is the obvious half — adopting an already-satisfied
|
|
357
|
+
// criterion can be the thing that completes a goal. The SOURCE is the half
|
|
358
|
+
// that is easy to miss and just as real: taking an UNSATISFIED criterion out
|
|
359
|
+
// of a goal can leave every criterion that remains satisfied, which is that
|
|
360
|
+
// goal completing. Skipping it would leave a goal silently open that nothing
|
|
361
|
+
// will ever close again — no later ship re-checks it, because no task under
|
|
362
|
+
// it changed. achieveGoalIfComplete is conservative and idempotent (open
|
|
363
|
+
// goals only, and it never un-achieves), so asking twice is two reads.
|
|
364
|
+
for (const goalId of [fromGoalId, targetGoalId]) {
|
|
365
|
+
if (goalId === null) continue;
|
|
366
|
+
const outcome = await db.achieveGoalIfComplete({ goalId });
|
|
367
|
+
if (outcome && outcome.achieved) achievedGoals.push(goalId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return res.json({ ok: true, criterion, deltas, unchanged, achieved_goals: achievedGoals });
|
|
274
371
|
}, { errorCode: 'criterion_update_failed' }));
|
|
275
372
|
|
|
276
373
|
// Auth: flip a criterion back to unsatisfied (rare; for corrections).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.675",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.675",
|
|
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.
|
|
3
|
+
"version": "1.19.675",
|
|
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",
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// directly — it asks the deployed bot (via the API) to do the work.
|
|
11
11
|
//
|
|
12
12
|
// Usage:
|
|
13
|
-
// node scripts/gds/discord-channels.js plan
|
|
13
|
+
// node scripts/gds/discord-channels.js plan # dry run: what WOULD change
|
|
14
14
|
// node scripts/gds/discord-channels.js apply # apply docs/discord/channels.json to Discord
|
|
15
|
+
// node scripts/gds/discord-channels.js snapshot # print the guild AS channels.json (seed the file)
|
|
15
16
|
//
|
|
16
17
|
// The channel layout itself is edited in docs/discord/channels.json and merged
|
|
17
18
|
// (rank-gated) — this command just triggers the reconcile.
|
|
@@ -20,8 +21,8 @@ const { cliClient } = require('./cli-lib');
|
|
|
20
21
|
|
|
21
22
|
async function main() {
|
|
22
23
|
const cmd = (process.argv[2] || '').toLowerCase();
|
|
23
|
-
if (cmd !== 'plan' && cmd !== 'apply') {
|
|
24
|
-
console.error('usage: node scripts/gds/discord-channels.js <plan|apply>');
|
|
24
|
+
if (cmd !== 'plan' && cmd !== 'apply' && cmd !== 'snapshot') {
|
|
25
|
+
console.error('usage: node scripts/gds/discord-channels.js <plan|apply|snapshot>');
|
|
25
26
|
process.exit(2);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -29,6 +30,8 @@ async function main() {
|
|
|
29
30
|
let res;
|
|
30
31
|
if (cmd === 'plan') {
|
|
31
32
|
res = await api.discord.getDiscordChannelsPlan();
|
|
33
|
+
} else if (cmd === 'snapshot') {
|
|
34
|
+
res = await api.discord.getDiscordChannelsSnapshot();
|
|
32
35
|
} else {
|
|
33
36
|
res = await api.discord.postDiscordChannelsReconcile({ body: {} });
|
|
34
37
|
}
|
|
@@ -47,7 +50,15 @@ async function main() {
|
|
|
47
50
|
process.exit(1);
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
// Warnings go to STDERR so `snapshot > channels.json` writes clean JSON.
|
|
50
54
|
for (const w of body.warnings || []) console.warn(`warning: ${w}`);
|
|
55
|
+
if (cmd === 'snapshot') {
|
|
56
|
+
// STDOUT is the file body and nothing else — this command exists to be
|
|
57
|
+
// redirected. Review the diff before committing: it is the guild's real
|
|
58
|
+
// state, which is not necessarily the state anyone intended.
|
|
59
|
+
console.log(JSON.stringify(body.config, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
if (cmd === 'plan') {
|
|
52
63
|
const ops = body.ops || [];
|
|
53
64
|
if (!ops.length) { console.log('In sync — no channel changes needed.'); return; }
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.675'; // 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');
|