@bongos/core 1.21.4 → 1.21.6
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 +233 -108
- 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 +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +37 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +541 -658
- package/docs/copy-registry.json +1205 -2340
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +47 -28
- package/docs/page-readings.json +1128 -1339
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +27 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +24 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +68 -2
- package/src/bongos/routes/modules.js +17 -0
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/provision.mjs +78 -0
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +428 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -132,6 +132,69 @@ async function recentShipsForRollup(builderId) {
|
|
|
132
132
|
}));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Each craft by its own work, for the hub's profile tiles (task 1004440 /
|
|
136
|
+
// WA6.RP10, goal 1000095). Until now the rollup said "Roles" from the declared
|
|
137
|
+
// preference alone and counted only shipped tasks, so an artist or an ideator who
|
|
138
|
+
// had done real work read on the hub as an engineer with nothing shipped.
|
|
139
|
+
//
|
|
140
|
+
// A craft is EARNED when this builder holds credits in it — the same rule the
|
|
141
|
+
// local profile uses to show a role (src/bongos/role-stats.js, task 1004433),
|
|
142
|
+
// read through the same ports. This is a kernel file, so it reads those ports
|
|
143
|
+
// by key rather than importing role-stats (ADR 0091 §1). For each earned craft it
|
|
144
|
+
// sends that craft's headline counts; an unearned craft sends nothing at all.
|
|
145
|
+
// Counts and sums only: no idea title, no page wording, no task list.
|
|
146
|
+
//
|
|
147
|
+
// `disciplines` becomes the earned crafts, in the builder's preferred order;
|
|
148
|
+
// with nothing earned (or no economy module to ask) it falls back to the declared
|
|
149
|
+
// preference, exactly as before. A port that cannot be asked leaves its numbers
|
|
150
|
+
// null ("could not ask"), never 0. Never throws: a failed read drops its craft's
|
|
151
|
+
// numbers, not the report.
|
|
152
|
+
const ROLLUP_CRAFTS = Object.freeze(['engineer', 'artist', 'ideator']);
|
|
153
|
+
const countOrNull = (v) => (Number.isInteger(v) && v >= 0 ? v : null);
|
|
154
|
+
async function readCraftRollup(builderId, { shipped = 0, preferred = [] } = {}) {
|
|
155
|
+
const fallback = { counts: null, disciplines: Array.isArray(preferred) ? preferred : [] };
|
|
156
|
+
try {
|
|
157
|
+
const reward = seams.resolveOptional('reward');
|
|
158
|
+
const byCraftMap = reward && typeof reward.creditsByCraft === 'function'
|
|
159
|
+
? await reward.creditsByCraft([builderId]) : null;
|
|
160
|
+
const byCraft = byCraftMap ? byCraftMap.get(String(builderId)) : null;
|
|
161
|
+
if (!byCraft) return fallback;
|
|
162
|
+
const prefCrafts = fallback.disciplines
|
|
163
|
+
.map((d) => (typeof reward.craftForPreference === 'function' ? reward.craftForPreference(d) : d))
|
|
164
|
+
.filter((c) => ROLLUP_CRAFTS.includes(c));
|
|
165
|
+
const earned = [...prefCrafts, ...ROLLUP_CRAFTS]
|
|
166
|
+
.filter((c, i, a) => a.indexOf(c) === i && Number(byCraft[c]) > 0);
|
|
167
|
+
if (!earned.length) return fallback;
|
|
168
|
+
const ask = async (port) => {
|
|
169
|
+
const fn = seams.resolveOptional(port);
|
|
170
|
+
if (typeof fn !== 'function') return null;
|
|
171
|
+
try { return (await fn(builderId)) || null; } catch { return null; }
|
|
172
|
+
};
|
|
173
|
+
const [artist, ideator] = await Promise.all([
|
|
174
|
+
earned.includes('artist') ? ask('copy-desk.artistStats') : null,
|
|
175
|
+
earned.includes('ideator') ? ask('ideas.ideatorStats') : null,
|
|
176
|
+
]);
|
|
177
|
+
const credits = (c) => countOrNull(Math.round(Number(byCraft[c])));
|
|
178
|
+
const counts = {};
|
|
179
|
+
if (earned.includes('engineer')) counts.engineer = { shipped: countOrNull(shipped), credits: credits('engineer') };
|
|
180
|
+
if (earned.includes('artist')) {
|
|
181
|
+
counts.artist = { pages_approved: countOrNull(artist && artist.pages_approved), credits: credits('artist') };
|
|
182
|
+
}
|
|
183
|
+
if (earned.includes('ideator')) {
|
|
184
|
+
counts.ideator = {
|
|
185
|
+
filed: countOrNull(ideator && ideator.filed),
|
|
186
|
+
full_ideas: countOrNull(ideator && ideator.full_ideas),
|
|
187
|
+
ratified: countOrNull(ideator && ideator.ratified),
|
|
188
|
+
credits: credits('ideator'),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return { counts, disciplines: earned };
|
|
192
|
+
} catch (err) {
|
|
193
|
+
log.info('[auth] craft rollup read failed (non-blocking):', err.message);
|
|
194
|
+
return fallback;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
135
198
|
// Best-effort cross-project activity rollup to the hub (ADR 0141 §4): report this
|
|
136
199
|
// builder's descriptive talent snapshot (credits, ships, karma, rank, disciplines,
|
|
137
200
|
// and since task 1002803 their last few shipped works) for the cross-project
|
|
@@ -162,26 +225,45 @@ async function reportActivityRollup({ builderId, githubId = null }) {
|
|
|
162
225
|
const row = b.rows[0];
|
|
163
226
|
const ghId = githubId ?? row.github_id;
|
|
164
227
|
if (!/^\d+$/.test(String(ghId ?? ''))) return;
|
|
165
|
-
|
|
228
|
+
// SHIPPED works only (task 1004440): a count with no status filter also
|
|
229
|
+
// counted work that was handed back after shipping. The same predicate as the
|
|
230
|
+
// instance's own profile (lifecycle countShippedTasksForBuilder).
|
|
231
|
+
const shipped = await pool.query(
|
|
232
|
+
`SELECT count(*)::int AS n FROM tasks WHERE shipped_by = $1 AND status = 'shipped'`,
|
|
233
|
+
[builderId],
|
|
234
|
+
);
|
|
235
|
+
const shippedCount = shipped.rows[0] ? shipped.rows[0].n : 0;
|
|
166
236
|
const recentShips = await recentShipsForRollup(builderId);
|
|
167
|
-
await
|
|
237
|
+
const craft = await readCraftRollup(builderId, { shipped: shippedCount, preferred: row.preferred_disciplines });
|
|
238
|
+
const body = {
|
|
239
|
+
client_id: idp.clientId,
|
|
240
|
+
client_secret: idp.clientSecret,
|
|
241
|
+
github_id: ghId,
|
|
242
|
+
credits: Number(row.total_credits) || 0,
|
|
243
|
+
tasks_shipped: shippedCount,
|
|
244
|
+
karma: Number(row.karma) || 0,
|
|
245
|
+
rank: row.rank || null,
|
|
246
|
+
disciplines: craft.disciplines,
|
|
247
|
+
recent_ships: recentShips,
|
|
248
|
+
};
|
|
249
|
+
if (craft.counts) body.crafts = craft.counts;
|
|
250
|
+
const send = (payload) => fetch(`${idp.origin}/api/bongos/sso/activity/rollup`, {
|
|
168
251
|
method: 'POST',
|
|
169
252
|
headers: {
|
|
170
253
|
'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent(),
|
|
171
254
|
'Bongos-Report-As-Of': asOf,
|
|
172
255
|
},
|
|
173
|
-
body: JSON.stringify(
|
|
174
|
-
client_id: idp.clientId,
|
|
175
|
-
client_secret: idp.clientSecret,
|
|
176
|
-
github_id: ghId,
|
|
177
|
-
credits: Number(row.total_credits) || 0,
|
|
178
|
-
tasks_shipped: shipped.rows[0] ? shipped.rows[0].n : 0,
|
|
179
|
-
karma: Number(row.karma) || 0,
|
|
180
|
-
rank: row.rank || null,
|
|
181
|
-
disciplines: Array.isArray(row.preferred_disciplines) ? row.preferred_disciplines : [],
|
|
182
|
-
recent_ships: recentShips,
|
|
183
|
-
}),
|
|
256
|
+
body: JSON.stringify(payload),
|
|
184
257
|
});
|
|
258
|
+
const res = await send(body);
|
|
259
|
+
// A hub older than `crafts` validates the body strictly and refuses the
|
|
260
|
+
// unknown field with a 400, which would lose the whole report. So a refused
|
|
261
|
+
// report that carried crafts goes again once without them: the totals still
|
|
262
|
+
// land, and the craft numbers arrive once that hub upgrades.
|
|
263
|
+
if (body.crafts && res && res.status === 400) {
|
|
264
|
+
const { crafts: _unsent, ...older } = body;
|
|
265
|
+
await send(older);
|
|
266
|
+
}
|
|
185
267
|
} catch (err) {
|
|
186
268
|
log.info('[auth] hub activity rollup failed (non-blocking):', err.message);
|
|
187
269
|
}
|
package/src/bongos/db-kernel.js
CHANGED
|
@@ -55,7 +55,7 @@ async function getBuilderById(id) {
|
|
|
55
55
|
// AFTER INSERT triggers on credit_log + karma_log respectively.
|
|
56
56
|
const { rows } = await pool.query(
|
|
57
57
|
`SELECT id, github_id, github_login, display_name, avatar_url, total_credits, karma, created_at, rank,
|
|
58
|
-
status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, system_role,
|
|
58
|
+
status, deactivated_at, reactivated_at, deactivated_reason, preferred_disciplines, main_discipline, system_role,
|
|
59
59
|
anthropic_email, anthropic_user_id, monthly_budget_usd
|
|
60
60
|
FROM builders WHERE id = $1`,
|
|
61
61
|
[id]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/module-rechecks.js — the module versions that are waiting on, or
|
|
4
|
+
// going through, a RE-CHECK, for the governor's docket (task 1004523, goal
|
|
5
|
+
// 1000125; collector: modules/government/docket.js, decision: ADR 0362).
|
|
6
|
+
//
|
|
7
|
+
// WHY. The Governor City's Release building carries "1 flagged for a re-check".
|
|
8
|
+
// The assessment ledger (core_265 `module_assessment_signals`, ADR 0343) already
|
|
9
|
+
// records it: each version's parts are measured as rows, the CURRENT row per
|
|
10
|
+
// (version, part) is the newest, and two current states mean a governor has a
|
|
11
|
+
// re-check to look at — a Security gate that FAILED (the version should not be
|
|
12
|
+
// offered until someone asks for it to be checked again; asking is
|
|
13
|
+
// POST /store/modules/:key/versions/:version/reassess, `module.assessment.rerun`)
|
|
14
|
+
// and a part still PENDING (a re-check is running). Nothing read this ledger for
|
|
15
|
+
// that question before; this is the one query, served only to the docket.
|
|
16
|
+
//
|
|
17
|
+
// Core, not a module: the store and its assessment tables are core
|
|
18
|
+
// (src/bongos/module-store.js), so the contribution is registered from the core
|
|
19
|
+
// modules route factory on the kernel seam.
|
|
20
|
+
|
|
21
|
+
const RECHECK_LIMIT = 50;
|
|
22
|
+
|
|
23
|
+
// Current signal per (version, part), then the versions with a failed Security
|
|
24
|
+
// gate or any part still pending. Bounded. The DISTINCT ON is served by core_265's
|
|
25
|
+
// idx_module_assessment_signals_current (version_id, part, measured_at DESC, id
|
|
26
|
+
// DESC) — the same column order as the ORDER BY, so it walks the index rather than
|
|
27
|
+
// sorting the table (pinned in tests/governor_docket.mjs). The table grows with
|
|
28
|
+
// module publishes and re-checks, not with builders or requests.
|
|
29
|
+
async function listModuleRechecks({ limit = RECHECK_LIMIT, db = null } = {}) {
|
|
30
|
+
const exec = db || require('./pool').pool; // lazy, so a DB-free test can load this file
|
|
31
|
+
const { rows } = await exec.query(
|
|
32
|
+
`WITH current AS (
|
|
33
|
+
SELECT DISTINCT ON (s.version_id, s.part)
|
|
34
|
+
s.version_id, s.part, s.outcome, s.reason, s.measured_at
|
|
35
|
+
FROM module_assessment_signals s
|
|
36
|
+
ORDER BY s.version_id, s.part, s.measured_at DESC, s.id DESC
|
|
37
|
+
)
|
|
38
|
+
SELECT v.id AS version_id, v.module_key, v.version,
|
|
39
|
+
bool_or(c.part = 'security' AND c.outcome = 'failed') AS security_failed,
|
|
40
|
+
array_agg(c.part ORDER BY c.part) FILTER (WHERE c.outcome = 'pending') AS pending_parts,
|
|
41
|
+
max(c.measured_at) AS measured_at
|
|
42
|
+
FROM current c
|
|
43
|
+
JOIN store_module_versions v ON v.id = c.version_id
|
|
44
|
+
GROUP BY v.id, v.module_key, v.version
|
|
45
|
+
HAVING bool_or(c.part = 'security' AND c.outcome = 'failed')
|
|
46
|
+
OR bool_or(c.outcome = 'pending')
|
|
47
|
+
ORDER BY max(c.measured_at) DESC
|
|
48
|
+
LIMIT $1`,
|
|
49
|
+
[limit],
|
|
50
|
+
);
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// One version as a docket matter. Pure. A failed gate needs a governor who may
|
|
55
|
+
// ask for a re-check; a pending part is a re-check already moving.
|
|
56
|
+
function moduleRecheckMatter(r, mayRerun) {
|
|
57
|
+
const failed = r.security_failed === true;
|
|
58
|
+
const pending = Array.isArray(r.pending_parts) ? r.pending_parts.filter(Boolean) : [];
|
|
59
|
+
return {
|
|
60
|
+
id: r.version_id,
|
|
61
|
+
rule: 'rule',
|
|
62
|
+
state: failed ? (mayRerun ? 'needs' : 'moving') : 'moving',
|
|
63
|
+
title: failed
|
|
64
|
+
? `${r.module_key} ${r.version} failed its security check`
|
|
65
|
+
: `${r.module_key} ${r.version} is being checked again`,
|
|
66
|
+
short: failed ? `re-check ${r.module_key}` : `checking ${r.module_key}`,
|
|
67
|
+
where: failed ? 'release · modules · flagged for a re-check' : `release · modules · ${pending.join(', ') || 'a part'} pending`,
|
|
68
|
+
opened_at: r.measured_at,
|
|
69
|
+
deadline: null,
|
|
70
|
+
proposer: null,
|
|
71
|
+
tally: null,
|
|
72
|
+
href: '/modules',
|
|
73
|
+
ref: { type: 'module_version', id: r.version_id },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { listModuleRechecks, moduleRecheckMatter, RECHECK_LIMIT };
|
|
@@ -278,6 +278,9 @@ const GOVERNMENT_PINS = {
|
|
|
278
278
|
// task 1004427: the genesis charter + legislation choice — an amendment by
|
|
279
279
|
// another door, so the same atom as proposing one.
|
|
280
280
|
'POST /government/board/charter': 'perm:board.item.open',
|
|
281
|
+
// task 1004523: the governor's docket — every matter awaiting a governor. The
|
|
282
|
+
// coarse gate is the room's; each source inside is scoped to its own atom.
|
|
283
|
+
'GET /government/docket': 'perm:board.vote.cast',
|
|
281
284
|
};
|
|
282
285
|
|
|
283
286
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
|
@@ -90,12 +90,72 @@ function upgradesLimit(raw) {
|
|
|
90
90
|
return Math.min(100, Math.max(1, n));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// The governor's docket (task 1004523, modules/government/docket.js): a newer
|
|
94
|
+
// core stands at Release as a matter — "taking it is your call". Contributed on
|
|
95
|
+
// the kernel seam because the read is core's (a module cannot require it), and
|
|
96
|
+
// shown only to a viewer holding `core.pin.move`, GET /core-update's own atom (the
|
|
97
|
+
// running version is a vulnerability-window disclosure, ADR 0043). Same
|
|
98
|
+
// readCoreUpdate the banner uses: TTL-cached, never throws; `no_answer` from the
|
|
99
|
+
// registry reads as unavailable, never as "up to date".
|
|
100
|
+
function coreUpdateDocketMatters(update) {
|
|
101
|
+
if (!update || update.reading !== 'ok') throw new Error('core_update_no_answer');
|
|
102
|
+
if (!update.behind) return [];
|
|
103
|
+
return [{
|
|
104
|
+
id: String(update.latest),
|
|
105
|
+
rule: 'decide',
|
|
106
|
+
state: 'needs',
|
|
107
|
+
title: `Take core ${update.latest} (running ${update.running})`,
|
|
108
|
+
short: `core ${update.latest}`,
|
|
109
|
+
where: 'release · core update',
|
|
110
|
+
opened_at: null,
|
|
111
|
+
deadline: null,
|
|
112
|
+
proposer: null,
|
|
113
|
+
tally: null,
|
|
114
|
+
href: '/settings#software-update',
|
|
115
|
+
ref: { type: 'core_version', id: String(update.latest) },
|
|
116
|
+
}];
|
|
117
|
+
}
|
|
118
|
+
// The running core, or null when it cannot be named. Read lazily and fail-soft,
|
|
119
|
+
// the same way the two routes below read it per request.
|
|
120
|
+
function runningCoreVersion() {
|
|
121
|
+
try { return require('../../module-api').CORE_VERSION; } catch { return null; }
|
|
122
|
+
}
|
|
123
|
+
function contributeCoreUpdateToDocket(read = readCoreUpdate) {
|
|
124
|
+
seams.contribute('governor.docket', {
|
|
125
|
+
source: 'core_update',
|
|
126
|
+
building: 'release',
|
|
127
|
+
gate: { permission: 'core.pin.move' },
|
|
128
|
+
read: async () => coreUpdateDocketMatters(await read({ running: runningCoreVersion() })),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// This hall's own update rule, from the provisioning module when it runs (task 1004535):
|
|
133
|
+
// the rule set on /deploy, which the runner and the sweep already follow (ADR 0357: the
|
|
134
|
+
// /deploy row beats the sweep's roster file, which lives in the box's own checkout and is
|
|
135
|
+
// not this server's to read). No module, no row, or a failed read → null, and the readers
|
|
136
|
+
// fall back to the platform default exactly as before.
|
|
137
|
+
function storedSelfChannel() {
|
|
138
|
+
const read = seams.resolveOptional('provisioning.selfUpdateChannel');
|
|
139
|
+
return typeof read === 'function' ? read() : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// { channel, source } — WHICH rule the answer was computed under, so the page can say so:
|
|
143
|
+
// 'deploy' (the project's stored rule) or 'default' (none could be read). Never throws.
|
|
144
|
+
async function resolveChannel(selfChannel) {
|
|
145
|
+
let channel = null;
|
|
146
|
+
try { channel = (await selfChannel()) || null; } catch { channel = null; }
|
|
147
|
+
return { channel, source: channel ? 'deploy' : 'default' };
|
|
148
|
+
}
|
|
149
|
+
|
|
93
150
|
function buildCoreUpdateRouter({
|
|
151
|
+
coreUpdate = readCoreUpdate,
|
|
152
|
+
selfChannel = storedSelfChannel,
|
|
94
153
|
upgrades = readCoreUpgrades,
|
|
95
154
|
softwareUpdate = readSoftwareUpdate,
|
|
96
155
|
canUpdate = holdsCorePin,
|
|
97
156
|
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig() }),
|
|
98
157
|
} = {}) {
|
|
158
|
+
contributeCoreUpdateToDocket();
|
|
99
159
|
const router = express.Router();
|
|
100
160
|
|
|
101
161
|
router.get(
|
|
@@ -115,7 +175,8 @@ function buildCoreUpdateRouter({
|
|
|
115
175
|
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
116
176
|
// No `deploy_url` any more (task 1004296): the banner on a hall without a door of its
|
|
117
177
|
// own links to Settings → Software update, and the hand-off lives on GET /software-update.
|
|
118
|
-
|
|
178
|
+
const { channel, source } = await resolveChannel(selfChannel);
|
|
179
|
+
res.json({ ok: true, update: { ...(await coreUpdate({ running, channel })), channel_source: source } });
|
|
119
180
|
}, { errorCode: 'core_update_failed', message: false }),
|
|
120
181
|
);
|
|
121
182
|
|
|
@@ -146,7 +207,11 @@ function buildCoreUpdateRouter({
|
|
|
146
207
|
asyncHandler('GET /software-update', async (req, res) => {
|
|
147
208
|
let running = null;
|
|
148
209
|
try { running = require('../../module-api').CORE_VERSION; } catch { /* version unknown */ }
|
|
149
|
-
|
|
210
|
+
// canUpdate does not depend on the rule, so it runs beside the rule read, not after it.
|
|
211
|
+
const [update, can] = await Promise.all([
|
|
212
|
+
resolveChannel(selfChannel).then(async ({ channel, source }) => ({ ...(await softwareUpdate({ running, channel })), channel_source: source })),
|
|
213
|
+
canUpdate(req),
|
|
214
|
+
]);
|
|
150
215
|
res.json({ ok: true, update, door: updateDoor(doorInputs()), can_update: can === true });
|
|
151
216
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
152
217
|
);
|
|
@@ -156,3 +221,4 @@ function buildCoreUpdateRouter({
|
|
|
156
221
|
|
|
157
222
|
module.exports = buildCoreUpdateRouter;
|
|
158
223
|
module.exports.hubDeployUrl = hubDeployUrl;
|
|
224
|
+
module.exports.coreUpdateDocketMatters = coreUpdateDocketMatters;
|
|
@@ -77,6 +77,8 @@ const modulesLib = require('../../modules');
|
|
|
77
77
|
// server process.
|
|
78
78
|
const moduleCli = require('../../../scripts/gds/module');
|
|
79
79
|
const moduleSubmissions = require('../module-submissions');
|
|
80
|
+
const moduleRechecks = require('../module-rechecks');
|
|
81
|
+
const seams = require('../../module-seams');
|
|
80
82
|
const moduleArtifact = require('../../../scripts/gds/module-artifact');
|
|
81
83
|
const moduleAssessVersion = require('../../../scripts/gds/module-assess-version');
|
|
82
84
|
const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } = require('../module-entitlements');
|
|
@@ -160,7 +162,22 @@ const HOWTO_CACHE_MAX = 200;
|
|
|
160
162
|
// list bounded.
|
|
161
163
|
const STORE_LATEST_MAX_KEYS = 200;
|
|
162
164
|
|
|
165
|
+
// The governor's docket (task 1004523, ADR 0362): module versions flagged for, or
|
|
166
|
+
// going through, a re-check stand at Release. Shown only to a viewer holding
|
|
167
|
+
// `module.assessment.rerun`, the atom of the reassess route below, the one act
|
|
168
|
+
// a governor can take on one (there is no assessment read route to borrow from).
|
|
169
|
+
function contributeModuleRechecksToDocket() {
|
|
170
|
+
seams.contribute('governor.docket', {
|
|
171
|
+
source: 'module_rechecks',
|
|
172
|
+
building: 'release',
|
|
173
|
+
gate: { permission: 'module.assessment.rerun' },
|
|
174
|
+
read: async (ctx) => (await moduleRechecks.listModuleRechecks())
|
|
175
|
+
.map((r) => moduleRechecks.moduleRecheckMatter(r, ctx.has('module.assessment.rerun'))),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
module.exports = function buildModulesRouter() {
|
|
180
|
+
contributeModuleRechecksToDocket();
|
|
164
181
|
const router = express.Router();
|
|
165
182
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
166
183
|
// router starts empty.
|
|
@@ -941,6 +941,16 @@ function mountInternalSurfaces(app) {
|
|
|
941
941
|
// statically, and a comment in that gap drops the regex out of the map, which
|
|
942
942
|
// makes the page test as UNGATED while looking gated here. Prose goes above.
|
|
943
943
|
const BOARD_ROOM_PAGE_RE = /^\/builders\/board-room(?:\.html)?\/?$/;
|
|
944
|
+
// /city — the Governor City's home (task 1004524, goal 1000125). Gated on the
|
|
945
|
+
// `board.vote.cast` atom, like the Board Room, because that is the atom its one
|
|
946
|
+
// governed read (GET /government/docket) checks: the page's reach equals the
|
|
947
|
+
// right to read what it draws, so no holder is refused a city they could read
|
|
948
|
+
// and no one else is served its shell. Its two goal reads are requireBuilder,
|
|
949
|
+
// a wider floor.
|
|
950
|
+
//
|
|
951
|
+
// NB as at PROJECT_SETTINGS_PAGE_RE: nothing between the `=` and the literal
|
|
952
|
+
// (tests/hall_page_gate_map.mjs parses this file statically). Prose goes above.
|
|
953
|
+
const CITY_PAGE_RE = /^\/builders\/city(?:\.html)?\/?$/;
|
|
944
954
|
// /deploy — the owner's deploy door (task 1003159, ADR 0293). Gated on the
|
|
945
955
|
// `core.pin.move` atom, the same key its preview and its move both check, so the
|
|
946
956
|
// shell's reach equals the right to press the button. Archon-floor and system:true,
|
|
@@ -1023,6 +1033,10 @@ function mountInternalSurfaces(app) {
|
|
|
1023
1033
|
if (BOARD_ROOM_PAGE_RE.test(norm)) {
|
|
1024
1034
|
return gdsAuth.requireBoardVotePage(req, res, next);
|
|
1025
1035
|
}
|
|
1036
|
+
// task 1004524: the Governor City, on the docket's own atom (see CITY_PAGE_RE).
|
|
1037
|
+
if (CITY_PAGE_RE.test(norm)) {
|
|
1038
|
+
return gdsAuth.requireBoardVotePage(req, res, next);
|
|
1039
|
+
}
|
|
1026
1040
|
// ADR 0293: the deploy door, on the same atom its two routes check.
|
|
1027
1041
|
if (DEPLOY_PAGE_RE.test(norm)) {
|
|
1028
1042
|
return gdsAuth.requireCorePinPage(req, res, next);
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
//
|
|
19
19
|
// THE CHANNEL RULE IS NOT RESTATED HERE, as in core-update.js: `resolveChannelTarget` is the
|
|
20
20
|
// upgrade runner's own function, so this never offers a version the runner would refuse.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// The channel is the project's own update rule when the route can read it (the rule set on
|
|
22
|
+
// /deploy, via the provisioning.selfUpdateChannel seam — task 1004535), else the runner's
|
|
23
|
+
// default. The banner (core-update.js) is handed the same rule, so the two cannot disagree.
|
|
23
24
|
//
|
|
24
25
|
// A READING, NEVER A GUESS. `reading` is 'ok' only when the registry answered; "up to date"
|
|
25
26
|
// is `offered: null` on an ok reading and nothing else. Notes that could not be read say so,
|
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.6'; // 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');
|
|
@@ -74,7 +74,9 @@ const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg
|
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
76
|
for (const f of ['platform_identity_005_builder_activity.sql', 'platform_identity_012_recent_ships.sql',
|
|
77
|
-
'platform_identity_022_activity_snapshots.sql', 'platform_identity_026_activity_reported_as_of.sql'
|
|
77
|
+
'platform_identity_022_activity_snapshots.sql', 'platform_identity_026_activity_reported_as_of.sql',
|
|
78
|
+
// upsertActivity also writes crafts (029, task 1004440): 42703 without it.
|
|
79
|
+
'platform_identity_029_activity_crafts.sql']) {
|
|
78
80
|
await q(migrationSql(f));
|
|
79
81
|
}
|
|
80
82
|
await q(migrationSql('platform_identity_026_activity_reported_as_of.sql')); // idempotent: replays cleanly
|
|
@@ -99,6 +99,8 @@ try {
|
|
|
99
99
|
await q(migrationSql('platform_identity_022_activity_snapshots.sql'));
|
|
100
100
|
// upsertActivity also writes reported_as_of (task 1004268) — same reason as 012 above.
|
|
101
101
|
await q(migrationSql('platform_identity_026_activity_reported_as_of.sql'));
|
|
102
|
+
// ...and each craft's counts (029, task 1004440), same reason again.
|
|
103
|
+
await q(migrationSql('platform_identity_029_activity_crafts.sql'));
|
|
102
104
|
await cleanup();
|
|
103
105
|
|
|
104
106
|
// 1 — the writer snapshots the LIVE row, and only when it changed.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// tests/autobongos_grade_cap.mjs — the grade cap for one autonomous run (task 1004536).
|
|
2
|
+
//
|
|
3
|
+
// Owner rule, 2026-10-02: a task that fails the grade twice goes to the owner
|
|
4
|
+
// instead of being retried — the first grade plus at most ONE re-grade per
|
|
5
|
+
// worker run. The worker prompt already asked for this and was ignored (task
|
|
6
|
+
// 1004512 spent $63.81 over three rounds), so the cap is enforced in ship.js,
|
|
7
|
+
// where the panel spend happens, and read back by the supervisor.
|
|
8
|
+
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const cap = require('../scripts/gds/autobongos-grade-cap.js');
|
|
20
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
|
|
22
|
+
const fail = { passed: false, score: 5.6, signals: { panel_outcome: 'fail' } };
|
|
23
|
+
const pass = { passed: true, score: 8.1, signals: { panel_outcome: 'pass' } };
|
|
24
|
+
const outage = { passed: false, score: 0, signals: { panel_outcome: 'unavailable' } };
|
|
25
|
+
|
|
26
|
+
function tmpLedger() {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grade-cap-'));
|
|
28
|
+
return path.join(dir, 'run.jsonl');
|
|
29
|
+
}
|
|
30
|
+
function ledgerWith(grades, taskId = 4242) {
|
|
31
|
+
const file = tmpLedger();
|
|
32
|
+
const env = { [cap.LEDGER_ENV]: file };
|
|
33
|
+
for (const g of grades) assert.equal(cap.recordGradeOutcome({ taskId, grade: g, env }), true);
|
|
34
|
+
return { file, env };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('the cap is the first grade plus ONE re-grade', () => {
|
|
38
|
+
assert.equal(cap.GRADE_CAP, 2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('one failed grade leaves room for exactly one re-grade', () => {
|
|
42
|
+
const { env } = ledgerWith([fail]);
|
|
43
|
+
assert.equal(cap.checkGradeCap({ taskId: 4242, env }).ok, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('a third grade attempt after two FAILs is refused, and says to hand it to the owner', () => {
|
|
47
|
+
const { env } = ledgerWith([fail, fail]);
|
|
48
|
+
const r = cap.checkGradeCap({ taskId: 4242, env });
|
|
49
|
+
assert.equal(r.ok, false);
|
|
50
|
+
assert.equal(r.state.exhausted, true);
|
|
51
|
+
assert.match(r.lines.join('\n'), /cannot_complete/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a grader-UNAVAILABLE outcome does not count toward the two', () => {
|
|
55
|
+
const { env, file } = ledgerWith([fail, outage, outage]);
|
|
56
|
+
const r = cap.checkGradeCap({ taskId: 4242, env });
|
|
57
|
+
assert.equal(r.ok, true, 'an outage is not a quality verdict — the panel never ran');
|
|
58
|
+
const s = cap.gradeCapState(cap.readLedger(file), 4242);
|
|
59
|
+
assert.equal(s.graded, 1);
|
|
60
|
+
assert.equal(s.outages, 2);
|
|
61
|
+
assert.equal(s.exhausted, false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a second round that PASSED is not refused — its merge leg still has to run', () => {
|
|
65
|
+
const { env } = ledgerWith([fail, pass]);
|
|
66
|
+
assert.equal(cap.checkGradeCap({ taskId: 4242, env }).ok, true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('rounds are counted per task, and a human ship (no ledger) is never capped', () => {
|
|
70
|
+
const { env } = ledgerWith([fail, fail], 1111);
|
|
71
|
+
assert.equal(cap.checkGradeCap({ taskId: 4242, env }).ok, true, 'another task\'s fails are not this one\'s');
|
|
72
|
+
assert.deepEqual(cap.checkGradeCap({ taskId: 4242, env: {} }), { ok: true, active: false });
|
|
73
|
+
assert.equal(cap.recordGradeOutcome({ taskId: 4242, grade: fail, env: {} }), false, 'nothing is written without a ledger');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a torn or missing ledger reads as empty rather than throwing', () => {
|
|
77
|
+
assert.deepEqual(cap.readLedger(path.join(os.tmpdir(), 'no-such-ledger-1004536.jsonl')), []);
|
|
78
|
+
const file = tmpLedger();
|
|
79
|
+
fs.writeFileSync(file, '{"task_id":4242,"passed":false,"outage":false}\n{"task_id":42');
|
|
80
|
+
assert.equal(cap.readLedger(file).length, 1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('server attempts: an outage is every GATING worker erroring; the advisory narc does not decide it', () => {
|
|
84
|
+
const w = (kind, error) => ({ kind, verdict: error ? null : 'fail', error, severities: [] });
|
|
85
|
+
assert.equal(cap.attemptIsOutage({ workers: [w('narc', null), w('quality', 'X'), w('hacker', 'X'), w('efficiency', 'X')] }), true);
|
|
86
|
+
assert.equal(cap.attemptIsOutage({ workers: [w('narc', 'X'), w('quality', null), w('hacker', 'X'), w('efficiency', 'X')] }), false, 'one gating worker ran: a real verdict');
|
|
87
|
+
assert.equal(cap.attemptIsOutage({ workers: [] }), false, 'an unknown shape is counted, never a free round');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('server attempts: only rounds newer than the pre-run baseline belong to this run', () => {
|
|
91
|
+
const attempts = [
|
|
92
|
+
{ attempted_at: '2026-10-01T00:00:00Z', passed: false, workers: [] },
|
|
93
|
+
{ attempted_at: '2026-10-02T00:00:00Z', passed: false, workers: [] },
|
|
94
|
+
];
|
|
95
|
+
const baseline = cap.newestAttemptAt(attempts.slice(0, 1));
|
|
96
|
+
const rows = cap.roundsFromAttempts(attempts, 4242, baseline);
|
|
97
|
+
assert.equal(rows.length, 1);
|
|
98
|
+
assert.equal(cap.newestAttemptAt([]), null);
|
|
99
|
+
assert.equal(cap.roundsFromAttempts(attempts, 4242, null).length, 2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('readServerAttempts: unreadable is null (unknown), never graded is [] (a real zero)', async () => {
|
|
103
|
+
assert.equal(await cap.readServerAttempts({}, 4242), null);
|
|
104
|
+
assert.equal(await cap.readServerAttempts({ tasks: { getTasksId: async () => ({ ok: false, status: 502 }) } }, 4242), null);
|
|
105
|
+
assert.deepEqual(await cap.readServerAttempts({ tasks: { getTasksId: async () => ({ ok: true, data: { task: {}, grade: null } }) } }, 4242), []);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// The early refusal (the courtesy half): ship.js, with any flag. It must refuse BEFORE a
|
|
109
|
+
// session is loaded or a panel started — the panel spend is what the cap saves.
|
|
110
|
+
for (const flags of [['--regrade'], [], ['--skip-grade']]) {
|
|
111
|
+
test(`ship.js ${flags.join(' ') || '(plain re-ship)'} refuses a third grade in an autonomous run`, () => {
|
|
112
|
+
const { env } = ledgerWith([fail, fail]);
|
|
113
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'grade-cap-home-'));
|
|
114
|
+
const res = spawnSync(process.execPath, [path.join(REPO, 'scripts', 'gds', 'ship.js'), '4242', ...flags], {
|
|
115
|
+
cwd: REPO,
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
timeout: 30000,
|
|
118
|
+
// No session and a dead API base: if the refusal did NOT fire first, the
|
|
119
|
+
// run fails some other way and the assertions below say so.
|
|
120
|
+
env: { ...process.env, ...env, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: home, BONGOS_API_BASE: 'http://127.0.0.1:9' },
|
|
121
|
+
});
|
|
122
|
+
assert.equal(res.status, cap.REFUSED_EXIT, `expected the cap's exit code; stderr:\n${res.stderr}`);
|
|
123
|
+
assert.match(res.stderr, /graded 2 time\(s\) in this autonomous run/);
|
|
124
|
+
});
|
|
125
|
+
}
|