@bongos/core 1.19.51 → 1.19.53
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 +30 -20
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +2 -0
- package/migrations/core_219_grade_attempts_attempted_at_idx.sql +7 -0
- package/modules/dev-box/app/src/gds-client.js +6 -2
- package/modules/hall-ui/public/watch.js +10 -0
- package/modules/lifecycle/db.js +51 -0
- package/modules/lifecycle/lifecycle.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/public.js +5 -1
- package/src/bongos/serve-internal.js +34 -21
- package/src/module-api.js +5 -1
- package/tests/devbox_downloads.mjs +17 -0
- package/tests/devbox_update_path.mjs +84 -0
- package/tests/grade_attempts.mjs +45 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
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.53",
|
|
6
|
+
"core_contract": "1.19.53",
|
|
7
|
+
"source_commit": "47298870569c3fc932d5b2f0cb2d484ce07c506a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-11T19:17:46.981Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 291,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1219,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1531,
|
|
20
|
+
"tree_sha256": "32b317228ddd8735e031c7475020fc3ea23d32069a4660b906853faadf0af1e0",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "5c89524ec76f5f5df365a188541d50aa31167ae7920124baf562fb32ad30632c"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1312,7 +1312,7 @@
|
|
|
1312
1312
|
{
|
|
1313
1313
|
"path": "docs/api/openapi.json",
|
|
1314
1314
|
"mode": "0000644",
|
|
1315
|
-
"sha256": "
|
|
1315
|
+
"sha256": "0076d08133c342fa905fa90682e606eefa02747d5b11aea9f0bdb3563b293f76"
|
|
1316
1316
|
},
|
|
1317
1317
|
{
|
|
1318
1318
|
"path": "docs/architecture.md",
|
|
@@ -2229,6 +2229,11 @@
|
|
|
2229
2229
|
"mode": "0000644",
|
|
2230
2230
|
"sha256": "5d970696a87e6f663f9fda83a268dbe99ced00ac02fb805d91114f54d4a8fece"
|
|
2231
2231
|
},
|
|
2232
|
+
{
|
|
2233
|
+
"path": "migrations/core_219_grade_attempts_attempted_at_idx.sql",
|
|
2234
|
+
"mode": "0000644",
|
|
2235
|
+
"sha256": "80ff025953285762f9fdfc8de813db3471ade4d02cb6db5a65f72fa13797a0fc"
|
|
2236
|
+
},
|
|
2232
2237
|
{
|
|
2233
2238
|
"path": "modules/autonomy/CLAUDE.md",
|
|
2234
2239
|
"mode": "0000644",
|
|
@@ -2502,7 +2507,7 @@
|
|
|
2502
2507
|
{
|
|
2503
2508
|
"path": "modules/dev-box/app/src/gds-client.js",
|
|
2504
2509
|
"mode": "0000644",
|
|
2505
|
-
"sha256": "
|
|
2510
|
+
"sha256": "323f5e71b7836dd1fb6c7710e1b2159dec6382df89a7c5d001b7e7cccd5aa46d"
|
|
2506
2511
|
},
|
|
2507
2512
|
{
|
|
2508
2513
|
"path": "modules/dev-box/app/src/main.js",
|
|
@@ -3317,7 +3322,7 @@
|
|
|
3317
3322
|
{
|
|
3318
3323
|
"path": "modules/hall-ui/public/watch.js",
|
|
3319
3324
|
"mode": "0000644",
|
|
3320
|
-
"sha256": "
|
|
3325
|
+
"sha256": "e8821ce2c96b637e703879ea3e4840622f2f1846bf8d726d05c455f9bcfbf13e"
|
|
3321
3326
|
},
|
|
3322
3327
|
{
|
|
3323
3328
|
"path": "modules/hall-ui/public/work.css",
|
|
@@ -3387,7 +3392,7 @@
|
|
|
3387
3392
|
{
|
|
3388
3393
|
"path": "modules/lifecycle/db.js",
|
|
3389
3394
|
"mode": "0000644",
|
|
3390
|
-
"sha256": "
|
|
3395
|
+
"sha256": "89febe6a0002d2277a61b14cc08c4076d1ab6c738fdea8f8a4a1348e330c0b9e"
|
|
3391
3396
|
},
|
|
3392
3397
|
{
|
|
3393
3398
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -3422,7 +3427,7 @@
|
|
|
3422
3427
|
{
|
|
3423
3428
|
"path": "modules/lifecycle/lifecycle.js",
|
|
3424
3429
|
"mode": "0000644",
|
|
3425
|
-
"sha256": "
|
|
3430
|
+
"sha256": "5c0d0da5d6a8bcfb920bc432558066840e649cbc624e064dc4e55a926a3d8bb4"
|
|
3426
3431
|
},
|
|
3427
3432
|
{
|
|
3428
3433
|
"path": "modules/lifecycle/merge-lock.js",
|
|
@@ -4037,12 +4042,12 @@
|
|
|
4037
4042
|
{
|
|
4038
4043
|
"path": "package-lock.json",
|
|
4039
4044
|
"mode": "0000644",
|
|
4040
|
-
"sha256": "
|
|
4045
|
+
"sha256": "cd3aa458c21647143fdfaab782c7022dc3e7eed2ae0d227f201b4c4de80503ec"
|
|
4041
4046
|
},
|
|
4042
4047
|
{
|
|
4043
4048
|
"path": "package.json",
|
|
4044
4049
|
"mode": "0000644",
|
|
4045
|
-
"sha256": "
|
|
4050
|
+
"sha256": "7b005ce2936c09e1dd97ea8891db02f41fc77844c87c52c8d2ff876264715d5e"
|
|
4046
4051
|
},
|
|
4047
4052
|
{
|
|
4048
4053
|
"path": "public-docs/index.html",
|
|
@@ -5612,7 +5617,7 @@
|
|
|
5612
5617
|
{
|
|
5613
5618
|
"path": "src/bongos/routes/public.js",
|
|
5614
5619
|
"mode": "0000644",
|
|
5615
|
-
"sha256": "
|
|
5620
|
+
"sha256": "fbfc3422f111e6d3f1429cefec78741d4a919c89985bc9d69f0a7a8e070c52b5"
|
|
5616
5621
|
},
|
|
5617
5622
|
{
|
|
5618
5623
|
"path": "src/bongos/routes/security.js",
|
|
@@ -5632,7 +5637,7 @@
|
|
|
5632
5637
|
{
|
|
5633
5638
|
"path": "src/bongos/serve-internal.js",
|
|
5634
5639
|
"mode": "0000644",
|
|
5635
|
-
"sha256": "
|
|
5640
|
+
"sha256": "2f88796346b1c3a6dc49e25766b223fbf2d99e34b38b9a0f1e64a6fb9a59856d"
|
|
5636
5641
|
},
|
|
5637
5642
|
{
|
|
5638
5643
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -5662,7 +5667,7 @@
|
|
|
5662
5667
|
{
|
|
5663
5668
|
"path": "src/module-api.js",
|
|
5664
5669
|
"mode": "0000644",
|
|
5665
|
-
"sha256": "
|
|
5670
|
+
"sha256": "b3774e26aaaae064f271b354d54d01003130d9d7cada57c8202b6da60db95d0c"
|
|
5666
5671
|
},
|
|
5667
5672
|
{
|
|
5668
5673
|
"path": "src/module-loader/loader.js",
|
|
@@ -6247,7 +6252,7 @@
|
|
|
6247
6252
|
{
|
|
6248
6253
|
"path": "tests/devbox_downloads.mjs",
|
|
6249
6254
|
"mode": "0000644",
|
|
6250
|
-
"sha256": "
|
|
6255
|
+
"sha256": "897fe6b114c612e132e6fa0b3c68f43b865c684c7653e73cf42db894a7e4dc94"
|
|
6251
6256
|
},
|
|
6252
6257
|
{
|
|
6253
6258
|
"path": "tests/devbox_icons.mjs",
|
|
@@ -6264,6 +6269,11 @@
|
|
|
6264
6269
|
"mode": "0000644",
|
|
6265
6270
|
"sha256": "ac0a024d96ef40af52af8b94570447ad5f1556467b68380c1b45fd3419139ad6"
|
|
6266
6271
|
},
|
|
6272
|
+
{
|
|
6273
|
+
"path": "tests/devbox_update_path.mjs",
|
|
6274
|
+
"mode": "0000644",
|
|
6275
|
+
"sha256": "b58e87eeca17053709a8e2bdcefd032060358cc85314d28bf816a92d465fdae8"
|
|
6276
|
+
},
|
|
6267
6277
|
{
|
|
6268
6278
|
"path": "tests/device_flow_start.mjs",
|
|
6269
6279
|
"mode": "0000644",
|
|
@@ -6622,7 +6632,7 @@
|
|
|
6622
6632
|
{
|
|
6623
6633
|
"path": "tests/grade_attempts.mjs",
|
|
6624
6634
|
"mode": "0000644",
|
|
6625
|
-
"sha256": "
|
|
6635
|
+
"sha256": "68792cfc57d211958190732e5c20051ff66c4b4d2ee3e39431db66e9dbfceae0"
|
|
6626
6636
|
},
|
|
6627
6637
|
{
|
|
6628
6638
|
"path": "tests/grade_attribution.mjs",
|
|
@@ -77,7 +77,7 @@ export interface GetProvisioningInstancesResponse { instances: unknown }
|
|
|
77
77
|
export interface GetProvisioningOnboardPlanResponse { mode: unknown; steps: unknown }
|
|
78
78
|
export interface GetPublicBlockersIdResponse { blocker: unknown }
|
|
79
79
|
export interface GetPublicBountyTableResponse { table: unknown; currency: unknown }
|
|
80
|
-
export interface GetPublicGradesResponse { window_days: unknown; rolling: unknown; trend: unknown; threshold: unknown; grader_version: unknown }
|
|
80
|
+
export interface GetPublicGradesResponse { window_days: unknown; rolling: unknown; trend: unknown; attempts: unknown; threshold: unknown; grader_version: unknown }
|
|
81
81
|
export interface GetPublicIdeasIdResponse { idea: unknown }
|
|
82
82
|
export interface GetPublicLeaderboardResponse { leaderboard: unknown; page: PageInfo }
|
|
83
83
|
export interface GetPublicProgressResponse { progress: unknown }
|
package/docs/api/openapi.json
CHANGED
|
@@ -13070,6 +13070,7 @@
|
|
|
13070
13070
|
"window_days": {},
|
|
13071
13071
|
"rolling": {},
|
|
13072
13072
|
"trend": {},
|
|
13073
|
+
"attempts": {},
|
|
13073
13074
|
"threshold": {},
|
|
13074
13075
|
"grader_version": {}
|
|
13075
13076
|
},
|
|
@@ -13077,6 +13078,7 @@
|
|
|
13077
13078
|
"window_days",
|
|
13078
13079
|
"rolling",
|
|
13079
13080
|
"trend",
|
|
13081
|
+
"attempts",
|
|
13080
13082
|
"threshold",
|
|
13081
13083
|
"grader_version"
|
|
13082
13084
|
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- lifecycle_003_grade_attempts_attempted_at_idx.sql — task 1002661 (panel finding).
|
|
2
|
+
--
|
|
3
|
+
-- gradeAttemptStats windows grade_attempts on attempted_at, but migration 064
|
|
4
|
+
-- only indexed task_id — every /public/grades call would seq-scan an
|
|
5
|
+
-- append-only table that grows on every grader run. Index the window column.
|
|
6
|
+
CREATE INDEX IF NOT EXISTS idx_grade_attempts_attempted_at
|
|
7
|
+
ON grade_attempts (attempted_at DESC);
|
|
@@ -82,8 +82,12 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
|
|
|
82
82
|
claimableTasks: () => unwrap(gen('').request('GET', '/api/gds/tasks/claimable')),
|
|
83
83
|
claimTask: (taskId) => unwrap(gen('').request('POST', '/api/gds/claims', { body: { task_id: taskId } })),
|
|
84
84
|
boxScope: () => unwrap(gen('').request('GET', '/api/gds/box/scope')),
|
|
85
|
-
// update check (public
|
|
86
|
-
|
|
85
|
+
// update check (public route at the origin root, not under /api/gds).
|
|
86
|
+
// task 1002649: polls the EXTENSIONLESS /version twin, NOT latest.json — a
|
|
87
|
+
// front proxy with a file-matcher can eat *.json paths before Node (the
|
|
88
|
+
// live instance's pre-ADR-0152 vhost 404'd latest.json empty while this
|
|
89
|
+
// endpoint worked), and the updater only needs { version, assets } anyway.
|
|
90
|
+
latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/version')),
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -644,11 +644,21 @@
|
|
|
644
644
|
const rolling = data.rolling || {};
|
|
645
645
|
const n = num(rolling.n);
|
|
646
646
|
const passRate = n > 0 ? num(rolling.n_passed) / n : null;
|
|
647
|
+
// task 1002661: grade_attempts remediation reader — attempts per graded
|
|
648
|
+
// task and, of tasks that failed a round, the share that ended passed.
|
|
649
|
+
const at = data.attempts || null;
|
|
650
|
+
const meanAttempts = at && at.graded_tasks > 0
|
|
651
|
+
? Math.round((at.total_attempts / at.graded_tasks) * 10) / 10
|
|
652
|
+
: null;
|
|
647
653
|
stats.innerHTML = statCards([
|
|
648
654
|
{ num: (rolling.avg_score ?? '—') === '—' ? '—' : escapeHtml(String(rolling.avg_score)), label: 'Avg score' },
|
|
649
655
|
{ num: fmtPct(passRate), label: 'Pass rate' },
|
|
650
656
|
{ num: fmtInt(n), label: 'Sample size' },
|
|
651
657
|
{ num: data.threshold !== undefined && data.threshold !== null ? escapeHtml(String(data.threshold)) : '—', label: 'Threshold' },
|
|
658
|
+
...(at ? [
|
|
659
|
+
{ num: meanAttempts === null ? '—' : escapeHtml(String(meanAttempts)), label: 'Attempts/task' },
|
|
660
|
+
{ num: at.remediation_rate === null ? '—' : fmtPct(at.remediation_rate), label: 'Remediation' },
|
|
661
|
+
] : []),
|
|
652
662
|
]);
|
|
653
663
|
|
|
654
664
|
const trend = Array.isArray(data.trend) ? data.trend.slice(-14) : [];
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -5309,6 +5309,56 @@ async function countGradeAttempts(taskId, queryFn) {
|
|
|
5309
5309
|
return rows[0] ? rows[0].n : 0;
|
|
5310
5310
|
}
|
|
5311
5311
|
|
|
5312
|
+
// gradeAttemptStats — the grade_attempts READER (task 1002661). The table has
|
|
5313
|
+
// been written on every grade since 6D.6 and read by nothing, leaving
|
|
5314
|
+
// remediation rate structurally unmeasurable. Windowed per-task rollup:
|
|
5315
|
+
// remediation_rate = of the tasks that FAILED at least one attempt, the share
|
|
5316
|
+
// whose latest attempt passed (null when no task failed — no denominator).
|
|
5317
|
+
// waived_zero_issue_passes counts the ADR 0164 implausible_pass_waived audit
|
|
5318
|
+
// markers in the same window, so the views can tell "waived on panel
|
|
5319
|
+
// evidence" from "never evaluated" at a glance.
|
|
5320
|
+
async function gradeAttemptStats(days = 7, queryFn) {
|
|
5321
|
+
const q = queryFn || ((text, params) => pool.query(text, params));
|
|
5322
|
+
const d = Number.isFinite(Number(days)) && Number(days) > 0 ? Math.min(Number(days), 90) : 7;
|
|
5323
|
+
const [{ rows }, waived] = await Promise.all([q(
|
|
5324
|
+
`SELECT
|
|
5325
|
+
COALESCE(SUM(n), 0)::int AS total_attempts,
|
|
5326
|
+
COUNT(*)::int AS graded_tasks,
|
|
5327
|
+
COUNT(*) FILTER (WHERE n > 1)::int AS retried_tasks,
|
|
5328
|
+
COUNT(*) FILTER (WHERE had_fail)::int AS tasks_with_fail,
|
|
5329
|
+
COUNT(*) FILTER (WHERE had_fail AND last_passed)::int AS remediated_tasks
|
|
5330
|
+
FROM (
|
|
5331
|
+
SELECT task_id,
|
|
5332
|
+
COUNT(*) AS n,
|
|
5333
|
+
BOOL_OR(NOT passed) AS had_fail,
|
|
5334
|
+
(ARRAY_AGG(passed ORDER BY attempted_at DESC))[1] AS last_passed
|
|
5335
|
+
FROM grade_attempts
|
|
5336
|
+
WHERE attempted_at > now() - ($1 * interval '1 day')
|
|
5337
|
+
GROUP BY task_id
|
|
5338
|
+
) t`,
|
|
5339
|
+
[d]
|
|
5340
|
+
), q(
|
|
5341
|
+
`SELECT COUNT(*)::int AS n
|
|
5342
|
+
FROM task_grades
|
|
5343
|
+
WHERE graded_at > now() - ($1 * interval '1 day')
|
|
5344
|
+
AND signals->'server_guard'->>'implausible_pass_waived' IS NOT NULL`,
|
|
5345
|
+
[d]
|
|
5346
|
+
)]);
|
|
5347
|
+
const s = rows[0] || {};
|
|
5348
|
+
const tasksWithFail = Number(s.tasks_with_fail) || 0;
|
|
5349
|
+
const remediated = Number(s.remediated_tasks) || 0;
|
|
5350
|
+
return {
|
|
5351
|
+
window_days: d,
|
|
5352
|
+
total_attempts: Number(s.total_attempts) || 0,
|
|
5353
|
+
graded_tasks: Number(s.graded_tasks) || 0,
|
|
5354
|
+
retried_tasks: Number(s.retried_tasks) || 0,
|
|
5355
|
+
tasks_with_fail: tasksWithFail,
|
|
5356
|
+
remediated_tasks: remediated,
|
|
5357
|
+
remediation_rate: tasksWithFail > 0 ? Math.round((remediated / tasksWithFail) * 100) / 100 : null,
|
|
5358
|
+
waived_zero_issue_passes: (waived.rows[0] && Number(waived.rows[0].n)) || 0,
|
|
5359
|
+
};
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5312
5362
|
// ---- security_docs: Archon-only sensitive security documents (task 991) -----
|
|
5313
5363
|
//
|
|
5314
5364
|
// The DATED security engagement reports (exploit detail) live in the DB, NOT in
|
|
@@ -5561,6 +5611,7 @@ module.exports = {
|
|
|
5561
5611
|
decideOverrideRequest,
|
|
5562
5612
|
logGradeAttempt,
|
|
5563
5613
|
countGradeAttempts,
|
|
5614
|
+
gradeAttemptStats, // task 1002661: the grade_attempts remediation-rate reader
|
|
5564
5615
|
acquireMergeLease,
|
|
5565
5616
|
releaseMergeLease,
|
|
5566
5617
|
mergeLeaseStatus,
|
|
@@ -41,6 +41,7 @@ module.exports = {
|
|
|
41
41
|
recentShippedTasks: (...a) => db.recentShippedTasks(...a),
|
|
42
42
|
estimationDriftSummary: (...a) => db.estimationDriftSummary(...a),
|
|
43
43
|
rollingAvgGrade: (...a) => db.rollingAvgGrade(...a),
|
|
44
|
+
gradeAttemptStats: (...a) => db.gradeAttemptStats(...a), // task 1002661: remediation-rate reader
|
|
44
45
|
gradeTrendByDay: (...a) => db.gradeTrendByDay(...a),
|
|
45
46
|
// me.js (the profile reader): per-builder claims + estimation + grade reads.
|
|
46
47
|
getActiveClaimsForBuilder: (...a) => db.getActiveClaimsForBuilder(...a),
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.53",
|
|
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.53",
|
|
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.53",
|
|
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",
|
|
@@ -293,9 +293,12 @@ module.exports = function buildPublicRouter() {
|
|
|
293
293
|
const rawDays = parseInt(req.query.days, 10);
|
|
294
294
|
const days = Number.isFinite(rawDays) && rawDays > 0 && rawDays <= 90 ? rawDays : 7;
|
|
295
295
|
const trendDays = days >= 30 ? days : 30;
|
|
296
|
-
const [rolling, trend] = await Promise.all([
|
|
296
|
+
const [rolling, trend, attempts] = await Promise.all([
|
|
297
297
|
lc().rollingAvgGrade(days),
|
|
298
298
|
lc().gradeTrendByDay(trendDays),
|
|
299
|
+
// task 1002661: grade_attempts remediation reader. Best-effort — a
|
|
300
|
+
// mid-migration instance without the table must not break the tile.
|
|
301
|
+
lc().gradeAttemptStats(days).catch(() => null),
|
|
299
302
|
]);
|
|
300
303
|
// Rubric meta via the `grade` kernel PORT (BV1.R73). resolveOptional → null
|
|
301
304
|
// when grading is disabled, so the tile still renders (threshold/version null)
|
|
@@ -307,6 +310,7 @@ module.exports = function buildPublicRouter() {
|
|
|
307
310
|
window_days: days,
|
|
308
311
|
rolling,
|
|
309
312
|
trend,
|
|
313
|
+
attempts, // task 1002661: attempt counts + remediation rate (null pre-migration)
|
|
310
314
|
threshold: rubric ? rubric.threshold : null, // single source of truth — grader-rubric.json
|
|
311
315
|
grader_version: rubric ? rubric.version : null,
|
|
312
316
|
});
|
|
@@ -359,6 +359,38 @@ function isBuildersHost(req) {
|
|
|
359
359
|
// Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
|
|
360
360
|
// entrypoint-specific tail (the game static + themed 404 in server.js; the plain
|
|
361
361
|
// 404 in platform-server.js).
|
|
362
|
+
// The Dev Box download/manifest mini-router (#904, ADR 0045), extracted so the
|
|
363
|
+
// ACTUAL route handlers are hermetically testable (tests/devbox_update_path.mjs
|
|
364
|
+
// mounts them on a throwaway app — the docs_openapi_route pattern) instead of
|
|
365
|
+
// only their pure builder. task 1002649: the manifest is served under BOTH
|
|
366
|
+
// /latest.json and an extensionless /latest twin — a front proxy with a
|
|
367
|
+
// file-matcher can eat *.json paths before Node (the live pre-ADR-0152 vhost
|
|
368
|
+
// 404'd latest.json empty while extensionless siblings worked), so every
|
|
369
|
+
// manifest consumer has a path that survives that class. Registration order
|
|
370
|
+
// matters: the literal routes must precede the `:target` catch-all.
|
|
371
|
+
function mountDevboxDownloads(app, devboxDownloads) {
|
|
372
|
+
const manifest = (_req, res) => {
|
|
373
|
+
// Short TTL: a release bump (config change + deploy) is visible within a
|
|
374
|
+
// minute, same posture as the hall's HTML.
|
|
375
|
+
res.set('Cache-Control', 'public, max-age=60');
|
|
376
|
+
res.json(devboxDownloads.latestJson());
|
|
377
|
+
};
|
|
378
|
+
app.get('/downloads/devbox/latest.json', manifest);
|
|
379
|
+
app.get('/downloads/devbox/latest', manifest);
|
|
380
|
+
// #924: the lightweight update-check endpoint the Dev Box app polls + the hall
|
|
381
|
+
// /settings courtesy line reads.
|
|
382
|
+
app.get('/downloads/devbox/version', (_req, res) => {
|
|
383
|
+
res.set('Cache-Control', 'public, max-age=60');
|
|
384
|
+
res.json(devboxDownloads.versionJson());
|
|
385
|
+
});
|
|
386
|
+
app.get('/downloads/devbox/:target', (req, res) => {
|
|
387
|
+
const url = devboxDownloads.redirectFor(req.params.target);
|
|
388
|
+
if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
|
|
389
|
+
res.set('Cache-Control', 'public, max-age=60');
|
|
390
|
+
res.redirect(302, url);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
362
394
|
function mountInternalSurfaces(app) {
|
|
363
395
|
// Prometheus instrumentation (task #1680, V4.R28a). The counting middleware
|
|
364
396
|
// mounts FIRST so it wraps every surface registered below (and, since
|
|
@@ -477,26 +509,7 @@ function mountInternalSurfaces(app) {
|
|
|
477
509
|
// /builders/downloads/* and 404).
|
|
478
510
|
const devboxCfg = tryLoadInstanceConfig('devbox-app.json');
|
|
479
511
|
if (devboxCfg) {
|
|
480
|
-
|
|
481
|
-
app.get('/downloads/devbox/latest.json', (_req, res) => {
|
|
482
|
-
// Short TTL: a release bump (config change + deploy) is visible within a
|
|
483
|
-
// minute, same posture as the hall's HTML.
|
|
484
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
485
|
-
res.json(devboxDownloads.latestJson());
|
|
486
|
-
});
|
|
487
|
-
// #924: the lightweight update-check endpoint the Dev Box app polls + the hall
|
|
488
|
-
// /settings courtesy line reads. MUST be registered before the `:target` route
|
|
489
|
-
// below, or Express would treat "version" as a download target and 404 it.
|
|
490
|
-
app.get('/downloads/devbox/version', (_req, res) => {
|
|
491
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
492
|
-
res.json(devboxDownloads.versionJson());
|
|
493
|
-
});
|
|
494
|
-
app.get('/downloads/devbox/:target', (req, res) => {
|
|
495
|
-
const url = devboxDownloads.redirectFor(req.params.target);
|
|
496
|
-
if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
|
|
497
|
-
res.set('Cache-Control', 'public, max-age=60');
|
|
498
|
-
res.redirect(302, url);
|
|
499
|
-
});
|
|
512
|
+
mountDevboxDownloads(app, buildDevboxDownloads(devboxCfg));
|
|
500
513
|
}
|
|
501
514
|
|
|
502
515
|
// Standalone `bongos` CLI downloads + the one-line installer (task 1248,
|
|
@@ -979,4 +992,4 @@ function mountInternalSurfaces(app) {
|
|
|
979
992
|
return app;
|
|
980
993
|
}
|
|
981
994
|
|
|
982
|
-
module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
|
|
995
|
+
module.exports = { mountInternalSurfaces, mountDevboxDownloads, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
|
package/src/module-api.js
CHANGED
|
@@ -529,8 +529,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
529
529
|
// landed since 1.19.49 with no explicit bump. run 31524280584. (task 1002620)
|
|
530
530
|
// 1.19.51 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
531
531
|
// landed since 1.19.50 with no explicit bump. run 31525032273. (task 1002620)
|
|
532
|
+
// 1.19.52 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
533
|
+
// landed since 1.19.51 with no explicit bump. run 31526131183. (task 1002620)
|
|
534
|
+
// 1.19.53 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
535
|
+
// landed since 1.19.52 with no explicit bump. run 31527057565. (task 1002620)
|
|
532
536
|
// ---------------------------------------------------------------------------
|
|
533
|
-
const CORE_VERSION = '1.19.
|
|
537
|
+
const CORE_VERSION = '1.19.53'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
534
538
|
|
|
535
539
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
536
540
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -119,5 +119,22 @@ t('config/devbox-app.json parses, resolves every target, and aliases point at re
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
console.log('update manifest (task 1002649 — the payload the devbox-app updater reads):');
|
|
123
|
+
|
|
124
|
+
t('committed config yields a NON-EMPTY manifest — version + at least one asset in both shapes', () => {
|
|
125
|
+
// The live symptom behind task 1002649 was latest.json arriving as an empty
|
|
126
|
+
// body. The route is a pure function of this committed config, so pin here
|
|
127
|
+
// that the manifest can never be empty at the source.
|
|
128
|
+
const d = buildDevboxDownloads(committedConfig);
|
|
129
|
+
for (const shape of [d.latestJson(), d.versionJson()]) {
|
|
130
|
+
assert.match(shape.version, /^\d+\.\d+\.\d+$/, 'manifest carries a semver version');
|
|
131
|
+
assert.ok(Object.keys(shape.assets).length >= 1, 'manifest carries at least one asset');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// The client-path pin (extensionless /version, never .json) lives in
|
|
136
|
+
// tests/devbox_update_path.mjs — NOT here — so it still runs on the neutral
|
|
137
|
+
// core checkout where this file topology-skips (task 1002649).
|
|
138
|
+
|
|
122
139
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
123
140
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// tests/devbox_update_path.mjs — the devbox-app update-check path pin (task 1002649).
|
|
2
|
+
//
|
|
3
|
+
// Deliberately SEPARATE from tests/devbox_downloads.mjs: that file topology-
|
|
4
|
+
// skips wherever config/devbox-app.json is absent (this neutral core checkout),
|
|
5
|
+
// but this pin reads only the app client's SOURCE, so it must run everywhere —
|
|
6
|
+
// including this repo's `unit` lane.
|
|
7
|
+
//
|
|
8
|
+
// Why the pin exists: a front proxy with a file-matcher can eat *.json paths
|
|
9
|
+
// before they reach Node — the live pre-ADR-0152 vhost 404'd
|
|
10
|
+
// /downloads/devbox/latest.json with an empty body while the extensionless
|
|
11
|
+
// /downloads/devbox/version twin worked, silently breaking the app's update
|
|
12
|
+
// check. The updater only needs { version, assets }, which /version carries.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/devbox_update_path.mjs
|
|
15
|
+
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const src = readFileSync(new URL('../modules/dev-box/app/src/gds-client.js', import.meta.url), 'utf8');
|
|
23
|
+
|
|
24
|
+
test('latestRelease polls the extensionless /downloads/devbox/version, never a .json path', () => {
|
|
25
|
+
const line = src.split('\n').find((l) => l.includes('latestRelease:'));
|
|
26
|
+
assert.ok(line, 'gds-client exposes latestRelease');
|
|
27
|
+
assert.ok(line.includes("'/downloads/devbox/version'"), 'latestRelease polls /downloads/devbox/version');
|
|
28
|
+
assert.ok(!line.includes('latest.json'), 'latestRelease must not poll a .json-suffixed path');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ---- The ACTUAL route handlers, over loopback (the docs_openapi_route ----
|
|
32
|
+
// pattern). This is the layer the repo owns: the Express routes must serve a
|
|
33
|
+
// NON-EMPTY manifest on all three paths and redirect targets. (The infra layer
|
|
34
|
+
// above Node — the host's front proxy — cannot be pinned from a unit test;
|
|
35
|
+
// that is blocker 1000105. These pins guarantee that when a request reaches
|
|
36
|
+
// Node, the manifest is never empty.)
|
|
37
|
+
const CFG = {
|
|
38
|
+
releases_repo: 'owner/devbox-releases',
|
|
39
|
+
version: '9.9.9',
|
|
40
|
+
assets: { 'win-x64': 'Setup-9.9.9-x64.exe', 'mac-arm64': 'App-9.9.9-arm64.dmg' },
|
|
41
|
+
aliases: { windows: 'win-x64', mac: 'mac-arm64' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async function withDevboxServer(fn) {
|
|
45
|
+
const express = require('express');
|
|
46
|
+
const { mountDevboxDownloads } = require('../src/bongos/serve-internal.js');
|
|
47
|
+
const { buildDevboxDownloads } = require('../src/devbox-downloads.js');
|
|
48
|
+
const app = express();
|
|
49
|
+
mountDevboxDownloads(app, buildDevboxDownloads(CFG));
|
|
50
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
51
|
+
try {
|
|
52
|
+
await fn(`http://127.0.0.1:${server.address().port}`);
|
|
53
|
+
} finally {
|
|
54
|
+
await new Promise((r) => server.close(r));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test('latest.json and the extensionless /latest twin both serve the full non-empty manifest', async () => {
|
|
59
|
+
await withDevboxServer(async (base) => {
|
|
60
|
+
for (const p of ['/downloads/devbox/latest.json', '/downloads/devbox/latest']) {
|
|
61
|
+
const r = await fetch(base + p);
|
|
62
|
+
assert.equal(r.status, 200, `${p} must 200`);
|
|
63
|
+
assert.match(r.headers.get('content-type') || '', /application\/json/, `${p} content-type`);
|
|
64
|
+
const j = await r.json();
|
|
65
|
+
assert.equal(j.version, '9.9.9');
|
|
66
|
+
assert.ok(Object.keys(j.assets).length >= 1, `${p} manifest must carry assets — the task-1002649 empty-body class`);
|
|
67
|
+
assert.equal(j.assets['win-x64'].file, 'Setup-9.9.9-x64.exe');
|
|
68
|
+
assert.match(j.assets['win-x64'].url, /\/releases\/download\/v9\.9\.9\//);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('/version serves the flat update-check payload; a target redirects 302', async () => {
|
|
74
|
+
await withDevboxServer(async (base) => {
|
|
75
|
+
const v = await fetch(base + '/downloads/devbox/version');
|
|
76
|
+
assert.equal(v.status, 200);
|
|
77
|
+
const j = await v.json();
|
|
78
|
+
assert.equal(j.version, '9.9.9');
|
|
79
|
+
assert.equal(typeof j.assets['win-x64'], 'string', 'flat {target: url} shape');
|
|
80
|
+
const r = await fetch(base + '/downloads/devbox/windows', { redirect: 'manual' });
|
|
81
|
+
assert.equal(r.status, 302);
|
|
82
|
+
assert.match(r.headers.get('location') || '', /Setup-9\.9\.9-x64\.exe$/);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/tests/grade_attempts.mjs
CHANGED
|
@@ -68,6 +68,51 @@ await test('countGradeAttempts returns the count (0 when none)', async () => {
|
|
|
68
68
|
assert.equal(await db.countGradeAttempts(999, () => Promise.resolve({ rows: [] })), 0);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// ---- task 1002661: gradeAttemptStats — the remediation-rate reader ----
|
|
72
|
+
|
|
73
|
+
function statsQueryFn({ agg, waived }) {
|
|
74
|
+
return (text, params) => {
|
|
75
|
+
if (/FROM grade_attempts/.test(text)) return Promise.resolve({ rows: [{ ...agg, _params: params }] });
|
|
76
|
+
if (/implausible_pass_waived/.test(text)) return Promise.resolve({ rows: [{ n: waived }] });
|
|
77
|
+
return Promise.reject(new Error(`unexpected SQL: ${text.slice(0, 60)}`));
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await test('gradeAttemptStats maps the rollup + computes remediation rate', async () => {
|
|
82
|
+
const s = await db.gradeAttemptStats(7, statsQueryFn({
|
|
83
|
+
agg: { total_attempts: 14, graded_tasks: 10, retried_tasks: 3, tasks_with_fail: 4, remediated_tasks: 3 },
|
|
84
|
+
waived: 2,
|
|
85
|
+
}));
|
|
86
|
+
assert.equal(s.window_days, 7);
|
|
87
|
+
assert.equal(s.total_attempts, 14);
|
|
88
|
+
assert.equal(s.graded_tasks, 10);
|
|
89
|
+
assert.equal(s.retried_tasks, 3);
|
|
90
|
+
assert.equal(s.tasks_with_fail, 4);
|
|
91
|
+
assert.equal(s.remediated_tasks, 3);
|
|
92
|
+
assert.equal(s.remediation_rate, 0.75, '3 of 4 failed-at-least-once tasks ended passed');
|
|
93
|
+
assert.equal(s.waived_zero_issue_passes, 2, 'ADR 0164 waived markers counted in-window');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await test('gradeAttemptStats: no failed tasks → remediation_rate null (no denominator)', async () => {
|
|
97
|
+
const s = await db.gradeAttemptStats(7, statsQueryFn({
|
|
98
|
+
agg: { total_attempts: 5, graded_tasks: 5, retried_tasks: 0, tasks_with_fail: 0, remediated_tasks: 0 },
|
|
99
|
+
waived: 0,
|
|
100
|
+
}));
|
|
101
|
+
assert.equal(s.remediation_rate, null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await test('gradeAttemptStats clamps the window (bad input → 7, oversize → 90)', async () => {
|
|
105
|
+
const seen = [];
|
|
106
|
+
const qf = (text, params) => {
|
|
107
|
+
seen.push(params[0]);
|
|
108
|
+
if (/FROM grade_attempts/.test(text)) return Promise.resolve({ rows: [{}] });
|
|
109
|
+
return Promise.resolve({ rows: [{ n: 0 }] });
|
|
110
|
+
};
|
|
111
|
+
await db.gradeAttemptStats('junk', qf);
|
|
112
|
+
await db.gradeAttemptStats(500, qf);
|
|
113
|
+
assert.deepEqual(seen, [7, 7, 90, 90], 'both queries share the clamped window');
|
|
114
|
+
});
|
|
115
|
+
|
|
71
116
|
console.log('');
|
|
72
117
|
console.log(`Grade-attempts (6D.6) tests: ${passed} passed, ${failed} failed`);
|
|
73
118
|
if (failed > 0) process.exit(1);
|