@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.
- package/.bongos-core.json +67 -47
- package/docs/copy-inventory.md +1 -1
- package/docs/copy-registry.json +1 -1
- package/docs/module-api-changelog.md +6 -0
- package/docs/page-readings.json +40 -40
- package/modules/hall-ui/public/deploy.js +3 -3
- package/modules/hall-ui/public/dom-utils.js +5 -1
- package/modules/hall-ui/public/genesis-home.js +1 -1
- package/modules/lifecycle/github-push.js +1 -2
- package/modules/npm-release/preview/supervisor.js +2 -1
- package/modules/platform-identity/project-stats.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7 -0
- package/scripts/bongos/autobongos-run.js +2 -1
- package/scripts/bongos/autonomy-gauge.js +2 -1
- package/scripts/bongos/backfill-disciplines.js +1 -1
- package/scripts/bongos/backfill-sessions.js +1 -1
- package/scripts/bongos/backup.js +2 -1
- package/scripts/bongos/dev.js +1 -1
- package/scripts/bongos/do-api.js +2 -1
- package/scripts/bongos/fitness.js +5 -6
- package/scripts/bongos/local-preview.js +2 -1
- package/scripts/bongos/login.js +1 -1
- package/scripts/bongos/page-reader.js +2 -1
- package/scripts/bongos/probe-cache.js +2 -1
- package/scripts/bongos/provision-core-upgrade.js +2 -1
- package/scripts/bongos/provision.js +5 -4
- package/scripts/bongos/publish-integration-gate.js +2 -1
- package/scripts/bongos/render-api.js +2 -1
- package/scripts/bongos/sandbox-stage.js +2 -1
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +1 -1
- package/scripts/bongos/seed-bv1-profiles-tasks.js +1 -1
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +1 -1
- package/scripts/bongos/session-eval.js +2 -1
- package/scripts/bongos/setup.js +2 -1
- package/scripts/bongos/sleep-helper-guard.js +88 -0
- package/scripts/bongos/upgrade.js +2 -2
- package/scripts/gds/sleep-helper-guard.js +6 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +6 -1
- package/src/sleep.js +24 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/module_api.mjs +1 -0
- 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) —
|
|
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
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
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
|
|
24
|
+
await sleep(500);
|
|
24
25
|
}
|
|
25
26
|
return false;
|
|
26
27
|
}
|
package/scripts/bongos/login.js
CHANGED
|
@@ -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 = (
|
|
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),
|
|
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
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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 =
|
|
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 ||
|
|
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
|
|
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 = (
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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
|
package/scripts/bongos/setup.js
CHANGED
|
@@ -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
|
|
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 || (
|
|
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 || (
|
|
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.
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), toast(m) { toasts.push(String(m)); }
|
|
98
|
+
OTB: { escapeHtml: (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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 } : {}) },
|
package/tests/module_api.mjs
CHANGED
|
@@ -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
|
+
});
|