@bongos/core 1.19.804 → 1.19.806

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.
Files changed (241) hide show
  1. package/.bongos-core.json +267 -242
  2. package/.claude/skills/ask-for-help/SKILL.md +6 -6
  3. package/.claude/skills/backlog-review/SKILL.md +1 -1
  4. package/.claude/skills/blocker-review/SKILL.md +7 -7
  5. package/.claude/skills/blocker-solve/SKILL.md +5 -5
  6. package/.claude/skills/bug-triage/SKILL.md +7 -7
  7. package/.claude/skills/builder-claim/SKILL.md +3 -3
  8. package/.claude/skills/builder-cost/SKILL.md +2 -2
  9. package/.claude/skills/builder-end/SKILL.md +2 -2
  10. package/.claude/skills/builder-exit/SKILL.md +7 -7
  11. package/.claude/skills/builder-key/SKILL.md +2 -2
  12. package/.claude/skills/builder-reauth/SKILL.md +2 -2
  13. package/.claude/skills/builder-redteam/SKILL.md +7 -7
  14. package/.claude/skills/builder-release/SKILL.md +2 -2
  15. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  16. package/.claude/skills/builder-setup/SKILL.md +3 -3
  17. package/.claude/skills/builder-ship/SKILL.md +1 -1
  18. package/.claude/skills/builder-stage/SKILL.md +1 -1
  19. package/.claude/skills/builder-start/SKILL.md +1 -1
  20. package/.claude/skills/fix-task/SKILL.md +4 -4
  21. package/.claude/skills/goal-create/SKILL.md +18 -18
  22. package/.claude/skills/goal-review/SKILL.md +7 -7
  23. package/.claude/skills/grade-audit/SKILL.md +5 -5
  24. package/.claude/skills/grade-recover/SKILL.md +4 -4
  25. package/.claude/skills/grader-health/SKILL.md +6 -6
  26. package/.claude/skills/idea-triage/SKILL.md +14 -14
  27. package/.claude/skills/merge-mode/SKILL.md +3 -3
  28. package/.claude/skills/planning-session/SKILL.md +20 -20
  29. package/.claude/skills/priority-session/SKILL.md +10 -10
  30. package/.claude/skills/recall/SKILL.md +1 -1
  31. package/.claude/skills/scan-before-install/SKILL.md +3 -3
  32. package/.claude/skills/status/SKILL.md +2 -2
  33. package/.claude/skills/strand-fix/SKILL.md +2 -2
  34. package/clients/bongos-client/README.md +1 -1
  35. package/clients/bongos-client/bongos-client.global.js +2 -0
  36. package/clients/bongos-client/index.cjs +2 -0
  37. package/clients/bongos-client/index.d.ts +2 -0
  38. package/clients/bongos-client/index.mjs +2 -0
  39. package/docs/api/openapi.json +45 -11
  40. package/docs/api-reference.md +11 -10
  41. package/docs/copy-inventory.md +47 -34
  42. package/docs/copy-registry.json +200 -72
  43. package/docs/module-api-changelog.md +4 -0
  44. package/modules/builder-settings/profile-nudge-prefs.js +1 -1
  45. package/modules/builder-settings/render-prefs.js +1 -1
  46. package/modules/dev-box/app/README.md +1 -1
  47. package/modules/dev-box/app/src/gds-client.js +11 -9
  48. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  49. package/modules/dev-box/app/test/unit.mjs +13 -13
  50. package/modules/dev-box/box-access.js +3 -3
  51. package/modules/dev-box/box-onboard.js +7 -7
  52. package/modules/discord/bug-attachments.js +1 -1
  53. package/modules/economy/cost-classify.js +6 -6
  54. package/modules/grading/grader-rubric.json +1 -1
  55. package/modules/grading/grader-score.js +1 -1
  56. package/modules/hall-ui/module.json +1 -1
  57. package/modules/hall-ui/public/drachmae.js +3 -3
  58. package/modules/hall-ui/public/goals-page.js +1 -1
  59. package/modules/hall-ui/public/goals-render.js +144 -22
  60. package/modules/hall-ui/public/goals.css +16 -1
  61. package/modules/hall-ui/public/idea.js +1 -1
  62. package/modules/hall-ui/public/md-reader.js +1 -1
  63. package/modules/hall-ui/public/people.html +1 -1
  64. package/modules/hall-ui/public/people.js +2 -2
  65. package/modules/hall-ui/public/primer.js +4 -4
  66. package/modules/hall-ui/public/roster.js +1 -1
  67. package/modules/hall-ui/public/shell.js +1 -1
  68. package/modules/hall-ui/public/task.js +1 -1
  69. package/modules/ideas/inbox.js +2 -2
  70. package/modules/lifecycle/db-analytics.js +1 -1
  71. package/modules/lifecycle/done-when.js +4 -4
  72. package/modules/lifecycle/version-close.js +1 -1
  73. package/modules/memory/learnings.js +2 -2
  74. package/modules/memory/memory.js +1 -1
  75. package/modules/onboarding/onboarding.js +1 -1
  76. package/modules/platform-identity/routes/my-projects.js +1 -1
  77. package/modules/platform-identity/routes/sso.js +2 -2
  78. package/modules/public-landing/public/index.html +2 -2
  79. package/modules/public-landing/public/projects-floor.states.json +1 -1
  80. package/modules/public-landing/public/projects.html +1 -1
  81. package/modules/public-landing/public/projects.states.json +1 -1
  82. package/modules/sessions/db.js +120 -0
  83. package/modules/sessions/module.json +2 -2
  84. package/modules/sessions/pollers/session-upload-health.js +111 -0
  85. package/modules/sessions/pulse.js +2 -2
  86. package/modules/sessions/routes/sessions.js +46 -0
  87. package/modules/sessions/upload-health.js +197 -0
  88. package/modules/status-ui/module.json +1 -1
  89. package/modules/status-ui/public/explore.js +1 -1
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/scripts/gds/agent-invoke.js +3 -3
  93. package/scripts/gds/api.js +4 -4
  94. package/scripts/gds/api.sh +1 -1
  95. package/scripts/gds/architect-audit.js +1 -1
  96. package/scripts/gds/artifact-audit.js +2 -2
  97. package/scripts/gds/audit-ships.js +2 -2
  98. package/scripts/gds/backfill-cost.js +1 -1
  99. package/scripts/gds/backfill-sessions.js +1 -1
  100. package/scripts/gds/backfill-subagent-rewards.js +1 -1
  101. package/scripts/gds/backfill-task-rewards.js +2 -2
  102. package/scripts/gds/box-auth-check.js +1 -1
  103. package/scripts/gds/box-sync.js +3 -3
  104. package/scripts/gds/box.js +10 -10
  105. package/scripts/gds/build-cli-package.js +3 -3
  106. package/scripts/gds/capture.js +2 -2
  107. package/scripts/gds/ci-grade.js +1 -1
  108. package/scripts/gds/claim-recall.js +2 -2
  109. package/scripts/gds/cli-lib.js +3 -3
  110. package/scripts/gds/context-pack.js +1 -1
  111. package/scripts/gds/copy-apply.js +2 -2
  112. package/scripts/gds/dev-lib.js +2 -2
  113. package/scripts/gds/fetch-art-key.js +2 -2
  114. package/scripts/gds/fetch-sound-prefs.js +3 -3
  115. package/scripts/gds/grade-smoke.js +1 -1
  116. package/scripts/gds/hooks/pre-push.js +1 -1
  117. package/scripts/gds/init.js +9 -9
  118. package/scripts/gds/key-card.js +1 -1
  119. package/scripts/gds/learning-capture.js +2 -2
  120. package/scripts/gds/linkify-refs.js +2 -2
  121. package/scripts/gds/local-preview-lib.js +1 -1
  122. package/scripts/gds/login.js +11 -10
  123. package/scripts/gds/main-audit.js +1 -1
  124. package/scripts/gds/newcomer-floor.config.json +1 -1
  125. package/scripts/gds/newcomer-floor.js +2 -2
  126. package/scripts/gds/newcomer-floor.templates.json +2 -2
  127. package/scripts/gds/onboarding-ship.js +1 -1
  128. package/scripts/gds/paste-token.js +3 -3
  129. package/scripts/gds/portability-smoke-memory.sh +5 -5
  130. package/scripts/gds/prove-strict-versioning.js +2 -2
  131. package/scripts/gds/provision-net.js +2 -2
  132. package/scripts/gds/provision-units.js +3 -3
  133. package/scripts/gds/pulse.js +2 -2
  134. package/scripts/gds/push-path-brief.js +2 -2
  135. package/scripts/gds/reauth.js +1 -1
  136. package/scripts/gds/recall.js +1 -1
  137. package/scripts/gds/redteam-patrol.js +3 -3
  138. package/scripts/gds/render-ideas.js +5 -5
  139. package/scripts/gds/repo-metrics.js +1 -1
  140. package/scripts/gds/sandbox-stage.js +3 -3
  141. package/scripts/gds/seed-adopt-tasks.js +3 -3
  142. package/scripts/gds/seed-board-room-tasks.js +1 -1
  143. package/scripts/gds/seed-bongos-app-tasks.js +2 -2
  144. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
  145. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
  146. package/scripts/gds/seed-bongos-tasks.js +5 -5
  147. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
  148. package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
  149. package/scripts/gds/seed-bv1-community-tasks.js +1 -1
  150. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
  151. package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
  152. package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
  153. package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
  154. package/scripts/gds/seed-gds-v4-tasks.js +3 -3
  155. package/scripts/gds/seed-medusa-tasks.js +1 -1
  156. package/scripts/gds/seed-productization-tasks.js +1 -1
  157. package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
  158. package/scripts/gds/seed-provisioning-tasks.js +2 -2
  159. package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
  160. package/scripts/gds/seed-v2-world-tasks.js +2 -2
  161. package/scripts/gds/seed-verify-criteria.js +1 -1
  162. package/scripts/gds/session-digest.js +1 -1
  163. package/scripts/gds/session.js +2 -2
  164. package/scripts/gds/setup.js +7 -7
  165. package/scripts/gds/ship-flow.js +3 -3
  166. package/scripts/gds/ship-grade-steps.js +3 -3
  167. package/scripts/gds/ship-memory-sync.js +1 -1
  168. package/scripts/gds/ship-session-upload.js +3 -3
  169. package/scripts/gds/smoke-batch-claim.sh +10 -10
  170. package/scripts/gds/smoke-batch-release.sh +14 -14
  171. package/scripts/gds/smoke-claim-consistency.sh +19 -19
  172. package/scripts/gds/smoke-dependencies.sh +9 -9
  173. package/scripts/gds/smoke-write.sh +11 -11
  174. package/scripts/gds/start.js +1 -1
  175. package/scripts/gds/status.js +2 -2
  176. package/scripts/gds/strand-watch.js +1 -1
  177. package/scripts/gds/triage.js +4 -4
  178. package/scripts/gds/version-close.js +2 -2
  179. package/scripts/hall-preview/README.md +1 -1
  180. package/scripts/hall-preview/server.js +1 -1
  181. package/src/bongos/auth-admission.js +3 -3
  182. package/src/bongos/auth-idp.js +2 -2
  183. package/src/bongos/auth.js +3 -3
  184. package/src/bongos/db.js +2 -2
  185. package/src/bongos/llm-cache.js +2 -2
  186. package/src/bongos/metrics.js +1 -1
  187. package/src/bongos/middleware/audit.js +3 -3
  188. package/src/bongos/middleware/rate-limit.js +1 -1
  189. package/src/bongos/platform-visibility-gate.js +2 -2
  190. package/src/bongos/routes/_helpers.js +1 -1
  191. package/src/bongos/routes/audit-log.js +1 -1
  192. package/src/bongos/routes/auth.js +5 -5
  193. package/src/bongos/routes/backup.js +1 -1
  194. package/src/bongos/routes/builders.js +1 -1
  195. package/src/bongos/routes/instance.js +1 -1
  196. package/src/bongos/routes/live.js +1 -1
  197. package/src/bongos/routes/me.js +2 -2
  198. package/src/bongos/routes/modules.js +2 -2
  199. package/src/bongos/routes/public.js +1 -1
  200. package/src/bongos/routes/security.js +4 -4
  201. package/src/bongos/routes.js +1 -1
  202. package/src/bongos/serve-internal.js +6 -6
  203. package/src/branding.js +1 -1
  204. package/src/module-api.js +1 -1
  205. package/src/module-seams.js +2 -2
  206. package/src/platform-server.js +2 -2
  207. package/tests/api_alias_exceptions.mjs +67 -0
  208. package/tests/application_echo.mjs +12 -12
  209. package/tests/artifact_audit.mjs +1 -1
  210. package/tests/auth_page_gate.mjs +4 -4
  211. package/tests/auth_web_recovery.mjs +11 -11
  212. package/tests/bongos_login.mjs +7 -4
  213. package/tests/box_env.mjs +6 -6
  214. package/tests/bug_attachments.mjs +1 -1
  215. package/tests/cli_sessions.mjs +1 -1
  216. package/tests/dev_lib.mjs +1 -1
  217. package/tests/discord_bugs.mjs +2 -2
  218. package/tests/discord_inbound.mjs +2 -2
  219. package/tests/explore_visible_states.mjs +1 -1
  220. package/tests/goal_invite_consent.mjs +307 -0
  221. package/tests/government_page_gates.mjs +1 -1
  222. package/tests/hall_reference_world.mjs +1 -1
  223. package/tests/idea_ideate_full.mjs +1 -1
  224. package/tests/idp_federation.mjs +2 -2
  225. package/tests/llm-cache.mjs +1 -1
  226. package/tests/manage_manifest_shared_read.mjs +3 -3
  227. package/tests/people_directory_clip.mjs +1 -1
  228. package/tests/planning_session_skill.mjs +1 -1
  229. package/tests/platform_visibility_gate.mjs +15 -15
  230. package/tests/profile_route.mjs +25 -25
  231. package/tests/projects_hub.mjs +2 -2
  232. package/tests/provision.mjs +2 -2
  233. package/tests/provision_settings_apply.mjs +4 -4
  234. package/tests/sandbox_stage.mjs +8 -8
  235. package/tests/scouting_owner_access_and_reach.mjs +3 -3
  236. package/tests/session_upload_health.mjs +329 -0
  237. package/tests/setup_device_flow_fallback.mjs +1 -1
  238. package/tests/skill_ask_for_help.mjs +3 -3
  239. package/tests/skill_grade_audit.mjs +2 -2
  240. package/tests/skill_grader_health.mjs +3 -3
  241. package/tests/smoke_write_contract.mjs +1 -1
