@bongos/core 1.22.2 → 1.23.1

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 (45) hide show
  1. package/.bongos-core.json +67 -47
  2. package/docs/copy-inventory.md +1 -1
  3. package/docs/copy-registry.json +1 -1
  4. package/docs/module-api-changelog.md +6 -0
  5. package/docs/page-readings.json +40 -40
  6. package/modules/hall-ui/public/deploy.js +3 -3
  7. package/modules/hall-ui/public/dom-utils.js +5 -1
  8. package/modules/hall-ui/public/genesis-home.js +1 -1
  9. package/modules/lifecycle/github-push.js +1 -2
  10. package/modules/npm-release/preview/supervisor.js +2 -1
  11. package/modules/platform-identity/project-stats.js +2 -1
  12. package/package-lock.json +2 -2
  13. package/package.json +1 -1
  14. package/release-notes.json +7 -0
  15. package/scripts/bongos/autobongos-run.js +2 -1
  16. package/scripts/bongos/autonomy-gauge.js +2 -1
  17. package/scripts/bongos/backfill-disciplines.js +1 -1
  18. package/scripts/bongos/backfill-sessions.js +1 -1
  19. package/scripts/bongos/backup.js +2 -1
  20. package/scripts/bongos/dev.js +1 -1
  21. package/scripts/bongos/do-api.js +2 -1
  22. package/scripts/bongos/fitness.js +5 -6
  23. package/scripts/bongos/local-preview.js +2 -1
  24. package/scripts/bongos/login.js +1 -1
  25. package/scripts/bongos/page-reader.js +2 -1
  26. package/scripts/bongos/probe-cache.js +2 -1
  27. package/scripts/bongos/provision-core-upgrade.js +2 -1
  28. package/scripts/bongos/provision.js +5 -4
  29. package/scripts/bongos/publish-integration-gate.js +2 -1
  30. package/scripts/bongos/render-api.js +2 -1
  31. package/scripts/bongos/sandbox-stage.js +2 -1
  32. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +1 -1
  33. package/scripts/bongos/seed-bv1-profiles-tasks.js +1 -1
  34. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +1 -1
  35. package/scripts/bongos/session-eval.js +2 -1
  36. package/scripts/bongos/setup.js +2 -1
  37. package/scripts/bongos/sleep-helper-guard.js +88 -0
  38. package/scripts/bongos/upgrade.js +2 -2
  39. package/scripts/gds/sleep-helper-guard.js +6 -0
  40. package/src/bongos/module-scope-map.js +1 -0
  41. package/src/module-api.js +6 -1
  42. package/src/sleep.js +24 -0
  43. package/tests/deploy_page_projects.mjs +2 -1
  44. package/tests/module_api.mjs +1 -0
  45. package/tests/sleep_helper.mjs +99 -0
