@bongos/core 1.19.57 → 1.19.59
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 +16 -16
- package/modules/dev-box/boxes.js +14 -3
- package/modules/hall-ui/public/box-panel.js +20 -7
- package/modules/hall-ui/public/goals-page.js +16 -8
- package/modules/lifecycle/db.js +8 -4
- package/modules/lifecycle/routes/goals.js +16 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/box_code_staleness.mjs +15 -2
- package/tests/goal_membership_requests.mjs +25 -0
- package/tests/goal_tier.mjs +6 -5
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.59",
|
|
6
|
+
"core_contract": "1.19.59",
|
|
7
|
+
"source_commit": "e93244fafbe4236b1b90218f03c70c92bfd0becb",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-11T23:37:01.674Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 291,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1529,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "0f793cd7617fce56439932f68de11239e7639d28e50f8d9ee485ba8a602a5f30",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2577,7 +2577,7 @@
|
|
|
2577
2577
|
{
|
|
2578
2578
|
"path": "modules/dev-box/boxes.js",
|
|
2579
2579
|
"mode": "0000644",
|
|
2580
|
-
"sha256": "
|
|
2580
|
+
"sha256": "fbf9bb7bd4c70ac81627d7d473a94e98e6976581bc29f60c5342d428a39238cf"
|
|
2581
2581
|
},
|
|
2582
2582
|
{
|
|
2583
2583
|
"path": "modules/dev-box/db.js",
|
|
@@ -2962,7 +2962,7 @@
|
|
|
2962
2962
|
{
|
|
2963
2963
|
"path": "modules/hall-ui/public/box-panel.js",
|
|
2964
2964
|
"mode": "0000644",
|
|
2965
|
-
"sha256": "
|
|
2965
|
+
"sha256": "d4b32aa4382100a7e60cc83e83ef23ba34dac0ff759143286b2deaebb8f56cb1"
|
|
2966
2966
|
},
|
|
2967
2967
|
{
|
|
2968
2968
|
"path": "modules/hall-ui/public/brand-apply.js",
|
|
@@ -3057,7 +3057,7 @@
|
|
|
3057
3057
|
{
|
|
3058
3058
|
"path": "modules/hall-ui/public/goals-page.js",
|
|
3059
3059
|
"mode": "0000644",
|
|
3060
|
-
"sha256": "
|
|
3060
|
+
"sha256": "fcfe94b3e0cd00ddc881283d9897121518a7acc00b3d05b0715552911e2a0705"
|
|
3061
3061
|
},
|
|
3062
3062
|
{
|
|
3063
3063
|
"path": "modules/hall-ui/public/goals.css",
|
|
@@ -3382,7 +3382,7 @@
|
|
|
3382
3382
|
{
|
|
3383
3383
|
"path": "modules/lifecycle/db.js",
|
|
3384
3384
|
"mode": "0000644",
|
|
3385
|
-
"sha256": "
|
|
3385
|
+
"sha256": "37303ffa7a795ea41db83f346bc736898720a3d1521434f566a71bbeb7b4dc15"
|
|
3386
3386
|
},
|
|
3387
3387
|
{
|
|
3388
3388
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -3477,7 +3477,7 @@
|
|
|
3477
3477
|
{
|
|
3478
3478
|
"path": "modules/lifecycle/routes/goals.js",
|
|
3479
3479
|
"mode": "0000644",
|
|
3480
|
-
"sha256": "
|
|
3480
|
+
"sha256": "a291701ef3b3f8bb548d8e3475707e52562d0c9f03d334a885ff2022c90358dc"
|
|
3481
3481
|
},
|
|
3482
3482
|
{
|
|
3483
3483
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
@@ -4032,12 +4032,12 @@
|
|
|
4032
4032
|
{
|
|
4033
4033
|
"path": "package-lock.json",
|
|
4034
4034
|
"mode": "0000644",
|
|
4035
|
-
"sha256": "
|
|
4035
|
+
"sha256": "f2e15bf06d0e8080e47aeea514dfc26dc267c14a96d32fe805cb240491f0baad"
|
|
4036
4036
|
},
|
|
4037
4037
|
{
|
|
4038
4038
|
"path": "package.json",
|
|
4039
4039
|
"mode": "0000644",
|
|
4040
|
-
"sha256": "
|
|
4040
|
+
"sha256": "c3feb3c561c5bf96e9580673b6995835a0d5674f54f96c8b3109fc304e090556"
|
|
4041
4041
|
},
|
|
4042
4042
|
{
|
|
4043
4043
|
"path": "public-docs/index.html",
|
|
@@ -5657,7 +5657,7 @@
|
|
|
5657
5657
|
{
|
|
5658
5658
|
"path": "src/module-api.js",
|
|
5659
5659
|
"mode": "0000644",
|
|
5660
|
-
"sha256": "
|
|
5660
|
+
"sha256": "a02eb4c1cfa272d700a9776652612143de38566cd86208f059ed1d0fed24934b"
|
|
5661
5661
|
},
|
|
5662
5662
|
{
|
|
5663
5663
|
"path": "src/module-loader/loader.js",
|
|
@@ -5947,7 +5947,7 @@
|
|
|
5947
5947
|
{
|
|
5948
5948
|
"path": "tests/box_code_staleness.mjs",
|
|
5949
5949
|
"mode": "0000644",
|
|
5950
|
-
"sha256": "
|
|
5950
|
+
"sha256": "59d4580504c7352d5f83eec0914f21d0af559b9782bdde1353b5cb3eb2dbb31f"
|
|
5951
5951
|
},
|
|
5952
5952
|
{
|
|
5953
5953
|
"path": "tests/box_connect_e2e.mjs",
|
|
@@ -6497,7 +6497,7 @@
|
|
|
6497
6497
|
{
|
|
6498
6498
|
"path": "tests/goal_membership_requests.mjs",
|
|
6499
6499
|
"mode": "0000644",
|
|
6500
|
-
"sha256": "
|
|
6500
|
+
"sha256": "4bc1ecb2bec4345b55e074a06009d31f97e4b5e06b214669a72e485298307ac8"
|
|
6501
6501
|
},
|
|
6502
6502
|
{
|
|
6503
6503
|
"path": "tests/goal_routes.mjs",
|
|
@@ -6512,7 +6512,7 @@
|
|
|
6512
6512
|
{
|
|
6513
6513
|
"path": "tests/goal_tier.mjs",
|
|
6514
6514
|
"mode": "0000644",
|
|
6515
|
-
"sha256": "
|
|
6515
|
+
"sha256": "508b8b99afe67f64c86b5e1b382052e02a4cc916bec58e28d396ba3716998baf"
|
|
6516
6516
|
},
|
|
6517
6517
|
{
|
|
6518
6518
|
"path": "tests/goal_wall_proof.mjs",
|
package/modules/dev-box/boxes.js
CHANGED
|
@@ -752,8 +752,17 @@ const CODE_STALE_LAG_MS = 2 * 24 * 60 * 60 * 1000; // 2 days behind deployed cod
|
|
|
752
752
|
// { stale, reason } where reason ∈
|
|
753
753
|
// not_active — only active boxes are judged (parked/destroyed → n/a)
|
|
754
754
|
// awaiting_first_report — active, never reported, still inside provision grace
|
|
755
|
-
// never_reported — active, past grace, never reported ⇒
|
|
756
|
-
//
|
|
755
|
+
// never_reported — active, past grace, never reported ⇒ NOT stale. Silence
|
|
756
|
+
// is an unknown, not a verdict: it means the reporter never
|
|
757
|
+
// ran, which is at least as likely to be OUR fault as the
|
|
758
|
+
// box's. It was our fault for the whole life of this feature
|
|
759
|
+
// (task 1002698) — ADR 0072 specified infra/box-report-version.sh
|
|
760
|
+
// and it was never written (task 1002699), so EVERY box in the
|
|
761
|
+
// fleet read as stale, and the banner's only remedy (destroy the
|
|
762
|
+
// box, provision a fresh one) could not possibly clear it. A
|
|
763
|
+
// builder who believed it rebuilt in a loop, losing uncommitted
|
|
764
|
+
// work and spending money each time. Only a box that has ACTUALLY
|
|
765
|
+
// REPORTED can be called stale — see 'behind'.
|
|
757
766
|
// behind — reported, but its commit is > CODE_STALE_LAG_MS older
|
|
758
767
|
// than the server's deployed commit ⇒ STALE (a box whose
|
|
759
768
|
// `pull --ff-only` has been silently failing)
|
|
@@ -772,7 +781,9 @@ function computeCodeStaleness({ state, provisionedAt, codeReportedAt, codeCommit
|
|
|
772
781
|
const prov = toMs(provisionedAt);
|
|
773
782
|
const withinGrace = prov != null && (now - prov) < CODE_REPORT_GRACE_MS;
|
|
774
783
|
if (withinGrace) return { stale: false, reason: 'awaiting_first_report' };
|
|
775
|
-
|
|
784
|
+
// Not stale — see the 'never_reported' note above. The reason is still returned
|
|
785
|
+
// so an operator surface can show "we don't know", which is the honest answer.
|
|
786
|
+
return { stale: false, reason: 'never_reported' };
|
|
776
787
|
}
|
|
777
788
|
|
|
778
789
|
const boxCommit = toMs(codeCommittedAt);
|
|
@@ -183,15 +183,28 @@
|
|
|
183
183
|
function renderBoxActive(box, term, cost) {
|
|
184
184
|
const parts = ['<p class="setup__box-msg setup__box-msg--ok">Your dev box is ready.</p>'];
|
|
185
185
|
|
|
186
|
-
// idea 332 / task 1316 (ADR 0072): flag a box running old code.
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
186
|
+
// idea 332 / task 1316 (ADR 0072): flag a box running old code. The server
|
|
187
|
+
// decides `stale` (boxes.computeCodeStaleness), and since task 1002698 it says
|
|
188
|
+
// so ONLY for reason 'behind' — a box that actually reported a commit far
|
|
189
|
+
// behind the deployed one. Silence ('never_reported') is an unknown and no
|
|
190
|
+
// longer flags, so this banner can't fire on a healthy box any more.
|
|
191
|
+
//
|
|
192
|
+
// Lead with the CHEAP, NON-DESTRUCTIVE fix. A 'behind' box is reporting, so its
|
|
193
|
+
// sync cron is alive and a pull usually clears this on its own; closing the box
|
|
194
|
+
// destroys uncommitted work and costs a fresh spin-up. The old copy named the
|
|
195
|
+
// rebuild as the ONLY remedy ("It can't update itself"), which sent builders
|
|
196
|
+
// straight to the destructive option — and while never_reported still flagged,
|
|
197
|
+
// sent them there in a loop it could never resolve.
|
|
190
198
|
if (box.code && box.code.stale) {
|
|
199
|
+
const days = Number(box.code.lag_ms) > 0
|
|
200
|
+
? Math.floor(Number(box.code.lag_ms) / 86400000) : 0;
|
|
191
201
|
parts.push(
|
|
192
|
-
'<p class="setup__box-msg setup__box-msg--warn">⚠ This box is running <strong>
|
|
193
|
-
+
|
|
194
|
-
+ '
|
|
202
|
+
'<p class="setup__box-msg setup__box-msg--warn">⚠ This box is running <strong>older code</strong>'
|
|
203
|
+
+ (days ? ` (about ${days} day${days === 1 ? '' : 's'} behind)` : '')
|
|
204
|
+
+ ', which can break art generation and reconnecting. '
|
|
205
|
+
+ 'Run <code>/builder-sync</code> in the terminal to pull the latest — that fixes it in place, '
|
|
206
|
+
+ 'and keeps your work. If it is still behind after that, use <strong>Close box</strong> below '
|
|
207
|
+
+ 'and provision a fresh one.</p>'
|
|
195
208
|
);
|
|
196
209
|
}
|
|
197
210
|
|
|
@@ -1237,7 +1237,13 @@
|
|
|
1237
1237
|
// manager-only).
|
|
1238
1238
|
const lockSpan = g.visibility === 'private'
|
|
1239
1239
|
? '<span class="goal-badge goal-badge--locked" title="Private — join is by invitation or an approved request">🔒 Private</span>' : '';
|
|
1240
|
-
|
|
1240
|
+
// task 1002689 — a PROTECTED-scope goal is join-gated too (Archon-mediated
|
|
1241
|
+
// membership, ADR 0086 §3): badge it so it doesn't read as open-join. The
|
|
1242
|
+
// flag is server-derived (scope_protected on the goal payload) — the client
|
|
1243
|
+
// still never re-derives the protected roster.
|
|
1244
|
+
const gateSpan = g.scope_protected
|
|
1245
|
+
? '<span class="goal-badge goal-badge--locked" title="Protected module scope — join by request; an Archon admits members">🛡 Archon-mediated</span>' : '';
|
|
1246
|
+
meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${lockSpan} ${gateSpan} ${scope} ${depsSpan}`;
|
|
1241
1247
|
}
|
|
1242
1248
|
|
|
1243
1249
|
// task 1762: the goal's own owner/manager (or an Archon) can invite/manage
|
|
@@ -1266,11 +1272,13 @@
|
|
|
1266
1272
|
|
|
1267
1273
|
// task 1953 — the goal-detail Actions row: Leave/Join (unchanged), a manager's
|
|
1268
1274
|
// Private/Public toggle (PATCH /goals/:id/visibility), and — for a non-member
|
|
1269
|
-
// viewer of a PRIVATE goal — Request to join / Requested
|
|
1270
|
-
// Join
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1275
|
+
// viewer of a PRIVATE or PROTECTED-scope goal — Request to join / Requested
|
|
1276
|
+
// (withdraw) in place of Join. Both are join-GATED for a non-Archon non-member:
|
|
1277
|
+
// POST /goals/:id/join 409s goal_private on a private goal and 403s
|
|
1278
|
+
// archon_approval_required on a protected-scope one, while the server accepts a
|
|
1279
|
+
// join-request on either (task 1002689 — scope_protected is server-derived on
|
|
1280
|
+
// the goal payload). An Archon keeps the plain Join button: the server lets
|
|
1281
|
+
// them self-join any goal, so "request" would be a pointless extra step.
|
|
1274
1282
|
function renderActions(g, { joined, canManage }) {
|
|
1275
1283
|
const actions = $('#goal-detail-actions');
|
|
1276
1284
|
if (!actions) return;
|
|
@@ -1283,7 +1291,7 @@
|
|
|
1283
1291
|
if (g.status === 'open') {
|
|
1284
1292
|
if (joined) {
|
|
1285
1293
|
parts.push('<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>');
|
|
1286
|
-
} else if (g.visibility === 'private' && !isArchon()) {
|
|
1294
|
+
} else if ((g.visibility === 'private' || g.scope_protected) && !isArchon()) {
|
|
1287
1295
|
if (state.myPendingRequest) {
|
|
1288
1296
|
parts.push('<span class="role-pill" title="Awaiting the owner/manager\'s approval">Requested</span>');
|
|
1289
1297
|
parts.push('<button class="btn-ghost" id="goal-withdraw-btn" type="button">Withdraw</button>');
|
|
@@ -1468,7 +1476,7 @@
|
|
|
1468
1476
|
renderList();
|
|
1469
1477
|
} catch (err) {
|
|
1470
1478
|
const code = err?.data?.error;
|
|
1471
|
-
if (code === 'archon_approval_required') toast('This goal is Archon-mediated —
|
|
1479
|
+
if (code === 'archon_approval_required') toast('This goal is Archon-mediated — request to join instead.');
|
|
1472
1480
|
else if (code === 'goal_private') toast('This goal is private — request to join instead.');
|
|
1473
1481
|
else if (code === 'not_a_member') toast('You are not a member of this goal.');
|
|
1474
1482
|
else if (code === 'goal_not_open') toast('This goal is not open.');
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -823,16 +823,20 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
|
|
|
823
823
|
return { achieved: true, goal: updated };
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
// addGoalMember — join
|
|
827
|
-
//
|
|
828
|
-
//
|
|
826
|
+
// addGoalMember — join. Upserts on the (goal_id, builder_id) PK so a re-join is
|
|
827
|
+
// not a duplicate-key error, and the conflict arm PRESERVES the stored role
|
|
828
|
+
// (task 1002689): overwriting it let POST /goals/:id/join (always role 'member')
|
|
829
|
+
// and a POST /goals/:id/members re-add silently demote a lead — including the
|
|
830
|
+
// owner's row — bypassing authorizeMemberRoleChange's owner-only-demotion
|
|
831
|
+
// guards. Role changes go through PATCH /goals/:id/members/:builderId only
|
|
832
|
+
// (the acceptMembershipRequestAndAddMember precedent). role defaults to 'member'.
|
|
829
833
|
|
|
830
834
|
async function addGoalMember({ goalId, builderId, role = 'member' }, deps = {}) {
|
|
831
835
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
832
836
|
const { rows } = await activePool.query(
|
|
833
837
|
`INSERT INTO goal_members (goal_id, builder_id, role)
|
|
834
838
|
VALUES ($1, $2, $3)
|
|
835
|
-
ON CONFLICT (goal_id, builder_id) DO UPDATE SET role =
|
|
839
|
+
ON CONFLICT (goal_id, builder_id) DO UPDATE SET role = goal_members.role
|
|
836
840
|
RETURNING goal_id, builder_id, role, joined_at`,
|
|
837
841
|
[goalId, builderId, role]
|
|
838
842
|
);
|
|
@@ -373,6 +373,16 @@ function authorizeWithdrawJoinRequest({ request, goalId, actorId }) {
|
|
|
373
373
|
return { ok: true };
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
// decorateGoalScope — additive, derived scope_protected flag on goal payloads
|
|
377
|
+
// (task 1002689). A protected-scope goal is join-gated like a private one
|
|
378
|
+
// (Archon-mediated membership, ADR 0086 §3), but the hall cannot re-derive the
|
|
379
|
+
// protected roster client-side (module-scope-map is server-only) — so the
|
|
380
|
+
// list/detail reads carry the boolean and the UI renders the request-to-join
|
|
381
|
+
// path instead of a dead-end Join button. Never persisted; a pure map.
|
|
382
|
+
function decorateGoalScope(goal) {
|
|
383
|
+
return goal ? { ...goal, scope_protected: scopeIncludesProtected(goal.scope_modules) } : goal;
|
|
384
|
+
}
|
|
385
|
+
|
|
376
386
|
module.exports = function buildGoalsRouter() {
|
|
377
387
|
const router = express.Router();
|
|
378
388
|
|
|
@@ -443,7 +453,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
443
453
|
}
|
|
444
454
|
try {
|
|
445
455
|
const goals = await db.listGoals({ versionId: version || null, status: status || null });
|
|
446
|
-
return res.json({ goals });
|
|
456
|
+
return res.json({ goals: goals.map(decorateGoalScope) });
|
|
447
457
|
} catch (err) {
|
|
448
458
|
console.error('[gds] GET /goals', err);
|
|
449
459
|
return res.fail('list_failed', { status: 500, message: 'internal error' });
|
|
@@ -512,7 +522,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
512
522
|
db.listCriteriaForGoal(id),
|
|
513
523
|
db.getGoalDependencies(id), // task 1761 — prerequisite goals + satisfied state
|
|
514
524
|
]);
|
|
515
|
-
return res.json({ goal, members, criteria, dependencies });
|
|
525
|
+
return res.json({ goal: decorateGoalScope(goal), members, criteria, dependencies });
|
|
516
526
|
} catch (err) {
|
|
517
527
|
console.error('[gds] GET /goals/:id', err);
|
|
518
528
|
return res.fail('fetch_failed', { status: 500, message: 'internal error' });
|
|
@@ -559,7 +569,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
559
569
|
return res.fail('goal_not_open', 409, { status: goal.status });
|
|
560
570
|
}
|
|
561
571
|
if (scopeIncludesProtected(goal.scope_modules) && req.builder.rank !== 'archon') {
|
|
562
|
-
return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; an Archon
|
|
572
|
+
return res.fail('archon_approval_required', { status: 403, message: 'This goal is scoped to a protected module; request to join (POST /goals/:id/requests) or ask an Archon to add you (POST /goals/:id/members).' });
|
|
563
573
|
}
|
|
564
574
|
// Private goals are join-GATED (task 1952 / ADR 0112): a non-member can't
|
|
565
575
|
// self-join — they request to join or wait for an invite. An existing member
|
|
@@ -1493,3 +1503,6 @@ module.exports.authorizeJoinRequestResponse = authorizeJoinRequestResponse;
|
|
|
1493
1503
|
// task 1953: the withdraw gate — exported for tests/goal_membership_requests.mjs,
|
|
1494
1504
|
// same precedent as its siblings above.
|
|
1495
1505
|
module.exports.authorizeWithdrawJoinRequest = authorizeWithdrawJoinRequest;
|
|
1506
|
+
// task 1002689: the derived scope_protected payload decorator — exported for
|
|
1507
|
+
// tests/goal_membership_requests.mjs (DB-free), same precedent.
|
|
1508
|
+
module.exports.decorateGoalScope = decorateGoalScope;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.59",
|
|
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.59",
|
|
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.59",
|
|
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
|
@@ -541,8 +541,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
541
541
|
// landed since 1.19.55 with no explicit bump. run 31535063954. (task 1002620)
|
|
542
542
|
// 1.19.57 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
543
543
|
// landed since 1.19.56 with no explicit bump. run 31542659439. (task 1002620)
|
|
544
|
+
// 1.19.58 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
545
|
+
// landed since 1.19.57 with no explicit bump. run 31546064883. (task 1002620)
|
|
546
|
+
// 1.19.59 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
547
|
+
// landed since 1.19.58 with no explicit bump. run 31547103265. (task 1002620)
|
|
544
548
|
// ---------------------------------------------------------------------------
|
|
545
|
-
const CORE_VERSION = '1.19.
|
|
549
|
+
const CORE_VERSION = '1.19.59'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
546
550
|
|
|
547
551
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
548
552
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -42,14 +42,27 @@ t('active + never reported + just provisioned → not stale (awaiting first repo
|
|
|
42
42
|
assert.equal(r.reason, 'awaiting_first_report');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
t('active + never reported + past the provision grace →
|
|
45
|
+
t('active + never reported + past the provision grace → NOT stale (never_reported is an unknown, task 1002698)', () => {
|
|
46
46
|
const r = boxes.computeCodeStaleness({
|
|
47
47
|
state: 'active', provisionedAt: iso(NOW - 13 * DAY), codeReportedAt: null, now: NOW,
|
|
48
48
|
});
|
|
49
|
-
assert.equal(r.stale,
|
|
49
|
+
assert.equal(r.stale, false);
|
|
50
50
|
assert.equal(r.reason, 'never_reported');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// Regression (task 1002698): the whole fleet reported never_reported because the
|
|
54
|
+
// reporter ADR 0072 specified was never written (task 1002699). Silence must never
|
|
55
|
+
// be enough on its own to tell a builder to destroy a working box.
|
|
56
|
+
t('silence alone never flags a box — a report is required to call it stale', () => {
|
|
57
|
+
for (const age of [31 * 60 * 1000, 13 * DAY, 400 * DAY]) {
|
|
58
|
+
const r = boxes.computeCodeStaleness({
|
|
59
|
+
state: 'active', provisionedAt: iso(NOW - age), codeReportedAt: null,
|
|
60
|
+
serverCommittedAt: iso(NOW - HOUR), now: NOW,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(r.stale, false, `age=${age}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
53
66
|
t('active + reported a commit within the lag window → current', () => {
|
|
54
67
|
const r = boxes.computeCodeStaleness({
|
|
55
68
|
state: 'active',
|
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
authorizeInvitationResponse,
|
|
42
42
|
authorizeJoinRequestResponse,
|
|
43
43
|
authorizeWithdrawJoinRequest,
|
|
44
|
+
decorateGoalScope,
|
|
44
45
|
} = require('../modules/lifecycle/routes/goals.js');
|
|
45
46
|
const { MODULE_EXPECTED_RANKS } = require('../src/bongos/route-rank-check.js');
|
|
46
47
|
|
|
@@ -338,6 +339,30 @@ await test('authorizeWithdrawJoinRequest: the requester (string vs number id)
|
|
|
338
339
|
assert.deepEqual(authorizeWithdrawJoinRequest({ request: REQUEST_FOR_WITHDRAW, goalId: '5', actorId: 2 }), { ok: true });
|
|
339
340
|
});
|
|
340
341
|
|
|
342
|
+
// =========================================================================
|
|
343
|
+
// decorateGoalScope (task 1002689) — goal payloads carry a derived
|
|
344
|
+
// scope_protected flag so the hall can render the join-gate on a
|
|
345
|
+
// protected-scope goal without re-deriving the roster client-side
|
|
346
|
+
// (module-scope-map is server-only). 'kernel' is definitionally protected;
|
|
347
|
+
// 'ideas' is not in the protected set.
|
|
348
|
+
// =========================================================================
|
|
349
|
+
|
|
350
|
+
await test('decorateGoalScope: protected-scope goal → scope_protected true', () => {
|
|
351
|
+
const g = decorateGoalScope({ id: 1, scope_modules: ['kernel'] });
|
|
352
|
+
assert.equal(g.scope_protected, true);
|
|
353
|
+
});
|
|
354
|
+
await test('decorateGoalScope: unprotected / empty scope → false; null passes through', () => {
|
|
355
|
+
assert.equal(decorateGoalScope({ id: 1, scope_modules: ['ideas'] }).scope_protected, false);
|
|
356
|
+
assert.equal(decorateGoalScope({ id: 1, scope_modules: [] }).scope_protected, false);
|
|
357
|
+
assert.equal(decorateGoalScope(null), null);
|
|
358
|
+
});
|
|
359
|
+
await test('decorateGoalScope: returns a copy, never mutates the row', () => {
|
|
360
|
+
const row = { id: 1, scope_modules: ['ideas'] };
|
|
361
|
+
const out = decorateGoalScope(row);
|
|
362
|
+
assert.notEqual(out, row);
|
|
363
|
+
assert.equal('scope_protected' in row, false);
|
|
364
|
+
});
|
|
365
|
+
|
|
341
366
|
// =========================================================================
|
|
342
367
|
// route-rank pins (task 1952/1953) — the walls are in-handler, so the routes
|
|
343
368
|
// stay 'any-builder'; an accidental requireRank re-gate must be a red build.
|
package/tests/goal_tier.mjs
CHANGED
|
@@ -154,13 +154,14 @@ await test('setGoalStatus: unknown goal returns null', async () => {
|
|
|
154
154
|
// =========================================================================
|
|
155
155
|
// addGoalMember
|
|
156
156
|
// =========================================================================
|
|
157
|
-
await test('addGoalMember:
|
|
157
|
+
await test('addGoalMember: upsert PRESERVES the existing role (re-join cannot demote a lead)', async () => {
|
|
158
158
|
const pool = fakePool((s) => (/^INSERT INTO goal_members/.test(s) ? { rows: [{ ...MEMBER_ROW, role: 'lead' }] } : null));
|
|
159
|
-
const out = await addGoalMember({ goalId: 7, builderId: 42, role: '
|
|
160
|
-
assert.equal(out.role, 'lead');
|
|
159
|
+
const out = await addGoalMember({ goalId: 7, builderId: 42, role: 'member' }, { pool });
|
|
160
|
+
assert.equal(out.role, 'lead', 'an existing lead row survives a role:member re-add');
|
|
161
161
|
const ins = find(pool.queries, /^INSERT INTO goal_members/);
|
|
162
|
-
assert.deepEqual(ins.params, [7, 42, '
|
|
163
|
-
assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET role =
|
|
162
|
+
assert.deepEqual(ins.params, [7, 42, 'member']);
|
|
163
|
+
assert.match(ins.sql, /ON CONFLICT \(goal_id, builder_id\) DO UPDATE SET role = goal_members\.role/,
|
|
164
|
+
'conflict keeps the stored role — role changes go through PATCH /goals/:id/members/:builderId only (task 1002689)');
|
|
164
165
|
});
|
|
165
166
|
|
|
166
167
|
await test('addGoalMember: role defaults to member', async () => {
|