@@ -0,0 +1,329 @@
1
+ // tests/session_upload_health.mjs — the session-upload outage signal
2
+ // (task 1003865, goal 1000118).
3
+ //
4
+ // THE INCIDENT. A Windows path-encoding bug made every transcript lookup miss, so
5
+ // each ship skipped its session upload with `transcript_not_found`. No
6
+ // session_records row was written, so no `session.token_reward` credit booked, and
7
+ // since ADR 0146 made cost-plus the SOLE reward stream the affected builder earned
8
+ // nothing for real shipped work — for weeks, before a human noticed. The only
9
+ // trace was a jsonl file on that builder's own laptop, which never reaches the
10
+ // server: from here, a skipped upload and a builder who did not work are the same
11
+ // absence.
12
+ //
13
+ // WHAT THIS FILE PINS, in order of what would actually go wrong:
14
+ // (1) the firing condition, against the REAL outage window's measured numbers;
15
+ // (2) that it stays quiet on the real healthy months, because a check that
16
+ // cries wolf is the state we started in with extra steps;
17
+ // (3) that the predicate never touches session_records.task_ids, which is EMPTY
18
+ // on 54% of rows and would report the linkage gap as an outage;
19
+ // (4) that the alert names the builder, the arithmetic, and the consequence.
20
+ //
21
+ // Run: node tests/session_upload_health.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { test } from 'node:test';
25
+ import { createRequire } from 'node:module';
26
+
27
+ process.env.NODE_ENV = 'test';
28
+ const require = createRequire(import.meta.url);
29
+ const health = require('../modules/sessions/upload-health.js');
30
+ const poller = require('../modules/sessions/pollers/session-upload-health.js');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // (1)+(2) THE REAL NUMBERS.
34
+ //
35
+ // Measured 2026-09-16 against the live corpora (4,495 session records and 2,640
36
+ // ships carrying shipped_at + shipped_by, paged out of /sessions/search and
37
+ // /tasks), replaying the check as a rolling 14-day lookback stepped one day at a
38
+ // time over the ~92 evaluable days. These are per-builder counts within one
39
+ // lookback, not inventions: the OUTAGE rows are the real peak, and the HEALTHY
40
+ // rows are the real 2026-09 shape the same sweep saw.
41
+ //
42
+ // The COUNTS are the measurement and are exact. The logins are NOT — real builder
43
+ // identities have no business in a fixture (audit-authorship.sh forbids the
44
+ // founder's outright, and the others are people too), so they are anonymised to
45
+ // the roles that matter: the builder whose uploads broke, and the ones whose
46
+ // did not.
47
+ // ---------------------------------------------------------------------------
48
+
49
+ // 2026-07-25, the peak of the outage: one builder, 19 ships, not one covered.
50
+ // The row's own account says "at least 16 ships" — this found 19 without being
51
+ // told where to look.
52
+ const OUTAGE_PEAK = [
53
+ { builder_id: '900001', github_login: 'affected-builder', ships: 19, uncovered: 19 },
54
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 41, uncovered: 0 },
55
+ ];
56
+
57
+ // 2026-08-05, the last day the sweep fired — a PARTIAL outage, which is the shape
58
+ // that matters most: a check tuned only to total failure would have gone quiet
59
+ // here while the builder was still losing pay.
60
+ const OUTAGE_TAIL = [
61
+ { builder_id: '900001', github_login: 'affected-builder', ships: 30, uncovered: 11 },
62
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 22, uncovered: 0 },
63
+ ];
64
+
65
+ // A healthy fortnight: the real 2026-09 shape (0.2% corpus-wide uncovered).
66
+ const HEALTHY = [
67
+ { builder_id: '900003', github_login: 'third-builder', ships: 200, uncovered: 0 },
68
+ { builder_id: '900001', github_login: 'affected-builder', ships: 147, uncovered: 1 },
69
+ { builder_id: '900002', github_login: 'healthy-builder', ships: 78, uncovered: 0 },
70
+ ];
71
+
72
+ test('it fires on the real outage peak, and says whose machine', () => {
73
+ const v = health.uploadHealthVerdict(OUTAGE_PEAK);
74
+ assert.equal(v.firing, true, 'the outage this task was filed about must fire');
75
+ assert.equal(v.status, 'uploads-missing');
76
+ assert.equal(v.offenders.length, 1, 'exactly the one builder, not the whole roster');
77
+ assert.equal(v.offenders[0].github_login, 'affected-builder');
78
+ // Naming the builder is what makes it actionable — the failure was one machine.
79
+ assert.match(v.reason, /affected-builder/);
80
+ assert.match(v.reason, /19 of 19/, 'carries its own arithmetic');
81
+ // And it must say what a missing row COSTS, or it reads as missing telemetry.
82
+ assert.match(v.reason, /reward is not booking/i);
83
+ });
84
+
85
+ test('it still fires on the partial tail of the same outage', () => {
86
+ // 11 of 30 is 36.7% — well under half. A threshold set at "mostly broken" would
87
+ // have gone quiet here while the builder was still losing pay for real ships.
88
+ const v = health.uploadHealthVerdict(OUTAGE_TAIL);
89
+ assert.equal(v.firing, true);
90
+ assert.equal(v.offenders[0].github_login, 'affected-builder');
91
+ assert.ok(v.offenders[0].uncovered_share < 0.5, 'and it is genuinely a partial outage');
92
+ });
93
+
94
+ test('it stays quiet on the real healthy month', () => {
95
+ const v = health.uploadHealthVerdict(HEALTHY);
96
+ assert.equal(v.firing, false, 'a healthy fortnight must not page anyone');
97
+ assert.equal(v.status, 'healthy');
98
+ assert.deepEqual(v.offenders, []);
99
+ });
100
+
101
+ test('THE CORPUS-WIDE SHARE IS THE WRONG STATISTIC — the reason for per-builder', () => {
102
+ // The design decision, pinned because it is not obvious and the obvious version
103
+ // is worse than nothing. Folded into one row the way a corpus-wide check would
104
+ // fold it, the real outage peak is 19 uncovered out of 60 ships — and its own
105
+ // tail is 11 of 52, which a single ratio cannot separate from noise. Measured
106
+ // corpus-wide, the outage MONTH ran at 3.4% against 1.7-2.0% in healthy ones.
107
+ const folded = (rows) => [{
108
+ builder_id: 'all', github_login: null,
109
+ ships: rows.reduce((a, r) => a + r.ships, 0),
110
+ uncovered: rows.reduce((a, r) => a + r.uncovered, 0),
111
+ }];
112
+ assert.equal(health.uploadHealthVerdict(folded(OUTAGE_TAIL)).firing, false,
113
+ 'folded into one number, the real outage tail does not fire — which is why the SQL groups by builder');
114
+ // And the same rows, kept per builder, do fire. Same data, different key.
115
+ assert.equal(health.uploadHealthVerdict(OUTAGE_TAIL).firing, true);
116
+ });
117
+
118
+ test('a quiet fortnight is refused, not judged', () => {
119
+ // Three uncovered out of four is 75% and means nothing. A check that fires on
120
+ // someone's slow week teaches people to mute it, which is the state this task
121
+ // is fixing.
122
+ const v = health.uploadHealthVerdict([
123
+ { builder_id: '9', github_login: 'someone', ships: 4, uncovered: 4 },
124
+ ]);
125
+ assert.equal(v.firing, false);
126
+ assert.equal(v.status, 'insufficient-data');
127
+ assert.match(v.reason, /not enough to judge/);
128
+ // But the SAME share over enough ships is the outage.
129
+ const loud = health.uploadHealthVerdict([
130
+ { builder_id: '9', github_login: 'someone', ships: 20, uncovered: 15 },
131
+ ]);
132
+ assert.equal(loud.firing, true);
133
+ });
134
+
135
+ test('garbage counts cannot manufacture an outage', () => {
136
+ // These rows come from SQL, but the verdict decides whether someone is woken
137
+ // up: an uncovered count above the ship count is a query bug, not an outage
138
+ // several times over.
139
+ const v = health.uploadHealthVerdict([
140
+ { builder_id: '9', github_login: 'x', ships: 20, uncovered: 400 },
141
+ { builder_id: null, github_login: 'ghost', ships: 99, uncovered: 99 },
142
+ null,
143
+ ]);
144
+ assert.equal(v.builders.length, 1, 'a row with no builder is dropped, not trusted');
145
+ assert.equal(v.builders[0].uncovered, 20, 'uncovered is clamped to ships');
146
+ assert.equal(health.uploadHealthVerdict(null).status, 'insufficient-data', 'and no rows at all is not an outage');
147
+ assert.equal(health.uploadHealthVerdict([]).firing, false);
148
+ });
149
+
150
+ test('the clamps bound what reaches an SQL interval', () => {
151
+ assert.equal(health.clampLookbackDays(undefined), health.DEFAULT_LOOKBACK_DAYS);
152
+ assert.equal(health.clampLookbackDays('nonsense'), health.DEFAULT_LOOKBACK_DAYS);
153
+ assert.equal(health.clampLookbackDays(1e9), 365, 'a hostile ?days cannot ask for the whole history');
154
+ assert.equal(health.clampLookbackDays(-5), 1);
155
+ assert.equal(health.clampWindowHours(1e9), 720);
156
+ assert.equal(health.clampWindowHours(0), 1);
157
+ assert.equal(health.clampSampleLimit(null), 20);
158
+ assert.equal(health.clampSampleLimit(10_000), 100, 'the sample is evidence, never the corpus');
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // (3) THE PREDICATE, as SQL — the trap the scoping task measured and warned about.
163
+ // ---------------------------------------------------------------------------
164
+
165
+ test('the emitted SQL keys on builder_id + time and never on task_ids', async () => {
166
+ // Captured from the query the db slice ACTUALLY runs, not grepped from source:
167
+ // both files mention task_ids in prose explaining why they do not use it, so a
168
+ // source scan would pass on a version that used it.
169
+ const api = require('../src/module-api.js');
170
+ const db = require('../modules/sessions/db.js');
171
+ const seen = [];
172
+ const prev = api.pool.query;
173
+ api.pool.query = async (sql, params) => { seen.push({ sql: String(sql), params }); return { rows: [] }; };
174
+ try {
175
+ await db.sessionUploadCoverage({ days: 14, windowHours: 24 });
176
+ await db.shipsMissingSessionRecord({ days: 14, windowHours: 24, limit: 20 });
177
+ } finally {
178
+ api.pool.query = prev;
179
+ }
180
+ assert.equal(seen.length, 2, 'both queries ran');
181
+ for (const { sql, params } of seen) {
182
+ assert.ok(!/task_ids/.test(sql), 'task_ids is EMPTY on 54% of rows — keying on it reports the linkage gap as an outage');
183
+ assert.match(sql, /sr\.builder_id = t\.shipped_by/, 'keyed on the builder');
184
+ assert.match(sql, /sr\.uploaded_at >=[\s\S]*sr\.uploaded_at <=/, 'and bounded by a time window on both sides');
185
+ assert.match(sql, /status = 'shipped'/, 'over ships, which is the half the server knows for certain');
186
+ // The interval inputs are BOUND, never interpolated — they come from a URL.
187
+ assert.ok(sql.includes("$1 * interval '1 day'"), 'the lookback is a bound parameter');
188
+ assert.ok(sql.includes("$2 * interval '1 hour'"), 'and so is the window');
189
+ assert.equal(params[0], 14);
190
+ assert.equal(params[1], 24);
191
+ }
192
+ });
193
+
194
+ test('the clamps are applied by the db slice, not merely available to it', async () => {
195
+ // The route is not the only caller, so a clamp the route applies is not a bound.
196
+ const api = require('../src/module-api.js');
197
+ const db = require('../modules/sessions/db.js');
198
+ const seen = [];
199
+ const prev = api.pool.query;
200
+ api.pool.query = async (sql, params) => { seen.push({ sql: String(sql), params }); return { rows: [] }; };
201
+ try {
202
+ await db.sessionUploadCoverage({ days: 1e9, windowHours: 1e9 });
203
+ await db.shipsMissingSessionRecord({ days: 1e9, windowHours: 1e9, limit: 1e9 });
204
+ } finally {
205
+ api.pool.query = prev;
206
+ }
207
+ assert.deepEqual(seen[0].params.slice(0, 2), [365, 720]);
208
+ assert.deepEqual(seen[1].params.slice(0, 2), [365, 720]);
209
+ assert.match(seen[1].sql, /LIMIT 100\b/, 'the sample limit is clamped into the SQL, not passed through');
210
+ });
211
+
212
+ test('the healthy path costs one query — the sample is only fetched when it exists', async () => {
213
+ const api = require('../src/module-api.js');
214
+ const db = require('../modules/sessions/db.js');
215
+ const prev = api.pool.query;
216
+ let calls = 0;
217
+ api.pool.query = async () => {
218
+ calls += 1;
219
+ return { rows: calls === 1 ? [{ builder_id: '3', github_login: 'x', ships: 40, uncovered: 0 }] : [] };
220
+ };
221
+ try {
222
+ const clean = await db.sessionUploadHealth({});
223
+ assert.equal(calls, 1, 'nothing uncovered, so no sample query');
224
+ assert.equal(clean.verdict.firing, false);
225
+ assert.deepEqual(clean.sample, []);
226
+
227
+ calls = 0;
228
+ api.pool.query = async () => {
229
+ calls += 1;
230
+ return { rows: calls === 1 ? [{ builder_id: '3', github_login: 'x', ships: 40, uncovered: 30 }] : [{ id: '7' }] };
231
+ };
232
+ const bad = await db.sessionUploadHealth({});
233
+ assert.equal(calls, 2, 'something uncovered, so the evidence is fetched');
234
+ assert.equal(bad.verdict.firing, true);
235
+ assert.deepEqual(bad.sample, [{ id: '7' }]);
236
+ } finally {
237
+ api.pool.query = prev;
238
+ }
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // (4) THE ALERT. A line that says "unhealthy" and no more is the same silence
243
+ // with a timestamp on it.
244
+ // ---------------------------------------------------------------------------
245
+
246
+ test('the firing log line names the builder, the cost, and where to look', () => {
247
+ const lines = poller.logLinesFor({
248
+ days: 14,
249
+ verdict: health.uploadHealthVerdict(OUTAGE_PEAK),
250
+ sample: [{ id: '1002101' }, { id: '1002100' }],
251
+ }).join('\n');
252
+ assert.match(lines, /ALERT/);
253
+ assert.match(lines, /affected-builder/, 'whose machine');
254
+ assert.match(lines, /19 of 19/, 'the arithmetic');
255
+ assert.match(lines, /unpaid shipped work/, 'what it costs — not "missing telemetry"');
256
+ assert.match(lines, /sessions\/upload-health/, 'where to look next');
257
+ assert.match(lines, /session-upload-skips\.jsonl/, 'and the likely client-side cause');
258
+ assert.match(lines, /#1002101/, 'with the uncovered ships named, so the unpaid work is recoverable');
259
+ });
260
+
261
+ test('the quiet log line is one line, and says which quiet it is', () => {
262
+ const quiet = poller.logLinesFor({ days: 14, verdict: health.uploadHealthVerdict(HEALTHY), sample: [] });
263
+ assert.equal(quiet.length, 1, 'a healthy sweep must not be noisy — hourly, that is 24 lines a day');
264
+ assert.match(quiet[0], /healthy/);
265
+ // 'insufficient-data' must not read as healthy: they are different situations
266
+ // and only one of them is evidence that uploads work.
267
+ const thin = poller.logLinesFor({ days: 14, verdict: health.uploadHealthVerdict([]), sample: [] });
268
+ assert.match(thin[0], /insufficient-data/);
269
+ });
270
+
271
+ test('runSweep is the whole chain, and the loader calls IT — not the helpers', async () => {
272
+ // The (helper-nothing-calls) lesson: logLinesFor and sessionUploadHealth can
273
+ // both be perfect while the poller the loader actually starts reaches neither.
274
+ // Drive the entry point end to end against a patched pool and read what it
275
+ // would have logged.
276
+ const api = require('../src/module-api.js');
277
+ const prevQuery = api.pool.query;
278
+ const prevLogger = api.logger;
279
+ const emitted = [];
280
+ api.logger = () => ({
281
+ info: (...a) => emitted.push(['info', a.map(String).join(' ')]),
282
+ error: (...a) => emitted.push(['error', a.map(String).join(' ')]),
283
+ });
284
+ let calls = 0;
285
+ api.pool.query = async () => {
286
+ calls += 1;
287
+ return {
288
+ rows: calls === 1
289
+ ? [{ builder_id: '900001', github_login: 'affected-builder', ships: 19, uncovered: 19 }]
290
+ : [{ id: '1002101' }],
291
+ };
292
+ };
293
+ // The logger is resolved at module load, so re-import the poller under the stub.
294
+ const fresh = require('node:module').createRequire(import.meta.url);
295
+ delete fresh.cache[fresh.resolve('../modules/sessions/pollers/session-upload-health.js')];
296
+ const live = fresh('../modules/sessions/pollers/session-upload-health.js');
297
+ try {
298
+ const health = await live.runSweep();
299
+ assert.equal(health.verdict.firing, true, 'the sweep reaches the verdict');
300
+ assert.equal(calls, 2, 'and fetches the evidence when there is any');
301
+ const errors = emitted.filter(([lvl]) => lvl === 'error');
302
+ assert.ok(errors.length >= 1, 'a firing sweep logs at ERROR so a level filter cannot hide it');
303
+ assert.match(errors.map(([, m]) => m).join('\n'), /ALERT[\s\S]*affected-builder/,
304
+ 'and what it logs is the alert, not a bare status');
305
+ } finally {
306
+ api.pool.query = prevQuery;
307
+ api.logger = prevLogger;
308
+ delete fresh.cache[fresh.resolve('../modules/sessions/pollers/session-upload-health.js')];
309
+ }
310
+ });
311
+
312
+ test('the poller reports and never writes', () => {
313
+ // It runs beside the reward reconciler, which MOVES MONEY. A telemetry sweep
314
+ // that could book, delete or update anything would need its own safety
315
+ // argument; this one needs none, and that must stay true.
316
+ const src = require('node:fs').readFileSync(
317
+ new URL('../modules/sessions/pollers/session-upload-health.js', import.meta.url), 'utf8');
318
+ const code = src.replace(/\/\/[^\n]*/g, '');
319
+ for (const forbidden of ['INSERT', 'UPDATE', 'DELETE', 'withTx', 'reconcile', 'award']) {
320
+ assert.ok(!new RegExp(forbidden, 'i').test(code), `the health sweep must not ${forbidden}`);
321
+ }
322
+ assert.match(code, /db\.sessionUploadHealth/, 'it reads through the one read entry point');
323
+ assert.equal(typeof poller.start, 'function');
324
+ assert.equal(typeof poller.stop, 'function');
325
+ // Kill-switched and separate from the payout sweep's switch, so silencing the
326
+ // telemetry can never silence the money.
327
+ assert.match(code, /SESSION_UPLOAD_HEALTH_DISABLED/);
328
+ assert.ok(!/SESSION_REWARD_RECONCILE/.test(code), 'and it does not share the payout poller\'s switch');
329
+ });
@@ -120,7 +120,7 @@ const server = http.createServer((req, res) => {
120
120
  res.writeHead(status, { 'Content-Type': 'application/json' });
121
121
  res.end(JSON.stringify(body));
122
122
  };
123
- if (req.method === 'GET' && req.url.endsWith('/api/gds/me')) {
123
+ if (req.method === 'GET' && req.url.endsWith('/api/bongos/me')) {
124
124
  if ((req.headers.authorization || '') === `Bearer ${GOOD_TOKEN}`) {
125
125
  return send(200, { builder: { id: 6, github_login: 'newbuilder', display_name: 'New Builder' } });
126
126
  }
@@ -27,11 +27,11 @@ test('/ask-for-help files a help request and routes its near-neighbours', (t) =>
27
27
  const md = readFileSync(SKILL, 'utf8');
28
28
 
29
29
  // The write path, named literally — this is the whole point of the skill.
30
- assert.match(md, /POST \/(?:api\/gds\/)?help-requests/, 'names the help-request write route');
30
+ assert.match(md, /POST \/(?:api\/bongos\/)?help-requests/, 'names the help-request write route');
31
31
  assert.match(md, /--body-file/, 'files via --body-file (api.js drops a positional body)');
32
32
 
33
33
  // Resolve the addressee — never guess a builder id.
34
- assert.match(md, /GET \/api\/gds\/builders\/directory/, 'resolves a named builder through the directory');
34
+ assert.match(md, /GET \/api\/bongos\/builders\/directory/, 'resolves a named builder through the directory');
35
35
  assert.match(md, /never guess/i, 'forbids guessing a builder id');
36
36
 
37
37
  // The two XOR rules the table's CHECK constraints enforce.
@@ -57,7 +57,7 @@ test('/ask-for-help files a help request and routes its near-neighbours', (t) =>
57
57
  assert.match(md, /help-requests\/for-me/, 'names the addressed read the recipient sees');
58
58
 
59
59
  // Settling is the asker's job — an unsettled pile is noise.
60
- assert.match(md, /PATCH \/api\/gds\/help-requests\/<id>/, 'names the settle route');
60
+ assert.match(md, /PATCH \/api\/bongos\/help-requests\/<id>/, 'names the settle route');
61
61
  assert.match(md, /answered/, 'names the answered status');
62
62
  assert.match(md, /withdrawn/, 'names the withdrawn status');
63
63
 
@@ -23,9 +23,9 @@ test('/grade-audit exists, covers the sweep legs, and stays describe-and-queue o
23
23
  const md = readFileSync(SKILL, 'utf8');
24
24
 
25
25
  // Data surfaces.
26
- assert.match(md, /\/api\/gds\/public\/recent-shipped/, 'sweeps the recent-shipped window');
26
+ assert.match(md, /\/api\/bongos\/public\/recent-shipped/, 'sweeps the recent-shipped window');
27
27
  assert.match(md, /include=grade/, 'reads each task\'s persisted grade');
28
- assert.match(md, /\/api\/gds\/override-requests/, 'cross-references the override ledger');
28
+ assert.match(md, /\/api\/bongos\/override-requests/, 'cross-references the override ledger');
29
29
 
30
30
  // Leg 1 — override ledger: shipped-with-failing-grade, infra vs real split.
31
31
  assert.match(md, /panel_outcome='unavailable'/, 'splits infra outage from a real fail');
@@ -22,9 +22,9 @@ test('/grader-health exists and covers the four health axes on live data', (t) =
22
22
  const md = readFileSync(SKILL, 'utf8');
23
23
 
24
24
  // Live data surfaces, named concretely.
25
- assert.match(md, /\/api\/gds\/public\/grades/, 'reads the public grade trend');
26
- assert.match(md, /\/api\/gds\/grades\/by-builder/, 'reads the per-builder split (Archon)');
27
- assert.match(md, /\/api\/gds\/public\/cost-summary/, 'reads grader spend');
25
+ assert.match(md, /\/api\/bongos\/public\/grades/, 'reads the public grade trend');
26
+ assert.match(md, /\/api\/bongos\/grades\/by-builder/, 'reads the per-builder split (Archon)');
27
+ assert.match(md, /\/api\/bongos\/public\/cost-summary/, 'reads grader spend');
28
28
 
29
29
  // Axis 1 — outage runs: the 21×0.0 pattern and outage-vs-rejection confirmation.
30
30
  assert.match(md, /n_unavailable/, 'names the outage bucket the aggregates expose');
@@ -47,7 +47,7 @@ test('goal_id is the RESOLVED id, never a literal', () => {
47
47
  });
48
48
 
49
49
  test('GOAL_ID is resolved at runtime, from the is_maintenance flag', () => {
50
- assert.match(SH, /GOAL_ID=\$\(call GET "\/api\/gds\/goals\?version=\$\{VERSION_ID\}"/,
50
+ assert.match(SH, /GOAL_ID=\$\(call GET "\/api\/bongos\/goals\?version=\$\{VERSION_ID\}"/,
51
51
  'it asks the API for this version-s goals');
52
52
  assert.match(SH, /is_maintenance/, 'and picks the maintenance goal by its flag');
53
53
  // Exactly one open maintenance goal exists per version (BV1.R18). Ambiguity here