@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 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.51",
6
- "core_contract": "1.19.51",
7
- "source_commit": "fe0b1449ce7c972b972778e74945261c35565c48",
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-11T18:53:46.990Z",
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": 1217,
14
+ "functional_verbatim": 1219,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1529,
20
- "tree_sha256": "c3aec243f4a401c529edd9c426192e56b1264abfa615e29045ac5cf054e33d57",
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": "a5b9642b383c8c415158e091785ff40f28490d8341b4010f4c051b1a68942daf"
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": "612c7a81756f61f2c18d4b35540379cf63f32a4caa9e8d84a64b247d497ad7a3"
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": "845f51622803363dbc1229c325c4d6148087c756812e7e8e049ced7aa479f168"
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": "31dc79e6aaf72260cae3b46eb0fbb356e4e2bb01e3a2cbce3c420dad5221f25e"
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": "8e67cd1325da44cf95d5d8c34427222e555e0458bb17f23f33c1055b1ae7e211"
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": "7d1be0defe08ca1905b04b646b988f9be3baae519ca2393166ce91f5a31e0c5d"
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": "43bd308850fdf28f3fa803c778af55a4a86e72edb62ccd8dc2d3297da95efbe2"
4045
+ "sha256": "cd3aa458c21647143fdfaab782c7022dc3e7eed2ae0d227f201b4c4de80503ec"
4041
4046
  },
4042
4047
  {
4043
4048
  "path": "package.json",
4044
4049
  "mode": "0000644",
4045
- "sha256": "a402f396d0bfd5b10f24685a15ac36f9022c182db018787fbdd96b296e26c5db"
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": "fd3dd30a194cd4638e7ef6fe34d17c1e94183926543f3c662b8cb9ec2b4a6e97"
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": "25c52c06c03af400c94f04f2cda1aaab0c5e5486a084005b9a4e224f345a4871"
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": "3c2f62e446506de3fa855989e8606aad486566e37eed47b61c3a18b835a404f4"
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": "91950e7477f9fff30e361bc32def211e1cb3792fa098e9c839d5db1e1ba21d6a"
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": "2fe60fa9d070ba6946509413561ae5a273d902ed3b24ff113bb089fd9d9097f1"
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 }
@@ -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 static asset at the origin root, not under /api/gds)
86
- latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/latest.json')),
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) : [];
@@ -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.51",
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.51",
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.51",
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
- const devboxDownloads = buildDevboxDownloads(devboxCfg);
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.51'; // CI auto-patch carrier (ADR 0161); see changelog above
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
+ });
@@ -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);