@bongos/core 1.21.106 → 1.21.108
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 +72 -52
- package/.claude/skills/goal-create/SKILL.md +8 -3
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +6 -4
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +6 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +25 -19
- package/docs/copy-registry.json +92 -37
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +189 -188
- package/migrations/core_275_claims_auto_pickup.sql +24 -0
- package/modules/copy-desk/docx.js +27 -7
- package/modules/copy-desk/page-status.js +3 -1
- package/modules/copy-desk/pages.js +44 -5
- package/modules/copy-desk/routes/copy-desk.js +4 -3
- package/modules/copy-desk/tests/copy_docx.mjs +38 -0
- package/modules/copy-desk/tests/copy_page_draft.mjs +79 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/hall-ui/public/paintbrush.js +152 -12
- package/modules/hall-ui/public/studio.js +3 -1
- package/modules/hall-ui/public/style.css +16 -0
- package/modules/hall-ui/public/tweak-editor-lib.js +24 -12
- package/modules/hall-ui/records/craft-mode.md +5 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/lifecycle/autopickup.js +94 -0
- package/modules/lifecycle/db-claims.js +18 -4
- package/modules/lifecycle/routes/claims.js +12 -1
- package/modules/lifecycle/routes/project-settings.js +59 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/government-walk.js +1 -0
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +12 -21
- package/scripts/gds/seed-verify-criteria.js +73 -18
- package/src/bongos/project-settings.js +79 -0
- package/src/module-api.js +8 -1
- package/tests/autopickup_cap.mjs +236 -0
- package/tests/autopickup_cap_db.mjs +115 -0
- package/tests/copy_desk_page_submit.mjs +36 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/hall_tweak_editor.mjs +19 -3
- package/tests/paintbrush.mjs +151 -2
- package/tests/planning_session_skill.mjs +40 -0
- package/tests/seed_verify_criteria.mjs +130 -0
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
const express = require('express');
|
|
32
32
|
const api = require('../../../src/module-api');
|
|
33
33
|
const goalCategory = require('../goal-category');
|
|
34
|
+
const autopickup = require('../autopickup');
|
|
34
35
|
|
|
35
36
|
const log = api.logger('lifecycle');
|
|
36
37
|
|
|
@@ -380,6 +381,64 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
380
381
|
}
|
|
381
382
|
});
|
|
382
383
|
|
|
384
|
+
// --- tweak auto-pickup (task 1004677, ADR 0371 D3/D5) ----------------------
|
|
385
|
+
//
|
|
386
|
+
// GET /project-settings/tweak-autopickup — the switch, the monthly cap, and how
|
|
387
|
+
// many rounds the lane has picked up this calendar month (counted from marked
|
|
388
|
+
// claims, never a stored counter).
|
|
389
|
+
//
|
|
390
|
+
// rank: any signed-in builder. The runner reads it before it tries a round, and
|
|
391
|
+
// the studio shows the count; it carries no reward internals or file paths.
|
|
392
|
+
async function autopickupBody() {
|
|
393
|
+
const resolved = await api.projectSettings.resolveTweakAutopickup();
|
|
394
|
+
const used = await autopickup.countThisMonth();
|
|
395
|
+
return {
|
|
396
|
+
mode: resolved.mode,
|
|
397
|
+
cap: resolved.cap,
|
|
398
|
+
used_this_month: used,
|
|
399
|
+
remaining_this_month: Math.max(0, resolved.cap - used),
|
|
400
|
+
per_round_limit: autopickup.MAX_AUTO_APPLIES_PER_ROUND,
|
|
401
|
+
source: resolved.source,
|
|
402
|
+
invalid: resolved.invalid,
|
|
403
|
+
sentence: api.projectSettings.describeTweakAutopickup(resolved),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
router.get('/project-settings/tweak-autopickup', api.requireBuilder, async (req, res) => {
|
|
408
|
+
try {
|
|
409
|
+
res.set('Cache-Control', 'no-store');
|
|
410
|
+
res.json(await autopickupBody());
|
|
411
|
+
} catch (err) {
|
|
412
|
+
log.error(`GET /project-settings/tweak-autopickup: ${err && err.message}`);
|
|
413
|
+
res.fail('project_settings_read_failed', { status: 500, message: 'internal error' });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// PATCH /project-settings/tweak-autopickup — body { mode?: 'off' | 'on', cap?: n }.
|
|
418
|
+
//
|
|
419
|
+
// rank: project.settings.manage (metic+, government_010), the same atom as the
|
|
420
|
+
// other writes on this page. Each field is validated by the resolver's own parser,
|
|
421
|
+
// and both are checked before either is written, so a half-valid body writes nothing.
|
|
422
|
+
router.patch('/project-settings/tweak-autopickup', api.requireBuilder, api.requirePermission('project.settings.manage'), api.requireActAlone('rules'), async (req, res) => {
|
|
423
|
+
if (api.validateOrRespond(req, res, { mode: { type: 'string' }, cap: { type: 'number' } })) return;
|
|
424
|
+
const body = req.body || {};
|
|
425
|
+
if (body.mode == null && body.cap == null) {
|
|
426
|
+
return res.fail('bad_tweak_autopickup', { status: 400, message: 'send mode, cap, or both' });
|
|
427
|
+
}
|
|
428
|
+
const mode = body.mode == null ? null : api.projectSettings.parseAutopickup(body.mode);
|
|
429
|
+
if (mode && !mode.mode) return res.fail('bad_tweak_autopickup', { status: 400, message: `mode: ${mode.reason}` });
|
|
430
|
+
const cap = body.cap == null ? null : api.projectSettings.parseAutopickupCap(body.cap);
|
|
431
|
+
if (cap && cap.cap == null) return res.fail('bad_tweak_autopickup', { status: 400, message: `cap: ${cap.reason}` });
|
|
432
|
+
try {
|
|
433
|
+
if (mode) await api.projectSettings.set(api.projectSettings.KEYS.TWEAK_AUTOPICKUP, mode.mode, req.builder.id);
|
|
434
|
+
if (cap) await api.projectSettings.set(api.projectSettings.KEYS.TWEAK_AUTOPICKUP_CAP, cap.cap, req.builder.id);
|
|
435
|
+
res.json({ ok: true, ...(await autopickupBody()) });
|
|
436
|
+
} catch (err) {
|
|
437
|
+
log.error(`PATCH /project-settings/tweak-autopickup: ${err && err.message}`);
|
|
438
|
+
res.fail('project_settings_write_failed', { status: 500, message: 'internal error' });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
383
442
|
return router;
|
|
384
443
|
};
|
|
385
444
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.108",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.108",
|
|
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.21.
|
|
3
|
+
"version": "1.21.108",
|
|
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/release-notes.json
CHANGED
|
@@ -9482,5 +9482,21 @@
|
|
|
9482
9482
|
"id": "1004683",
|
|
9483
9483
|
"text": "After pressing Create, a founder now sees \"Getting <name> ready\" with a plain count (\"4 of 6 done\", or \"ready\" with a tick) and one sentence saying what is happening and whether it is their turn. Below it are two short lists,"
|
|
9484
9484
|
}
|
|
9485
|
+
],
|
|
9486
|
+
"1.21.107": [
|
|
9487
|
+
{
|
|
9488
|
+
"id": "1004677",
|
|
9489
|
+
"text": "A project now has a switch for letting a runner pick up submitted page tweaks on its own, off by default, with a monthly limit of 20 rounds. The server enforces the limit when the work is claimed, so two runners can never both"
|
|
9490
|
+
}
|
|
9491
|
+
],
|
|
9492
|
+
"1.21.108": [
|
|
9493
|
+
{
|
|
9494
|
+
"id": "1004668",
|
|
9495
|
+
"text": "Adding new tasks to a goal that already has work in it no longer sets off a false alarm saying the tasks were filed in the wrong place. The planning guides also stop telling people that new tasks wait for an owner's go-ahead:"
|
|
9496
|
+
},
|
|
9497
|
+
{
|
|
9498
|
+
"id": "1004696",
|
|
9499
|
+
"text": "Artists can now recommend removing a line while editing a page's words: mark it with \"Remove this line\" or just delete its words, see it crossed out, keep it after all, and send it with their rewrites. Each removal goes to an"
|
|
9500
|
+
}
|
|
9485
9501
|
]
|
|
9486
9502
|
}
|
|
@@ -70,6 +70,7 @@ const BAD = 'x';
|
|
|
70
70
|
const REQUESTS = Object.freeze({
|
|
71
71
|
'PATCH /project-settings/rot-days': ['PATCH', '/project-settings/rot-days', { days: 'soon' }],
|
|
72
72
|
'PATCH /project-settings/ship-mode': ['PATCH', '/project-settings/ship-mode', { mode: 'sometimes' }],
|
|
73
|
+
'PATCH /project-settings/tweak-autopickup': ['PATCH', '/project-settings/tweak-autopickup', { mode: 'sometimes' }],
|
|
73
74
|
'PATCH /work-categories/:id': ['PATCH', `/work-categories/${BAD}`, {}],
|
|
74
75
|
'POST /security/reports/:id/confirm': ['POST', `/security/reports/${BAD}/confirm`, {}],
|
|
75
76
|
'POST /security/reports/:id/dispute': ['POST', `/security/reports/${BAD}/dispute`, {}],
|
|
@@ -121,6 +121,9 @@ const INTEGRATION = new Set([
|
|
|
121
121
|
// index actually refuses the commingling INSERT and still allows the parallel
|
|
122
122
|
// -worktree case. It self-skips without a DB, and refuses any database that
|
|
123
123
|
// has a `builders` table, since it TRUNCATEs claims.
|
|
124
|
+
// task 1004677: two marked auto-pickup claims racing for the last slot of the
|
|
125
|
+
// monthly cap; only real connections prove the advisory lock. Scratch DB only.
|
|
126
|
+
'autopickup_cap_db',
|
|
124
127
|
'claims_requested_worktree_db',
|
|
125
128
|
'clawback_confirmed_backlog',
|
|
126
129
|
// task 1003113: the copy_desk_flags schema half. The module's own tests are
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Run: node scripts/gds/seed-bv2-ideator-surface-tasks.js
|
|
15
15
|
const os = require('os');
|
|
16
|
-
const { expectedCriterionCounts, verifySeedCriteriaLinks } = require('./seed-verify-criteria');
|
|
16
|
+
const { baselineSeedCriteria, expectedCriterionCounts, verifySeedCriteriaLinks } = require('./seed-verify-criteria');
|
|
17
17
|
const sess = require(os.homedir() + '/.config/cloudbongos/gds-session.json');
|
|
18
18
|
const BASE = sess.api_base.replace(/\/$/, '');
|
|
19
19
|
const TOKEN = sess.token;
|
|
@@ -328,24 +328,11 @@ async function main() {
|
|
|
328
328
|
const ids = {};
|
|
329
329
|
const criterionIdListsPerTask = [];
|
|
330
330
|
|
|
331
|
-
// BASELINE
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
// before anything is written, and fold them into the plan — the check then still
|
|
337
|
-
// catches any drift on the four this seed owns, which is what it is for.
|
|
338
|
-
const OURS = new Set(Object.values(CRIT));
|
|
339
|
-
const baseline = {};
|
|
340
|
-
const pre = await api('GET', `/goals/${GOAL}`);
|
|
341
|
-
if (!pre.ok || !pre.json || !Array.isArray(pre.json.criteria)) {
|
|
342
|
-
console.error(`Could not read goal ${GOAL} to baseline its existing criteria (status ${pre.status}). Refusing to seed blind.`);
|
|
343
|
-
process.exit(1);
|
|
344
|
-
}
|
|
345
|
-
for (const row of pre.json.criteria) {
|
|
346
|
-
if (!OURS.has(row.criterion_id)) baseline[row.criterion_id] = Number(row.linked_tasks) || 0;
|
|
347
|
-
}
|
|
348
|
-
console.log(`Baselined ${Object.keys(baseline).length} pre-existing criteria on goal ${GOAL}.`);
|
|
331
|
+
// BASELINE, before anything is written. Goal 1000095 already held six criteria
|
|
332
|
+
// with live task links before this seed existed; verifying against the pre-seed
|
|
333
|
+
// snapshot plus the plan keeps those from reading as false mismatches while still
|
|
334
|
+
// catching any drift (task 1004668 moved this into seed-verify-criteria.js).
|
|
335
|
+
const baseline = await baselineSeedCriteria({ goalId: GOAL, fetchGoal: (gid) => api('GET', `/goals/${gid}`) });
|
|
349
336
|
|
|
350
337
|
console.log(`Seeding ${TASKS.length} tasks into goal ${GOAL} (${VER})…\n`);
|
|
351
338
|
|
|
@@ -370,7 +357,10 @@ async function main() {
|
|
|
370
357
|
continue;
|
|
371
358
|
}
|
|
372
359
|
ids[t.ref] = r.json.task.id;
|
|
373
|
-
|
|
360
|
+
// Count only links THIS run created. A re-run replays an existing task
|
|
361
|
+
// (`idempotent: true`) whose links the baseline already holds; counting it
|
|
362
|
+
// again would expect them twice (task 1004668).
|
|
363
|
+
if (!r.json.idempotent) criterionIdListsPerTask.push(t.crit);
|
|
374
364
|
console.log(` ${t.ref} → ${r.json.task.id} ${title.slice(0, 74)}`);
|
|
375
365
|
}
|
|
376
366
|
|
|
@@ -391,7 +381,8 @@ async function main() {
|
|
|
391
381
|
console.log('');
|
|
392
382
|
await verifySeedCriteriaLinks({
|
|
393
383
|
goalId: GOAL,
|
|
394
|
-
expectedCounts:
|
|
384
|
+
expectedCounts: expectedCriterionCounts(criterionIdListsPerTask),
|
|
385
|
+
baseline,
|
|
395
386
|
fetchGoal: (gid) => api('GET', `/goals/${gid}`),
|
|
396
387
|
});
|
|
397
388
|
}
|
|
@@ -12,21 +12,48 @@
|
|
|
12
12
|
// (some use cli-lib's apiCall, others a local throttled fetch — see the
|
|
13
13
|
// scripts/gds/CLAUDE.md roster), so this takes a `fetchGoal(goalId)` callback
|
|
14
14
|
// rather than assuming a client shape.
|
|
15
|
+
//
|
|
16
|
+
// WHY A BASELINE (task 1004668). A seed into a goal that already carries linked
|
|
17
|
+
// work used to cry wolf: every criterion the seed did not touch was compared
|
|
18
|
+
// against an expected count of zero, so goal 1000095 (six criteria already linked)
|
|
19
|
+
// would have reported six false mismatches and exit(1) on a correct run — and a
|
|
20
|
+
// check that fails correct runs trains the next builder to ignore it. A seed now
|
|
21
|
+
// snapshots the goal's counts BEFORE its first write (baselineSeedCriteria) and
|
|
22
|
+
// verifies `baseline + plan`, which still catches a stray link on a criterion the
|
|
23
|
+
// seed was never meant to touch. Without a baseline only the plan's own criteria
|
|
24
|
+
// are checked; nothing else is assumed to be zero.
|
|
25
|
+
//
|
|
26
|
+
// With a baseline, the plan is the links THIS RUN CREATED, not every task the seed
|
|
27
|
+
// names. A seed is resumable (POST /tasks is idempotent on source_ref and replays
|
|
28
|
+
// an existing task with `idempotent: true`), and a replayed task's links are
|
|
29
|
+
// already inside the baseline — tally it again and a correct re-run expects them
|
|
30
|
+
// twice. So a baselined seed skips replays when it tallies (see
|
|
31
|
+
// seed-bv2-ideator-surface-tasks.js).
|
|
32
|
+
|
|
33
|
+
// countsByCriterion — { criterion_id: linked_tasks } from GET /goals/:id rows.
|
|
34
|
+
function countsByCriterion(criteriaRows) {
|
|
35
|
+
const counts = {};
|
|
36
|
+
for (const row of criteriaRows || []) counts[row.criterion_id] = Number(row.linked_tasks) || 0;
|
|
37
|
+
return counts;
|
|
38
|
+
}
|
|
15
39
|
|
|
16
40
|
// diffCriterionLinks — pure comparison: expected (slug -> count, tallied from
|
|
17
41
|
// the criterion_ids actually POSTed) vs actual (the `criteria` rows
|
|
18
42
|
// GET /goals/:id returns, each carrying criterion_id + linked_tasks). Returns
|
|
19
|
-
// every mismatching slug
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
43
|
+
// every mismatching slug.
|
|
44
|
+
//
|
|
45
|
+
// With `baseline` (the pre-seed snapshot) every criterion on the goal is checked
|
|
46
|
+
// against baseline + plan — a criterion created after the snapshot starts at 0.
|
|
47
|
+
// Without it, only the plan's own criteria are checked: a criterion the seed
|
|
48
|
+
// does not cite has an unknown prior count, and guessing zero is the false alarm.
|
|
49
|
+
function diffCriterionLinks(expectedCounts, criteriaRows, { baseline = null } = {}) {
|
|
50
|
+
const actual = countsByCriterion(criteriaRows);
|
|
51
|
+
const plan = expectedCounts || {};
|
|
52
|
+
const slugs = new Set(Object.keys(plan));
|
|
53
|
+
if (baseline) for (const slug of [...Object.keys(baseline), ...Object.keys(actual)]) slugs.add(slug);
|
|
27
54
|
const mismatches = [];
|
|
28
55
|
for (const slug of slugs) {
|
|
29
|
-
const expected = (
|
|
56
|
+
const expected = ((baseline && baseline[slug]) || 0) + (plan[slug] || 0);
|
|
30
57
|
const got = actual[slug] || 0;
|
|
31
58
|
if (expected !== got) mismatches.push({ criterion_id: slug, expected, actual: got });
|
|
32
59
|
}
|
|
@@ -45,25 +72,53 @@ function expectedCriterionCounts(criterionIdListsPerTask) {
|
|
|
45
72
|
return counts;
|
|
46
73
|
}
|
|
47
74
|
|
|
75
|
+
function readCriteria(r) {
|
|
76
|
+
const body = (r && (r.json || r.data)) || null;
|
|
77
|
+
return r && r.ok && body && Array.isArray(body.criteria) ? body.criteria : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// baselineSeedCriteria — call BEFORE the seed's first write. Returns the goal's
|
|
81
|
+
// current { criterion_id: linked_tasks } snapshot to hand to
|
|
82
|
+
// verifySeedCriteriaLinks, or exit(1)s: a seed that cannot read the goal it is
|
|
83
|
+
// about to write into must not seed blind.
|
|
84
|
+
async function baselineSeedCriteria({ goalId, fetchGoal, log = console.log, exit = process.exit }) {
|
|
85
|
+
const r = await fetchGoal(goalId);
|
|
86
|
+
const criteria = readCriteria(r);
|
|
87
|
+
if (!criteria) {
|
|
88
|
+
log(`BASELINE FAILED: GET goal ${goalId} did not return a criteria list (status ${r && r.status}). Refusing to seed blind.`);
|
|
89
|
+
return exit(1);
|
|
90
|
+
}
|
|
91
|
+
const baseline = countsByCriterion(criteria);
|
|
92
|
+
const linked = Object.values(baseline).filter((n) => n > 0).length;
|
|
93
|
+
log(`Baselined goal ${goalId}: ${criteria.length} criteria, ${linked} already carrying linked tasks.`);
|
|
94
|
+
return baseline;
|
|
95
|
+
}
|
|
96
|
+
|
|
48
97
|
// verifySeedCriteriaLinks — Phase-5 self-check: fetch the goal, diff its
|
|
49
|
-
// linked_tasks against the plan
|
|
50
|
-
// where tasks were expected is the shape
|
|
51
|
-
// RETRO2 incident, but any drift is worth stopping
|
|
52
|
-
|
|
98
|
+
// linked_tasks against the plan (plus the pre-seed `baseline` when given), and
|
|
99
|
+
// exit(1) loudly on any mismatch (a zero where tasks were expected is the shape
|
|
100
|
+
// that shipped silently in the RETRO2 incident, but any drift is worth stopping
|
|
101
|
+
// the seed for).
|
|
102
|
+
async function verifySeedCriteriaLinks({ goalId, expectedCounts, baseline = null, fetchGoal, log = console.log, exit = process.exit }) {
|
|
53
103
|
const r = await fetchGoal(goalId);
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
104
|
+
const criteria = readCriteria(r);
|
|
105
|
+
if (!criteria) {
|
|
56
106
|
log(`VERIFY FAILED: GET goal ${goalId} did not return a criteria list (status ${r && r.status}). Cannot confirm criterion links landed.`);
|
|
57
107
|
return exit(1);
|
|
58
108
|
}
|
|
59
|
-
const mismatches = diffCriterionLinks(expectedCounts,
|
|
109
|
+
const mismatches = diffCriterionLinks(expectedCounts, criteria, { baseline });
|
|
60
110
|
if (mismatches.length) {
|
|
61
111
|
log(`VERIFY FAILED: criterion links on goal ${goalId} do not match the plan (${mismatches.length} mismatch${mismatches.length === 1 ? '' : 'es'}):`);
|
|
62
112
|
for (const m of mismatches) log(` ${m.criterion_id}: expected ${m.expected} linked task(s), got ${m.actual}`);
|
|
63
113
|
log('Likely cause: a criterion_ids mapping bug (e.g. a positional "Cn" token resolved against the wrong scope — learning 1000188). Fix and re-run before trusting this seed.');
|
|
64
114
|
return exit(1);
|
|
65
115
|
}
|
|
66
|
-
|
|
116
|
+
if (baseline) {
|
|
117
|
+
log(`VERIFY OK: all ${criteria.length} criteria on goal ${goalId} carry their pre-seed count plus the planned links.`);
|
|
118
|
+
} else {
|
|
119
|
+
log(`VERIFY OK: all ${Object.keys(expectedCounts || {}).length} planned criteria on goal ${goalId} carry their planned task count.`);
|
|
120
|
+
log(' (No baseline was taken, so criteria this seed does not cite were not checked — pass baselineSeedCriteria() output as `baseline` to cover them.)');
|
|
121
|
+
}
|
|
67
122
|
}
|
|
68
123
|
|
|
69
|
-
module.exports = { diffCriterionLinks, expectedCriterionCounts, verifySeedCriteriaLinks };
|
|
124
|
+
module.exports = { diffCriterionLinks, expectedCriterionCounts, baselineSeedCriteria, verifySeedCriteriaLinks };
|
|
@@ -50,6 +50,8 @@ const SETTING_KEYS = {
|
|
|
50
50
|
ROT_DAYS: 'attention.rot_days',
|
|
51
51
|
FOCUS_VERSION: 'roadmap.focus_version',
|
|
52
52
|
SHIP_MODE: 'ship.mode',
|
|
53
|
+
TWEAK_AUTOPICKUP: 'tweak.autopickup',
|
|
54
|
+
TWEAK_AUTOPICKUP_CAP: 'tweak.autopickup_monthly_cap',
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
// SHIP MODE (task 1004628, ADR 0369) — WHO starts a ship, never HOW it lands.
|
|
@@ -278,6 +280,75 @@ function describeShipMode(resolved) {
|
|
|
278
280
|
return `${what}${how}`;
|
|
279
281
|
}
|
|
280
282
|
|
|
283
|
+
|
|
284
|
+
// TWEAK AUTO-PICKUP (task 1004677, ADR 0371 D3/D5). Whether the Autobongos runner's
|
|
285
|
+
// tweak lane may pick up a submitted page round on its own, and how many rounds it
|
|
286
|
+
// may pick up per calendar month. Off by default: a project that never turns it on
|
|
287
|
+
// keeps today's queue exactly. The cap is ENFORCED by the server at claim time
|
|
288
|
+
// (modules/lifecycle/autopickup.js); this file only resolves the numbers.
|
|
289
|
+
const AUTOPICKUP_MODES = Object.freeze(['off', 'on']);
|
|
290
|
+
const DEFAULT_AUTOPICKUP = 'off';
|
|
291
|
+
// 20 rounds: a grade costs the project a few dollars, so 20 is a bounded, legible
|
|
292
|
+
// monthly spend at about the level of one active artist (ADR 0371 D5).
|
|
293
|
+
const DEFAULT_AUTOPICKUP_CAP = 20;
|
|
294
|
+
// Past 1000 the cap is an off switch typed as a big number; that should be asked
|
|
295
|
+
// for out loud, the same reasoning as MAX_ROT_DAYS.
|
|
296
|
+
const MAX_AUTOPICKUP_CAP = 1000;
|
|
297
|
+
|
|
298
|
+
function parseAutopickup(raw) {
|
|
299
|
+
if (raw == null || String(raw).trim() === '') return { reason: 'empty' };
|
|
300
|
+
const v = String(raw).trim().toLowerCase();
|
|
301
|
+
if (!AUTOPICKUP_MODES.includes(v)) return { reason: `must be one of ${AUTOPICKUP_MODES.join(' | ')}` };
|
|
302
|
+
return { mode: v };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseAutopickupCap(raw) {
|
|
306
|
+
if (raw == null || raw === '') return { reason: 'empty' };
|
|
307
|
+
const n = Number(raw);
|
|
308
|
+
if (!Number.isFinite(n)) return { reason: 'not a number' };
|
|
309
|
+
if (!Number.isInteger(n)) return { reason: 'must be a whole number of rounds' };
|
|
310
|
+
if (n <= 0) return { reason: 'must be positive' };
|
|
311
|
+
if (n > MAX_AUTOPICKUP_CAP) return { reason: `out of range (1..${MAX_AUTOPICKUP_CAP})` };
|
|
312
|
+
return { cap: n };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// One knob through its parser: { value, source, invalid }. Never throws; an
|
|
316
|
+
// unreadable database answers the default, the resolveShipMode posture.
|
|
317
|
+
async function resolveOne(key, parse, pick, fallback) {
|
|
318
|
+
let row = null;
|
|
319
|
+
try {
|
|
320
|
+
row = await getSetting(key);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger.error(`project_settings read failed; falling back to the default ${key}: ${err && err.message}`);
|
|
323
|
+
return { value: fallback, source: 'default', invalid: null };
|
|
324
|
+
}
|
|
325
|
+
if (row && row.value != null && row.value !== '') {
|
|
326
|
+
const got = parse(row.value);
|
|
327
|
+
if (pick(got) != null) return { value: pick(got), source: 'project_settings', invalid: null };
|
|
328
|
+
return { value: fallback, source: 'default', invalid: { where: `project_settings['${key}']`, raw: String(row.value), reason: got.reason } };
|
|
329
|
+
}
|
|
330
|
+
return { value: fallback, source: 'default', invalid: null };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// resolveTweakAutopickup() — { mode, cap, source: { mode, cap }, invalid: [...] }.
|
|
334
|
+
// mode is always 'off' or 'on', cap always a usable positive integer.
|
|
335
|
+
async function resolveTweakAutopickup() {
|
|
336
|
+
const m = await resolveOne(SETTING_KEYS.TWEAK_AUTOPICKUP, parseAutopickup, (g) => g.mode, DEFAULT_AUTOPICKUP);
|
|
337
|
+
const c = await resolveOne(SETTING_KEYS.TWEAK_AUTOPICKUP_CAP, parseAutopickupCap, (g) => g.cap, DEFAULT_AUTOPICKUP_CAP);
|
|
338
|
+
return {
|
|
339
|
+
mode: m.value,
|
|
340
|
+
cap: c.value,
|
|
341
|
+
source: { mode: m.source, cap: c.source },
|
|
342
|
+
invalid: [m.invalid, c.invalid].filter(Boolean),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function describeTweakAutopickup(resolved) {
|
|
347
|
+
const r = resolved || {};
|
|
348
|
+
if (r.mode !== 'on') return 'Off: a submitted page waits until a person runs /tweak for it.';
|
|
349
|
+
return `On: a running Autobongos runner applies submitted pages on its own, up to ${r.cap} a month. The artist still approves every one.`;
|
|
350
|
+
}
|
|
351
|
+
|
|
281
352
|
// parseDays is exported for the tests only — the range check and the default are
|
|
282
353
|
// asserted against the SAME constants the resolver reads, because a test that
|
|
283
354
|
// re-typed 30 or 365 would be the second copy of the number this file exists to
|
|
@@ -299,4 +370,12 @@ module.exports = {
|
|
|
299
370
|
parseShipMode,
|
|
300
371
|
resolveShipMode,
|
|
301
372
|
describeShipMode,
|
|
373
|
+
AUTOPICKUP_MODES,
|
|
374
|
+
DEFAULT_AUTOPICKUP,
|
|
375
|
+
DEFAULT_AUTOPICKUP_CAP,
|
|
376
|
+
MAX_AUTOPICKUP_CAP,
|
|
377
|
+
parseAutopickup,
|
|
378
|
+
parseAutopickupCap,
|
|
379
|
+
resolveTweakAutopickup,
|
|
380
|
+
describeTweakAutopickup,
|
|
302
381
|
};
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.108'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -474,6 +474,13 @@ module.exports = {
|
|
|
474
474
|
get parseShipMode() { return require('./bongos/project-settings').parseShipMode; },
|
|
475
475
|
get resolveShipMode() { return require('./bongos/project-settings').resolveShipMode; },
|
|
476
476
|
get describeShipMode() { return require('./bongos/project-settings').describeShipMode; },
|
|
477
|
+
// Tweak auto-pickup (task 1004677, ADR 0371 D3/D5): the switch and the monthly cap.
|
|
478
|
+
get AUTOPICKUP_MODES() { return require('./bongos/project-settings').AUTOPICKUP_MODES; },
|
|
479
|
+
get MAX_AUTOPICKUP_CAP() { return require('./bongos/project-settings').MAX_AUTOPICKUP_CAP; },
|
|
480
|
+
get parseAutopickup() { return require('./bongos/project-settings').parseAutopickup; },
|
|
481
|
+
get parseAutopickupCap() { return require('./bongos/project-settings').parseAutopickupCap; },
|
|
482
|
+
get resolveTweakAutopickup() { return require('./bongos/project-settings').resolveTweakAutopickup; },
|
|
483
|
+
get describeTweakAutopickup() { return require('./bongos/project-settings').describeTweakAutopickup; },
|
|
477
484
|
},
|
|
478
485
|
readConfigFileSync: instanceConfig.readConfigFileSync,
|
|
479
486
|
configHome: instanceConfig.configHome,
|