@@ -523,7 +523,7 @@ const KERNEL_FILES = [
523
523
  'src/module-api.js', // the published doorway
524
524
  'src/modules.js', // the registry/resolver facade
525
525
  'src/module-seams.js', // the seam registry (ports/events/contributions)
526
- 'src/role-responsibilities.js', 'src/html-escape.js', // owner's role statements (task 1003732) + THE server HTML escaper (task 1004876) — both import nothing
526
+ 'src/role-responsibilities.js', 'src/html-escape.js', 'src/sleep.js', // owner's role statements (task 1003732) + THE server HTML escaper (task 1004876) + THE server sleep (task 1004893) — all import nothing
527
527
  'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
528
528
  'src/module-loader/manifest-schema.js', // the manifest validator
529
529
  'src/module-loader/semver.js', // coreVersion compatibility
@@ -1365,13 +1365,12 @@ const CHECKS = [
1365
1365
  require('./doc-control-claims.js').checkDocControlClaims, // Check 37 — task 1003418 / audit A14: a doc cannot describe an enforcement control that does not exist (derived from the docs, not a hand list)
1366
1366
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1367
1367
  require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
1368
- // Check 24 — task 1003166 / G02: no ops script hardcodes a version id. It landed
1369
- // sharing Check 23's line, deliberately, because fitness.js was ~500 lines over
1370
- // the excess-lines ratchet and a new check could not afford one (idea 1000810).
1371
- // Task 1003206 carved this file from 2,004 lines to ~1,350, so that toll is paid
1372
- // and the check gets its own line back.
1368
+ // Check 24 — task 1003166 / G02: no ops script hardcodes a version id. It once shared
1369
+ // Check 23's line because fitness.js was over the excess-lines ratchet (idea 1000810);
1370
+ // task 1003206 carved this file from 2,004 lines to ~1,350, so it has its own line back.
1373
1371
  require('./version-literal-guard.js').checkNoVersionLiterals,
1374
1372
  require('./escape-helper-guard.js').checkOneHtmlEscaper, // task 1004876: ONE HTML escaper per side (dom-utils.js / src/html-escape.js), and hall pages load dom-utils first
1373
+ require('./sleep-helper-guard.js').checkOneSleepHelper, // task 1004893: ONE sleep per side (dom-utils.js OTB.sleep / src/sleep.js)
1375
1374
  require('./dev-box-guard.js').checkNoDevBox, // task 1003899 / ADR 0346: the retired dev box stays out (its exemption list lives in that file)
1376
1375
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1377
1376
  require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
@@ -15,12 +15,13 @@
15
15
  // shipped. Set OTB_PREVIEW_PORT to override the default 3100.
16
16
 
17
17
  const lib = require('./local-preview-lib');
18
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
18
19
 
19
20
  async function pollHealthy(ms = 20_000) {
20
21
  const end = Date.now() + ms;
21
22
  while (Date.now() < end) {
22
23
  if (await lib.health()) return true;
23
- await new Promise((r) => setTimeout(r, 500));
24
+ await sleep(500);
24
25
  }
25
26
  return false;
26
27
  }
@@ -86,7 +86,7 @@ function prompt(q) {
86
86
  return new Promise((resolve) => rl.question(q, (a) => { rl.close(); resolve(String(a || '').trim()); }));
87
87
  }
88
88
 
89
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
89
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
90
90
 
91
91
  // Run the device flow and return one of:
92
92
  // { state: 'ok', token, builder } | { state: 'not_approved' } | { state: 'expired' }
@@ -57,6 +57,7 @@ const fs = require('node:fs');
57
57
  const path = require('node:path');
58
58
  const crypto = require('node:crypto');
59
59
  const { spawn, spawnSync } = require('node:child_process');
60
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
60
61
 
61
62
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
62
63
  const INVENTORY_REL = 'docs/page-inventory.json';
@@ -511,7 +512,7 @@ async function renderOne(browser, kit, plan, base) {
511
512
  try { await kit.runActions(page, plan.actions); } catch (e) { notes.push(`${plan.state}: an action failed (${String(e.message).split('\n')[0].slice(0, 120)}); read as it stood`); }
512
513
  // A body read on a response from a page that has since navigated away never
513
514
  // settles, so the wait for the data is bounded.
514
- await Promise.race([Promise.all(pending), new Promise((r) => setTimeout(r, 2000))]);
515
+ await Promise.race([Promise.all(pending), sleep(2000)]);
515
516
  const got = await page.evaluate(captureInPage);
516
517
  if (got.branding) collectStrings(got.branding, data);
517
518
  return { state: plan.state, strings: got.strings, notes, data };
@@ -38,6 +38,7 @@ const fs = require('node:fs');
38
38
  const os = require('node:os');
39
39
  const path = require('node:path');
40
40
  const crypto = require('node:crypto');
41
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
41
42
 
42
43
  // Default freshness. The probes this serves answer questions that change on a
43
44
  // deploy or a ship — minutes, not seconds — so 60s is generous for a session brief
@@ -156,7 +157,7 @@ const POLL_MS = 100;
156
157
  async function waitForRecord(file, ttlMs, now) {
157
158
  const deadline = Date.now() + WAIT_MS;
158
159
  while (Date.now() < deadline) {
159
- await new Promise((r) => setTimeout(r, POLL_MS));
160
+ await sleep(POLL_MS);
160
161
  const rec = readRecord(file);
161
162
  if (isFresh(rec, ttlMs, now())) return rec;
162
163
  // The holder is gone and left nothing: stop waiting and do the work ourselves.
@@ -39,6 +39,7 @@ const { ensurePinWriteKey, hasOwnAccount, pinKeyPath, pinSshCommand } = require(
39
39
  const { replayPinCommit } = require('./provision-pin-replay.js'); // GitHub ahead → pin on top (task 1004733)
40
40
  const { isValidSlug } = require('../../modules/provisioning/provisioning.js');
41
41
  const { RELEASE_NOTES_FILE, RELEASE_NOTES_MAX_BYTES, parseChangeRecords, notesBetween } = require('./release-notes.js');
42
+ const { sleep: sharedSleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
42
43
  const outcome = require('./upgrade-outcome.js'); // how far a failed move got, as a class (task 1004446)
43
44
 
44
45
  // The shapes whose box this runner can actually reach. Mirrors UPGRADE_SHAPES in the
@@ -359,7 +360,7 @@ async function coreUpgradeInstance(inst, deps, intent) {
359
360
  // single immediate read races it binding its port. A false "did not take" here is
360
361
  // never cosmetic — it is what the owner is shown after the one act they were told they
361
362
  // could finally perform.
362
- const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
363
+ const sleep = deps.sleep || sharedSleep;
363
364
  let served = null;
364
365
  for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
365
366
  const res = boxExec(versionCmd(inst), { allowFail: true });
@@ -57,6 +57,7 @@ const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.
57
57
  const { installedCoreVersion, runnerBanner } = require('./runner-drift.js');
58
58
  const { teardownInstance } = require('./provision-teardown.js');
59
59
  const { disconnectInstance } = require('./provision-disconnect.js');
60
+ const { sleep: sharedSleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
60
61
  const { coreUpgradeInstance, preflightCoreUpgrade, snapshotAllCoreVersions } = require('./provision-core-upgrade.js'); // the owner's deploy door, ADR 0293
61
62
  const { isByoHost, recordByoHost } = require('./provision-byo-host.js'); // stand up NOTHING for a byo-host row, ADR 0323
62
63
 
@@ -515,7 +516,7 @@ async function provisionInstance(inst, deps) {
515
516
  // budget on an instance that may simply be slow); the liveness overlay is
516
517
  // what tells the owner the truth.
517
518
  log(' [healthz] verify the instance answers');
518
- const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
519
+ const sleep = deps.sleep || sharedSleep;
519
520
  let healthy = null; // null = not observed (dry-run)
520
521
  const tries = apply ? 10 : 1;
521
522
  for (let i = 0; i < tries; i++) {
@@ -869,7 +870,7 @@ async function settingsApplyInstance(inst, deps) {
869
870
  // 7's rule); a loopback PASS proves the process, not the address — the
870
871
  // domain-scoped sweep earns that claim.
871
872
  log(' [healthz] verify the instance answers after the restart');
872
- const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
873
+ const sleep = deps.sleep || sharedSleep;
873
874
  let healthy = false;
874
875
  for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
875
876
  const res = boxExec(healthzCmd(inst), { allowFail: true });
@@ -959,7 +960,7 @@ async function restartInstance(inst, deps) {
959
960
  // false "down" is never cosmetic here: this instance is ALREADY flagged
960
961
  // unreachable, and a hasty verdict would tell the owner their restart failed.
961
962
  log(' [healthz] verify the instance answers after the restart');
962
- const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
963
+ const sleep = deps.sleep || sharedSleep;
963
964
  let healthy = false;
964
965
  for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
965
966
  const res = boxExec(healthzCmd(inst), { allowFail: true });
@@ -1113,7 +1114,7 @@ async function placeManifestCreds(inst, mrow, deps) {
1113
1114
  // used to record a failure for a setup that had succeeded. The JSON answer lives
1114
1115
  // at /api/bongos/healthz — the root /healthz says a plain "ok" (task 1004577).
1115
1116
  if (apply) {
1116
- const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
1117
+ const sleep = deps.sleep || sharedSleep;
1117
1118
  let authConfigured = false;
1118
1119
  for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
1119
1120
  const out = boxExec(authHealthzCmd(inst), { allowFail: true });
@@ -38,6 +38,7 @@
38
38
  'use strict';
39
39
 
40
40
  const fs = require('node:fs');
41
+ const { sleep: sharedSleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
41
42
 
42
43
  const WORKFLOW_FILE = 'integration.yml';
43
44
  const DEFAULT_TIMEOUT_MIN = 12; // inside publish.yml's 20-minute job, leaving room to pack + publish
@@ -70,7 +71,7 @@ function runsUrl(repo, sha) {
70
71
  // touches the world is injectable for tests/publish_integration_gate.mjs.
71
72
  async function waitForIntegration({
72
73
  sha, repo, token, timeoutMs, intervalMs,
73
- fetchImpl = globalThis.fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now,
74
+ fetchImpl = globalThis.fetch, sleep = sharedSleep, now = Date.now,
74
75
  }) {
75
76
  const deadline = now() + timeoutMs;
76
77
  let last = { verdict: 'wait', run: null, why: 'no read yet' };
@@ -31,6 +31,7 @@
31
31
  'use strict';
32
32
 
33
33
  const { JsonHttpApi, makeApiError } = require('./http-api-client');
34
+ const { sleep: sharedSleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
34
35
 
35
36
  const RENDER_API_BASE = 'https://api.render.com/v1';
36
37
 
@@ -184,7 +185,7 @@ class RenderApi extends JsonHttpApi {
184
185
 
185
186
  // Poll until a new database is 'available'. `sleepFn` lets tests drive it with no clock.
186
187
  async settlePostgres(postgresId, { tries = 60, sleepMs = 5000, sleepFn } = {}) {
187
- const sleep = sleepFn || ((ms) => new Promise((r) => setTimeout(r, ms)));
188
+ const sleep = sleepFn || sharedSleep;
188
189
  for (let i = 0; i < tries; i++) {
189
190
  const pg = await this.getPostgres(postgresId);
190
191
  if (pg.status === 'available') return pg;
@@ -26,6 +26,7 @@ const os = require('node:os');
26
26
  const path = require('node:path');
27
27
  const readline = require('node:readline');
28
28
  const localPreview = require('./local-preview-lib');
29
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
29
30
 
30
31
  // The default health URL waitHealthy probes when a caller supplies none; the
31
32
  // local context always passes its own (localPreview.healthUrl).
@@ -198,7 +199,7 @@ async function waitHealthy(deps = {}) {
198
199
  } catch (e) {
199
200
  lastErr = e.message;
200
201
  }
201
- await new Promise((r) => setTimeout(r, deps.pollMs || HEALTH_POLL_MS));
202
+ await sleep(deps.pollMs || HEALTH_POLL_MS);
202
203
  }
203
204
  return { ok: false, reason: lastErr };
204
205
  }
@@ -25,7 +25,7 @@ const META_TASK = 1003059; // this planning session's meta task — moved i
25
25
  const DISCORD_BUGS_IDEA = 1000771; // subsumed by R11; marked promoted against it
26
26
  const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
27
27
 
28
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
29
29
  async function api(method, path, body, _tries = 0) {
30
30
  await sleep(400); // base throttle to stay under the per-IP rate limit
31
31
  const res = await fetch(`${BASE}/api/bongos${path}`, {
@@ -21,7 +21,7 @@ const GOAL = 1000045;
21
21
  const SRC = 'planning-session';
22
22
  const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
23
23
 
24
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
24
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
25
25
  async function api(method, path, body, _tries = 0) {
26
26
  await sleep(400); // base throttle to stay under the per-IP rate limit
27
27
  const res = await fetch(`${BASE}/api/bongos${path}`, {
@@ -32,7 +32,7 @@ const CRIT = {
32
32
 
33
33
  const SPEC = 'Spec: `docs/specs/bongos-v2-ideator-surface.md` (final). Design of record: idea 1001072 + `docs/design/ideator-surface-direction.md`.';
34
34
 
35
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
36
36
  async function api(method, path, body, _tries = 0) {
37
37
  await sleep(400); // base throttle to stay under the per-IP rate limit
38
38
  const res = await fetch(`${BASE}/api/bongos${path}`, {
@@ -27,6 +27,7 @@ const wanderingPrefs = require('../../modules/builder-settings/wandering-prefs')
27
27
  // fitness scans (modules/sessions/CLAUDE.md, "NOT in this module").
28
28
  const { scoreAccessibility } = require('../../modules/sessions/accessibility');
29
29
  const { resolveDefaultVersion } = require('./version-select.js');
30
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
30
31
 
31
32
  // ---- classification vocabularies -----------------------------------------
32
33
 
@@ -489,7 +490,7 @@ if (require.main === module) {
489
490
  candidates = (search.data && search.data.sessions) || [];
490
491
  if (candidates.length > 0 || attempt === maxAttempts) break;
491
492
  console.error(`search returned 0 candidates (attempt ${attempt}/${maxAttempts}) — retrying in 1500ms (possible stale read, #608)...`);
492
- await new Promise((r) => setTimeout(r, 1500));
493
+ await sleep(1500);
493
494
  }
494
495
  // An empty corpus is an ANOMALY by default (stale read / upload gap), NOT a
495
496
  // clean "nothing to do" — refuse to write a misleadingly-empty report so the
@@ -32,6 +32,7 @@ function statusOrigin() {
32
32
  return API_BASE;
33
33
  }
34
34
  const onboardingConfig = require('./onboarding-config');
35
+ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004893)
35
36
 
36
37
  const DRY_RUN = process.argv.includes('--dry-run');
37
38
  // --skip-keys mirrors the same flag on scripts/setup-builder.sh — when set,
@@ -586,7 +587,7 @@ async function main() {
586
587
  const deadline = Date.now() + expires_in * 1000;
587
588
  let pollInterval = interval * 1000;
588
589
  while (Date.now() < deadline) {
589
- await new Promise((r) => setTimeout(r, pollInterval));
590
+ await sleep(pollInterval);
590
591
  const poll = await (await cliClient()).auth.postAuthDevicePoll({ body: { device_code } });
591
592
  // Admission gate (SR-19 / task #1000): a first-time GitHub account that an
592
593
  // Archon hasn't approved is refused (HTTP 403, state:'not_approved'). Print a
@@ -0,0 +1,88 @@
1
+ // scripts/bongos/sleep-helper-guard.js — fitness check: ONE "wait N milliseconds"
2
+ // helper per side (task 1004893, goal 1000135).
3
+ //
4
+ // WHY. ~27 files each hand-rolled a Promise around setTimeout to wait. That is
5
+ // harmless on its own, but it is the copy-paste that task 1004876 found ~45 of for
6
+ // the HTML escaper, where some copies had quietly drifted. They were collapsed onto
7
+ // exactly two definitions:
8
+ //
9
+ // browser modules/hall-ui/public/dom-utils.js → window.OTB.sleep
10
+ // server src/sleep.js → sleep (modules reach it
11
+ // through src/module-api.js)
12
+ //
13
+ // This check keeps it that way. It looks for the SHAPE, not a name: a Promise whose
14
+ // resolver is handed straight to setTimeout is a sleep whatever the variable is
15
+ // called, and inline copies with no name at all were half of what the audit found. A BLOCKING sleep (Atomics.wait —
16
+ // merge-lock.js, rename-ledger-prose.js) has a different shape and is left alone:
17
+ // it freezes the thread on purpose, and the shared helper cannot do that.
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ const { REPO_ROOT } = require('./fitness-lib.js');
25
+
26
+ const ALLOWED = new Map([
27
+ ['modules/hall-ui/public/dom-utils.js', 'the shared BROWSER sleep (window.OTB.sleep)'],
28
+ ['src/sleep.js', 'the shared SERVER sleep (module-api sleep)'],
29
+ ['modules/ui-design/kit/render.js', 'its wait runs INSIDE the browser page via page.evaluate — the function is serialized, so it cannot reach either shared helper'],
30
+ ]);
31
+
32
+ // Where code lives. Tests are out of scope: a test injects its own fake sleep.
33
+ const SCAN_ROOTS = ['src', 'scripts', 'modules', 'server.js', 'bin'];
34
+ const SCAN_EXT = /\.(c|m)?js$|\.html$/;
35
+ const SKIP_DIR = new Set(['node_modules', '.git', 'tests', 'fixtures']);
36
+
37
+ // new Promise(<resolver> => [{] setTimeout(<same resolver>, … — arrow or function form.
38
+ const SLEEP_RE = /new\s+Promise\(\s*(?:function\s*)?\(?\s*(\w+)\s*\)?\s*(?:=>\s*)?\{?\s*setTimeout\(\s*(\w+)\s*,/g;
39
+
40
+ function walk(abs, out) {
41
+ let st;
42
+ try { st = fs.statSync(abs); } catch { return out; }
43
+ if (st.isFile()) { if (SCAN_EXT.test(abs)) out.push(abs); return out; }
44
+ for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
45
+ if (SKIP_DIR.has(ent.name)) continue;
46
+ walk(path.join(abs, ent.name), out);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ // findHandRolledSleeps(text) → 1-based line numbers holding a hand-rolled sleep.
52
+ function findHandRolledSleeps(text) {
53
+ const lines = String(text).split('\n');
54
+ const hits = [];
55
+ for (let i = 0; i < lines.length; i++) {
56
+ for (const m of lines[i].matchAll(SLEEP_RE)) {
57
+ if (m[1] === m[2]) { hits.push(i + 1); break; }
58
+ }
59
+ }
60
+ return hits;
61
+ }
62
+
63
+ function checkOneSleepHelper({ root = REPO_ROOT } = {}) {
64
+ const violations = [];
65
+ let scanned = 0;
66
+ for (const r of SCAN_ROOTS) {
67
+ for (const abs of walk(path.join(root, r), [])) {
68
+ scanned++;
69
+ const relPath = path.relative(root, abs).split(path.sep).join('/');
70
+ if (ALLOWED.has(relPath)) continue;
71
+ const hits = findHandRolledSleeps(fs.readFileSync(abs, 'utf8'));
72
+ if (hits.length) {
73
+ violations.push(`${relPath}:${hits[0]} — a hand-rolled sleep (new Promise → setTimeout). Use the shared one: `
74
+ + 'window.OTB.sleep in the browser, sleep from src/sleep.js (or api.sleep in a module) on the server.');
75
+ }
76
+ }
77
+ }
78
+ return {
79
+ name: 'one sleep helper per side (task 1004893)',
80
+ ok: violations.length === 0,
81
+ hardFail: violations.length > 0,
82
+ violations,
83
+ warnings: [],
84
+ note: `${scanned} code files hold no hand-rolled sleep (allowed: ${[...ALLOWED.keys()].join(', ')}).`,
85
+ };
86
+ }
87
+
88
+ module.exports = { checkOneSleepHelper, findHandRolledSleeps };
@@ -832,7 +832,7 @@ async function recordLedger({ fromVersion, toVersion, sourceCommit, note, reques
832
832
  // Connection-refused fails fast, so the early retries are cheap; the backoff caps at 2s.
833
833
  async function pollHealth(healthUrl, deps = {}) {
834
834
  const fetchImpl = deps.fetch || fetch;
835
- const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
835
+ const sleep = deps.sleep || require('../../src/sleep').sleep; // the one shared sleep (task 1004893)
836
836
  const now = deps.now || Date.now;
837
837
  const budgetMs = deps.healthBudgetMs != null ? deps.healthBudgetMs : 30_000;
838
838
  const deadline = now() + budgetMs;
@@ -882,7 +882,7 @@ function deriveVersionUrl(healthUrl) {
882
882
  // (a real failure) from "it wouldn't tell me" (unconfirmed), because only the first is proof.
883
883
  async function pollServedVersion(versionUrl, deps = {}) {
884
884
  const fetchImpl = deps.fetch || fetch;
885
- const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
885
+ const sleep = deps.sleep || require('../../src/sleep').sleep; // the one shared sleep (task 1004893)
886
886
  const now = deps.now || Date.now;
887
887
  // Default to the health budget when a caller (or a test) has already narrowed it, else 15s.
888
888
  const budgetMs = deps.versionBudgetMs != null
@@ -0,0 +1,6 @@
1
+ // scripts/gds/sleep-helper-guard.js — @bongos-forwarder (task 1004848). Generated by
2
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
3
+ // The real script is scripts/bongos/sleep-helper-guard.js. This forwarder keeps callers that still name
4
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
5
+ 'use strict';
6
+ require('../bongos/forwarder.js').forward(module, 'sleep-helper-guard.js');
@@ -465,6 +465,7 @@ const MODULE_GLOBS = {
465
465
  'scripts/bongos/seam-wiring-guard.js', 'scripts/bongos/skill-preflight.js',
466
466
  'scripts/bongos/test-path-guard.js', 'scripts/bongos/version-literal-guard.js', 'scripts/bongos/dev-box-guard.js',
467
467
  'scripts/bongos/escape-helper-guard.js', // task 1004876
468
+ 'scripts/bongos/sleep-helper-guard.js', // task 1004893
468
469
 
469
470
  // ADOPTED (task 1004111): the enforcement surfaces this module already IS.
470
471
  // install-git-hooks.js + scripts/bongos/hooks/ are the pre-push hook beside the
package/src/module-api.js CHANGED
@@ -61,6 +61,8 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
61
61
  // escapeHtml is THE server-side HTML escaper (task 1004876) — a module renders
62
62
  // user text into a page through this, never through a private copy.
63
63
  const { escapeHtml } = require('./html-escape');
64
+ // sleep is THE server-side "wait N milliseconds" (task 1004893); sleep.js requires nothing.
65
+ const { sleep } = require('./sleep');
64
66
 
65
67
  // ---------------------------------------------------------------------------
66
68
  // CORE_VERSION — the version of THIS published surface (semver).
@@ -79,7 +81,7 @@ const { escapeHtml } = require('./html-escape');
79
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
80
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
81
83
 
82
- const CORE_VERSION = '1.22.2'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.23.1'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
83
85
 
84
86
  // A namespaced logger so a module's log lines are attributable + consistent.
85
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -129,6 +131,9 @@ module.exports = {
129
131
  // escapeHtml(s) — the one server-side HTML escaper (& < > " '; null → ''). The
130
132
  // browser twin is window.OTB.escapeHtml; fitness fails on a third copy (task 1004876).
131
133
  escapeHtml,
134
+ // sleep(ms) — the one server-side "wait N milliseconds" (a Promise over setTimeout).
135
+ // The browser twin is window.OTB.sleep; fitness fails on a third copy (task 1004893).
136
+ sleep,
132
137
 
133
138
  // --- auth & rank (ADR 0016) — the gates the loader composes around a module's
134
139
  // routes; a module declares the rank it needs and never wires auth itself.
package/src/sleep.js ADDED
@@ -0,0 +1,24 @@
1
+ // src/sleep.js — THE server-side "wait N milliseconds" helper (task 1004893, goal 1000135).
2
+ //
3
+ // `await sleep(ms)` resolves to undefined after about `ms` milliseconds. It is a
4
+ // plain global setTimeout under the hood, so it keeps setTimeout's forgiving
5
+ // inputs (null, a numeric string, NaN all just mean "very soon") — which is why
6
+ // it is not node:timers/promises, whose setTimeout throws on null or a string.
7
+ //
8
+ // ONE copy per side, on purpose. The browser twin is window.OTB.sleep in
9
+ // modules/hall-ui/public/dom-utils.js. Modules reach this one through the doorway
10
+ // (`api.sleep`, src/module-api.js — ADR 0083); core files and scripts require it
11
+ // directly. scripts/bongos/sleep-helper-guard.js fails fitness on a third
12
+ // hand-rolled copy. A BLOCKING sleep (Atomics.wait — merge-lock.js,
13
+ // rename-ledger-prose.js) is a different tool and stays where it is.
14
+ //
15
+ // Zero dependencies by design: the CLI requires it, so it must cost nothing to load.
16
+
17
+ 'use strict';
18
+
19
+ /** Resolve after about `ms` milliseconds. */
20
+ function sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+
24
+ module.exports = { sleep };
@@ -95,7 +95,8 @@ function boot({ controlPlane = preview('1', 'cloudbongos'), controlPlaneStatus =
95
95
  };
96
96
  const windowObj = {
97
97
  BongosClient: { createClient: () => ({ request }) },
98
- OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast(m) { toasts.push(String(m)); } },
98
+ OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])), toast(m) { toasts.push(String(m)); },
99
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)) }, // dom-utils' OTB.sleep (task 1004893)
99
100
  // `search` is what decides which project the page is about. A stub without it is the
100
101
  // shape the page has to survive, and the default here keeps that path exercised.
101
102
  location: { assign() {}, search: scope ? `?project=${encodeURIComponent(scope)}` : '', ...(href ? { href } : {}) },
@@ -30,6 +30,7 @@ const PUBLISHED_SURFACE = [
30
30
  // published because the artist gate's one escape hatch is the owner's alone, and
31
31
  // a module cannot enforce that without being able to ask who the owner is.
32
32
  'getFoundingBuilder',
33
+ 'sleep', // task 1004893: THE server-side "wait N milliseconds" (1.23.0) — modules wait through it, never a private copy
33
34
  'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
34
35
  'grantedScopeCoversRepos', // the create-route credential preflight's scope read (task 1002909); kernel (auth-config.js), unlike db/github-repos which the module reaches via `pool` + its own client
35
36
  'accountExistenceReadRateLimit', // task 1003339 (ADR 0209): the shared per-IP budget both account-existence probes spend from — one instance, not a factory, so the budget is genuinely shared
@@ -0,0 +1,99 @@
1
+ // tests/sleep_helper.mjs — ONE "wait N milliseconds" helper per side (task 1004893, goal 1000135).
2
+ //
3
+ // ~27 files each hand-rolled `new Promise((r) => setTimeout(r, ms))`. They were
4
+ // collapsed onto two: window.OTB.sleep (modules/hall-ui/public/dom-utils.js) in
5
+ // the browser and sleep (src/sleep.js, re-exported by the module doorway as
6
+ // api.sleep) on the server. The two blocking Atomics.wait sleeps
7
+ // (modules/lifecycle/merge-lock.js, scripts/bongos/rename-ledger-prose.js) are a
8
+ // different thing — they freeze the thread on purpose — and stay local. These
9
+ // tests pin:
10
+ // 1. the server sleep waits about as long as asked, and resolves to undefined;
11
+ // 2. the module doorway hands modules the very same function;
12
+ // 3. the browser twin exists on window.OTB and waits too;
13
+ // 4. the fitness check goes red on a new hand-rolled copy and stays green on
14
+ // the repo, the two shared helpers and a blocking sleep.
15
+
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import vm from 'node:vm';
22
+ import { createRequire } from 'node:module';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
28
+
29
+ const { sleep } = require('../src/sleep.js');
30
+ const guard = require('../scripts/bongos/sleep-helper-guard.js');
31
+
32
+ test('server sleep waits roughly the time asked and resolves to undefined', async () => {
33
+ const t0 = Date.now();
34
+ const v = await sleep(30);
35
+ assert.equal(v, undefined);
36
+ assert.ok(Date.now() - t0 >= 25, 'waited at least ~30ms');
37
+ });
38
+
39
+ test('server sleep keeps setTimeout\'s forgiving inputs (null, a numeric string) instead of throwing', async () => {
40
+ // node:timers/promises throws ERR_INVALID_ARG_TYPE on these; the hand-rolled copies never did.
41
+ await sleep(null);
42
+ await sleep('1');
43
+ });
44
+
45
+ test('the module doorway exports the shared server sleep', () => {
46
+ const api = require('../src/module-api.js');
47
+ assert.equal(api.sleep, sleep);
48
+ });
49
+
50
+ test('the browser twin is window.OTB.sleep and waits', async () => {
51
+ const sandbox = { window: {}, document: { querySelector: () => null }, setTimeout, Promise };
52
+ vm.createContext(sandbox);
53
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'dom-utils.js'), 'utf8'), sandbox, { filename: 'dom-utils.js' });
54
+ const OTB = sandbox.window.OTB;
55
+ assert.equal(typeof OTB.sleep, 'function');
56
+ const t0 = Date.now();
57
+ await OTB.sleep(20);
58
+ assert.ok(Date.now() - t0 >= 15);
59
+ });
60
+
61
+ test('fitness: the repo holds no hand-rolled sleep outside the two shared helpers', () => {
62
+ const r = guard.checkOneSleepHelper();
63
+ assert.equal(r.ok, true, r.violations.join('\n'));
64
+ });
65
+
66
+ test('fitness: finds every shape of a hand-rolled sleep', () => {
67
+ const shapes = [
68
+ 'const sleep = (ms) => new Promise((r) => setTimeout(r, ms));',
69
+ 'const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));',
70
+ ' await new Promise(resolve => setTimeout(resolve, 3_000));',
71
+ ' sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now,',
72
+ 'await Promise.race([a, new Promise((r) => setTimeout(r, 2000))]);',
73
+ 'function sleep(ms) { return new Promise(function (res) { setTimeout(res, ms); }); }',
74
+ ];
75
+ for (const s of shapes) assert.deepEqual(guard.findHandRolledSleeps(s), [1], s);
76
+ });
77
+
78
+ test('fitness: a blocking Atomics.wait sleep and a timer that does real work are not flagged', () => {
79
+ assert.deepEqual(guard.findHandRolledSleeps('function sleepSync(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }'), []);
80
+ assert.deepEqual(guard.findHandRolledSleeps('new Promise((r) => setTimeout(() => r(compute()), 10))'), []);
81
+ assert.deepEqual(guard.findHandRolledSleeps('const t = setTimeout(resolve, ms); t.unref();'), []);
82
+ });
83
+
84
+ test('fitness: goes red on a planted copy, green once it is gone', () => {
85
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 't1004893-'));
86
+ try {
87
+ fs.mkdirSync(path.join(tmp, 'scripts', 'bongos'), { recursive: true });
88
+ const f = path.join(tmp, 'scripts', 'bongos', 'x.js');
89
+ fs.writeFileSync(f, "const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\n");
90
+ let r = guard.checkOneSleepHelper({ root: tmp });
91
+ assert.equal(r.ok, false);
92
+ assert.match(r.violations[0], /scripts\/bongos\/x\.js:1/);
93
+ fs.writeFileSync(f, "const { sleep } = require('../../src/sleep');\n");
94
+ r = guard.checkOneSleepHelper({ root: tmp });
95
+ assert.equal(r.ok, true, r.violations.join('\n'));
96
+ } finally {
97
+ fs.rmSync(tmp, { recursive: true, force: true });
98
+ }
99
+ });