@bongos/core 1.19.910 → 1.19.912

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 (35) hide show
  1. package/.bongos-core.json +62 -37
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0314-module-protection-is-per-path-not-per-module.md +7 -2
  8. package/docs/adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md +96 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +62 -2
  11. package/docs/api-reference.md +3 -2
  12. package/docs/copy-inventory.md +25 -25
  13. package/docs/copy-registry.json +29 -29
  14. package/docs/module-api-changelog.md +5 -1
  15. package/docs/recipes/self-host.md +1 -1
  16. package/modules/hall-ui/public/ideas.css +25 -0
  17. package/modules/hall-ui/public/ideas.html +12 -0
  18. package/modules/hall-ui/public/ideas.js +115 -2
  19. package/modules/ideas/landing-preview.js +180 -0
  20. package/modules/ideas/routes/inbox.js +47 -0
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/scripts/gds/api-alias-caller-scan.js +213 -0
  24. package/scripts/gds/fitness-ratchets.js +31 -0
  25. package/scripts/gds/provision-net.js +41 -0
  26. package/scripts/gds/provision.js +11 -3
  27. package/scripts/hall-preview/server.js +14 -1
  28. package/src/bongos/module-scope-map.js +43 -5
  29. package/src/module-api.js +1 -1
  30. package/tests/api_alias_caller_ratchet.mjs +130 -0
  31. package/tests/idea_landing_preview.mjs +337 -0
  32. package/tests/idea_spark_hall.mjs +183 -0
  33. package/tests/module-scope-map.mjs +87 -3
  34. package/tests/provision_caddy_wiring.mjs +76 -2
  35. package/tests/quarantine_canonical.mjs +32 -4
@@ -347,6 +347,15 @@ const METRIC_HINTS = {
347
347
  + 'Write Bongos instead. Run `node scripts/gds/gds-literal-scan.js` to see every counted file. '
348
348
  + 'Version ids (GDS-V##), docs/adr, docs/session-logs, docs/audits, limitations and migrations '
349
349
  + 'are excluded by construction — if your literal is history, it belongs in one of those.',
350
+ legacy_api_gds_callers:
351
+ ' — a NEW in-repo caller was written against the permanent `/api/gds` alias (task 1004064, '
352
+ + 'idea 1001030). Ask for `/api/bongos` instead: the alias exists for callers we CANNOT update '
353
+ + '(shipped Dev Box binaries, live dev boxes), not for code in this repo. Run '
354
+ + '`node scripts/gds/api-alias-caller-scan.js --files` to see every counted line. The alias '
355
+ + 'contract, the pinned OAuth callbacks, the uptime poller + smoke, the rename guardrails, '
356
+ + 'tests/, docs/, migrations/ and captured fixtures are excluded by construction — and so is '
357
+ + 'any line that also names /api/bongos or carries the words alias / legacy / permanent / forever, '
358
+ + 'so if your line is ABOUT the alias rather than a call to it, say so on the line.',
350
359
  };
351
360
 
352
361
  function compareToBaselines(metrics, baselines) {
@@ -379,6 +388,27 @@ function gdsMetrics() {
379
388
  return { legacy_gds_literals: scanGdsLiterals().total };
380
389
  }
381
390
 
391
+ // legacy_api_gds_callers — the LOWERCASE half of the same rename (task 1004064, idea 1001030).
392
+ // `legacy_gds_literals` above is case-sensitive on the shouting acronym, so it read 1135 both
393
+ // before and after task 1003707 moved 665 in-repo callers off `/api/gds` — correct for what it
394
+ // measures, and exactly why a caller written against the alias tomorrow moves no number and reds
395
+ // no build. This counts that spelling instead. Detector + the reason for every exclusion:
396
+ // scripts/gds/api-alias-caller-scan.js.
397
+ //
398
+ // It FAILS CLOSED on a collapsed enumeration. A static scan breaks by finding nothing, and a
399
+ // green run looks identical to a working one — so if the scanner scanned no files at all, the
400
+ // metric is OMITTED with a reason on stderr rather than reported as a flattering 0 that would
401
+ // invite someone to ratchet the baseline down to it.
402
+ function apiAliasCallerMetrics() {
403
+ const { scanAliasCallers } = require('./api-alias-caller-scan.js');
404
+ const { total, scanned } = scanAliasCallers();
405
+ if (!scanned) {
406
+ process.stderr.write('fitness-ratchets: legacy_api_gds_callers NOT MEASURED — the caller scan enumerated ZERO files, which is a scan defect, not a clean tree\n');
407
+ return {};
408
+ }
409
+ return { legacy_api_gds_callers: total };
410
+ }
411
+
382
412
  // skill_listing_chars — the resident cost of the skill listing (every name +
383
413
  // description, both skill homes). Ratcheted rather than merely warned about
384
414
  // because this number has now been trimmed three times (tasks 1003548, 1003587,
@@ -411,6 +441,7 @@ function computeMetrics({ withDocs = true } = {}) {
411
441
  require_cycle_count: countCycles(requireGraph(contents)),
412
442
  console_call_count: consoleCallCount(contents),
413
443
  ...gdsMetrics(),
444
+ ...apiAliasCallerMetrics(),
414
445
  ...skillListingMetrics(),
415
446
  ...(withDocs ? docMetrics() : {}),
416
447
  };
@@ -270,6 +270,45 @@ function checkCaddyWiring(exec, opts = {}) {
270
270
  }
271
271
  return caddyWiringGap(res && res.stdout, { ...opts, caddyfile });
272
272
  }
273
+ // Can this plane give a child instance a front door AT ALL? (task 1004017)
274
+ //
275
+ // ADR 0303 asks whether a box's Caddy is wired right. This is the question BEFORE it: the
276
+ // self-host image installs no caddy binary and runs unprivileged, so step 6's snippet write
277
+ // and `systemctl reload caddy` cannot work there. The 0303 preflight is gated on inst.domain,
278
+ // so a DOMAINLESS child skipped it entirely and died on the write — a hard error on attempt
279
+ // one, not the pending-retry a refusal earns. Probed, not configured: a plane either answers
280
+ // `caddy version` or it does not. Same cheap-read posture as checkCaddyWiring — no privilege,
281
+ // no config, no running server.
282
+ // A MISSING BINARY is the only failure that proves there is no proxy. Under remote exec
283
+ // boxExec is `ssh <host> bash -s`, which throws on a transport blip too — and diagnosing a
284
+ // briefly-unreachable co-hosting box as "this platform has no proxy, provision elsewhere"
285
+ // would be a confident wrong answer, the thing this check exists to stop. ssh reports its
286
+ // OWN failures as 255 and otherwise propagates the remote command's status, so 127 still
287
+ // means "no caddy" across the hop. cmd.exe says 9009 and words it differently.
288
+ const NO_CADDY_BINARY = /command not found|not recognized as an internal|No such file or directory/i;
289
+ function ingressPlaneGap(exec, { caddyfile = CONFIG.caddyfile } = {}) {
290
+ try { exec('caddy version'); return null; } catch (e) {
291
+ const why = String((e && (e.stderr || e.message)) || e).trim().slice(0, 200);
292
+ const status = e && e.status;
293
+ if (status !== 127 && status !== 9009 && !NO_CADDY_BINARY.test(why)) {
294
+ return `could not confirm a reverse proxy on this plane: 'caddy version' failed (${why}). Provisioning writes a Caddy site snippet for every child instance, so it refuses rather than assume — re-run once the box answers.`;
295
+ }
296
+ return `this plane has no reverse proxy, so a child instance would have no front door: 'caddy version' failed (${why}). Provision from a host that runs Caddy — a plane with no proxy cannot serve a child, and ${caddyfile} is not something provisioning will create.`;
297
+ }
298
+ }
299
+ // The preflight around ingressPlaneGap: the narration its sibling preflights already carry,
300
+ // including the dry-run line without which the plan prints step 6's Caddy write as though it
301
+ // will work. Returns the refusal, or null to proceed. `dedicated` skips it (Caddy comes from
302
+ // cloud-init). Lives here rather than inline at the two call sites to keep provision.js under
303
+ // its 1,500-line ratchet.
304
+ function ingressPreflight({ exec, apply, log, shape, probe = ingressPlaneGap, before = 'writing any Caddy config' }) {
305
+ if (shape === 'dedicated') return null;
306
+ if (!apply) { log(` [preflight] would probe the plane for a reverse proxy before ${before}`); return null; }
307
+ const missing = probe(exec);
308
+ if (missing) return `Ingress preflight failed: ${missing}`;
309
+ log(' [preflight] the plane serves a reverse proxy ✓');
310
+ return null;
311
+ }
273
312
  function healthzCmd(inst) {
274
313
  return `curl -sS --max-time 5 http://127.0.0.1:${inst.port}/healthz`;
275
314
  }
@@ -322,6 +361,8 @@ module.exports = {
322
361
  healthzCmd,
323
362
  identityCmd,
324
363
  identityVerdict,
364
+ ingressPlaneGap,
365
+ ingressPreflight,
325
366
  shouldFederate,
326
367
  zoneForDomain,
327
368
  };
@@ -52,7 +52,7 @@ const crypto = require('node:crypto');
52
52
  const { CONFIG, MANIFEST_UA, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
53
53
  const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, coreCheckoutHasGit, dbCreateCmd, dbName, dbRoleCmd, dbRoleCmdShown, dbRoleGrantsCmd, ensureInstanceUserCmd, generateDbPassword, grantInstanceRepoReadCmd, instanceDbRole, instanceStateDir, instanceUser, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
54
54
  const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
55
- const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, shouldFederate, zoneForDomain } = require('./provision-net.js');
55
+ const { caddyBlock, caddySnippetPath, checkCaddyWiring, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, identityCmd, identityVerdict, ingressPlaneGap, ingressPreflight, shouldFederate, zoneForDomain } = require('./provision-net.js');
56
56
  const { resolveProcessManager } = require('./provision-process.js');
57
57
  const { capacityPreflight, statusCapacityLine } = require('./provision-capacity.js');
58
58
  const { installedCoreVersion, runnerBanner } = require('./runner-drift.js');
@@ -268,6 +268,14 @@ async function provisionInstance(inst, deps) {
268
268
  log(` [preflight] would verify the Cloudflare token can reach zone ${zoneForDomain(inst.domain)} before the address step`);
269
269
  }
270
270
  }
271
+ const room = await capacityPreflight(inst, deps); // box capacity (ADR 0285, task 1003927): an honest refusal BEFORE the status flip — never a silent oversubscribe
272
+ if (room.ok === false) return room;
273
+ // Ingress preflight (task 1004017, ADR 0315). Ordered capacity → ingress → wiring, all three
274
+ // before any mutation: ingress asks whether this plane has a proxy AT ALL, 0303 below whether
275
+ // that proxy is wired for THIS domain. Deliberately NOT domain-gated — a domainless child
276
+ // skipped 0303 entirely, reached step 6 and died on the snippet write.
277
+ const ingressGap = ingressPreflight({ exec: boxExec, apply, log, shape: inst.hosting_shape, probe: deps.ingressPlaneGap });
278
+ if (ingressGap) return { ok: false, error: ingressGap };
271
279
  // Caddy preflight (task 1002860, ADR 0303): a box whose base Caddyfile has no tls-check ask
272
280
  // can never certify this domain, so refuse before touching it. Injectable like the DNS check.
273
281
  const caddyCheck = deps.checkCaddyWiring || checkCaddyWiring;
@@ -275,8 +283,6 @@ async function provisionInstance(inst, deps) {
275
283
  const gap = caddyCheck(boxExec, { domain: inst.domain });
276
284
  if (gap) return { ok: false, error: `Caddy preflight failed: ${gap}` };
277
285
  }
278
- const room = await capacityPreflight(inst, deps); // box capacity (ADR 0285, task 1003927): an honest refusal BEFORE the status flip — never a silent oversubscribe
279
- if (room.ok === false) return room;
280
286
  if (apply) await provisioning.setInstanceStatus(db, inst.id, 'provisioning');
281
287
  const patch = {};
282
288
 
@@ -750,6 +756,8 @@ async function domainAttachInstance(inst, deps) {
750
756
  }
751
757
  log(` [preflight] DNS token can reach zone ${scope.zone} ✓`);
752
758
  }
759
+ const ingressGap = ingressPreflight({ exec: boxExec, apply, log, shape: inst.hosting_shape, probe: deps.ingressPlaneGap, before: 're-writing the Caddy snippet' });
760
+ if (ingressGap) return { ok: false, error: ingressGap }; // Ingress preflight (task 1004017, ADR 0315)
753
761
  const caddyCheck = deps.checkCaddyWiring || checkCaddyWiring; // Caddy preflight (task 1002860, ADR 0303)
754
762
  if (apply) {
755
763
  const gap = caddyCheck(boxExec, { domain: inst.domain });
@@ -231,7 +231,20 @@ app.use((req, res, next) => {
231
231
  return res.end(png);
232
232
  }
233
233
 
234
- if (req.method !== 'GET') {
234
+ // READ-SHAPED POSTS. A handful of routes are POSTs only because the INPUT is a
235
+ // draft rather than an id — they write nothing. Serving them the 501 below made
236
+ // their surfaces permanently un-demoable here: the page renders its degraded
237
+ // state forever, which is the "a page that only ever renders its empty state
238
+ // cannot be redesigned by looking" trap modules/hall-ui/CLAUDE.md names. They
239
+ // fall through to the ordinary fixture lookup instead.
240
+ //
241
+ // AN ALLOWLIST, and it stays short. Anything absent is still refused, so a real
242
+ // write cannot reach the harness by growing a comment — adding a member means
243
+ // asserting that route writes nothing, which is checkable at its handler.
244
+ const READ_SHAPED_POSTS = new Set([
245
+ 'inbox/preview-landing', // the landing verdict for a draft (task 1004083)
246
+ ]);
247
+ if (req.method !== 'GET' && !READ_SHAPED_POSTS.has(apiPath)) {
235
248
  return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
236
249
  }
237
250
  // Your own account wins over the checked-in snapshot; a me-scoped read that
@@ -456,12 +456,50 @@ const SHARED_GLOBS = Object.freeze(
456
456
  SHARED_GLOBS_RAW.filter((g) => !touchesOverlap([g], PROTECTED_GLOBS)),
457
457
  );
458
458
 
459
- // A glob set is protected iff it overlaps the imported PROTECTED_GLOBS — the one
460
- // source of truth (permission-path-check.js). Prefix-aware both directions
461
- // (path-match.touchesOverlap), so e.g. a module owning 'src/bongos/routes/inbox.js'
462
- // is protected because PROTECTED_GLOBS carries the 'src/bongos/routes/' subtree.
459
+ // A CLI ENTRYPOINT — a single executable script under scripts/, e.g.
460
+ // 'scripts/gds/setup.js'. Structural, not a list: it is a path test, so no roster
461
+ // is hand-maintained here and nothing has to be kept in step with PROTECTED_GLOBS.
462
+ // Directories ('scripts/deploy/', 'scripts/gds/hooks/') are territory, not
463
+ // commands, and are deliberately NOT matched.
464
+ function isCommandPath(glob) {
465
+ return /^scripts\/.*\.js$/.test(glob);
466
+ }
467
+
468
+ // Which of a module's globs make it protected? A glob qualifies iff it overlaps
469
+ // the imported PROTECTED_GLOBS — the one source of truth (permission-path-check.js),
470
+ // prefix-aware both directions (path-match.touchesOverlap), so e.g. a module owning
471
+ // 'src/bongos/routes/inbox.js' qualifies because PROTECTED_GLOBS carries the
472
+ // 'src/bongos/routes/' subtree.
473
+ function protectingGlobs(globs) {
474
+ return (globs || []).filter((g) => touchesOverlap([g], PROTECTED_GLOBS));
475
+ }
476
+
477
+ // A module is protected iff it owns protected TERRITORY — ADR 0314.
478
+ //
479
+ // WHY THIS IS NOT JUST touchesOverlap(globs, PROTECTED_GLOBS) ANY MORE.
480
+ // That asked "does this module touch the trust core", and three modules answered
481
+ // yes because of exactly one CLI they own that happens to sit on a protected
482
+ // path: onboarding (scripts/gds/setup.js), autonomy (conductor-dispatch.js),
483
+ // copy-desk (copy-inventory.js). Owning the command is not owning the authority
484
+ // — modules/government/ IS government, but setup.js merely lives near
485
+ // onboarding — so the whole module became Archon-only to scope a goal to on the
486
+ // strength of one file that is not its domain. ADR 0314 narrows the QUESTION to
487
+ // the one the other three walls already ask: is the territory being scoped
488
+ // itself protected?
489
+ //
490
+ // So a protected CLI entrypoint no longer protects its owning module; protected
491
+ // territory (a directory, or a non-command file like server.js or CLAUDE.md)
492
+ // still does. The derivation stays COMPUTED from PROTECTED_GLOBS — no hand-set
493
+ // flag, no override list (ADR 0086 §7 / SEC #863). It can only ever SHRINK the
494
+ // protected set relative to the old rule, never grow it, because it filters a
495
+ // subset: no module can become protected that was not protected before.
496
+ //
497
+ // THE WALL THAT MATTERS DOES NOT MOVE. This decides who may CREATE a goal whose
498
+ // territory includes a protected path. It does not decide who may WRITE one: a
499
+ // sub-Metic builder touching scripts/gds/setup.js is still refused by
500
+ // checkPermissionPaths (goal-scope-check.js wall 2), exactly as before.
463
501
  function isProtected(globs) {
464
- return touchesOverlap(globs || [], PROTECTED_GLOBS);
502
+ return protectingGlobs(globs).some((g) => !isCommandPath(g));
465
503
  }
466
504
 
467
505
  // The frozen exported roster: key → { globs:[...], protected:bool } (derived).
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.910'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.912'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,130 @@
1
+ // tests/api_alias_caller_ratchet.mjs — the lowercase `/api/gds` CALLER ratchet
2
+ // (task 1004064, from idea 1001030). Mirrors tests/fitness_gds_ratchet.mjs, its
3
+ // case-sensitive sibling.
4
+ //
5
+ // WHAT THIS FILE IS DEFENDING AGAINST. The brief names the trap exactly: "this is a static scan;
6
+ // it breaks by finding nothing, and a green run looks identical to a working one." Every case
7
+ // below is written so that a scanner which enumerates nothing, excludes everything, or counts
8
+ // nothing FAILS here rather than passing quietly. The aggregation cases inject their own file
9
+ // list and reader, so the real exclusion + counting path runs without writing into the repo —
10
+ // the unit lane runs test files in parallel, and a test that edited a tracked file would break
11
+ // whichever suite happened to be scanning the tree at that moment.
12
+ import assert from 'node:assert/strict';
13
+ import { test } from 'node:test';
14
+ import { readFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { fileURLToPath } from 'node:url';
17
+ import path from 'node:path';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const scan = require(path.join(ROOT, 'scripts', 'gds', 'api-alias-caller-scan.js'));
22
+
23
+ // A reader over an in-memory tree — no repo writes.
24
+ const fake = (tree) => ({ files: Object.keys(tree), readFile: (rel) => tree[rel] });
25
+
26
+ test('the counter COUNTS — a file that should count, does (the trap case)', () => {
27
+ const r = scan.scanAliasCallers(fake({
28
+ 'src/bongos/routes/thing.js': "await fetch(`${base}/api/gds/tasks/${id}`);\n",
29
+ }));
30
+ assert.equal(r.total, 1, 'a plain in-repo caller must be counted');
31
+ assert.deepEqual(Object.keys(r.byFile), ['src/bongos/routes/thing.js']);
32
+ assert.equal(r.scanned, 1, 'and the file must actually have been scanned');
33
+ });
34
+
35
+ test('a collapsed enumeration is visible as scanned=0, not as a flattering total of 0', () => {
36
+ const empty = scan.scanAliasCallers(fake({}));
37
+ assert.equal(empty.total, 0);
38
+ assert.equal(empty.scanned, 0, 'scanned is what distinguishes "nothing to find" from "nothing looked at"');
39
+
40
+ const allExcluded = scan.scanAliasCallers(fake({
41
+ 'tests/x.mjs': '/api/gds\n',
42
+ 'docs/x.md': '/api/gds\n',
43
+ }));
44
+ assert.equal(allExcluded.total, 0);
45
+ assert.equal(allExcluded.scanned, 0, 'an every-path-excluded run is also scanned=0 — the same signal');
46
+ });
47
+
48
+ test('the REAL scan enumerates the repo and lands on the committed baseline', () => {
49
+ const r = scan.scanAliasCallers();
50
+ assert.ok(r.scanned > 500, `the real tree must enumerate substantially (got ${r.scanned})`);
51
+ const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8'));
52
+ const floor = baselines.metrics.legacy_api_gds_callers;
53
+ assert.equal(typeof floor, 'number', 'the metric must have a baseline, or the ratchet only warns');
54
+ assert.ok(r.total <= floor, `caller count ${r.total} is ABOVE baseline ${floor} — a new /api/gds caller entered the tree`);
55
+ });
56
+
57
+ test('the exclusions are real and each one is reachable', () => {
58
+ // Rule numbers refer to the header of api-alias-caller-scan.js.
59
+ for (const p of [
60
+ 'src/bongos/api-prefix.js', // 1 the alias contract
61
+ 'src/bongos/routes/auth.js', // 2 the pinned OAuth callbacks
62
+ 'modules/provisioning/provisioning.js',
63
+ 'src/bongos/uptime-poller.js', // 3 the live proof
64
+ 'scripts/gds/smoke-gds.js',
65
+ 'scripts/gds/gds-literal-scan.js', // 4 the rename guardrails
66
+ 'scripts/gds/fitness-ratchets.js',
67
+ 'scripts/gds/api-alias-caller-scan.js',
68
+ 'tests/api_alias_exceptions.mjs', // 5 tests
69
+ 'modules/dev-box/app/test/thing.mjs',
70
+ 'docs/architecture.md', // 6 docs
71
+ 'migrations/033_audit_log.sql', // 7 migrations
72
+ 'scripts/hall-preview/fixtures/tasks.json', // 9 captured fixtures
73
+ 'node_modules/pkg/index.js', // vendored
74
+ ]) {
75
+ assert.equal(scan.isExcludedPath(p), true, `${p} must be excluded`);
76
+ }
77
+ // …and the exclusion is not a blanket that swallows the tree.
78
+ for (const p of [
79
+ 'src/bongos/serve-internal.js',
80
+ 'modules/lifecycle/abandon-leftovers.js',
81
+ 'clients/bongos-client/index.mjs', // a .mjs that is NOT a test — a blanket \.mjs$ would blind the ratchet
82
+ 'bin/bongos.js',
83
+ 'CLAUDE.md',
84
+ ]) {
85
+ assert.equal(scan.isExcludedPath(p), false, `${p} must NOT be excluded`);
86
+ }
87
+ });
88
+
89
+ test('a line ABOUT the alias is not a caller of it — and the rule is not so loose it excuses everything', () => {
90
+ // Excluded: names the canonical prefix too, or says what it is.
91
+ assert.equal(scan.isCountedLine('mount /api/bongos and /api/gds'), false);
92
+ assert.equal(scan.isCountedLine('// /api/gds is a permanent alias'), false);
93
+ assert.equal(scan.isCountedLine('// the legacy /api/gds mount'), false);
94
+ assert.equal(scan.isCountedLine('// /api/gds stays FOREVER'), false);
95
+ // Counted: a bare caller, with none of those tells.
96
+ assert.equal(scan.isCountedLine('const r = await fetch(`${base}/api/gds/me`);'), true);
97
+ assert.equal(scan.isCountedLine(' node scripts/gds/api.js GET /api/gds/tasks/1'), true);
98
+ // And a line with no alias at all is never counted, whatever else it says.
99
+ assert.equal(scan.isCountedLine('fetch(`${base}/api/bongos/me`)'), false);
100
+ assert.equal(scan.isCountedLine('a line about a legacy alias, forever'), false);
101
+ });
102
+
103
+ test('occurrences are counted, not lines — moving ONE of two callers on a line lowers the number', () => {
104
+ assert.equal(scan.countInText('f("/api/gds/a"); f("/api/gds/b");\n'), 2);
105
+ assert.equal(scan.countInText('f("/api/bongos/a"); f("/api/gds/b");\n'), 0, 'the canonical name on the line is the alias-commentary rule, by design');
106
+ assert.equal(scan.countInText('nothing here\n'), 0);
107
+ });
108
+
109
+ test('the ratchet is WIRED — computeMetrics reports the metric and compareToBaselines fails a rise', () => {
110
+ const ratchets = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
111
+ const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8')).metrics;
112
+
113
+ // A rise is a violation…
114
+ const worse = ratchets.compareToBaselines(
115
+ { legacy_api_gds_callers: baselines.legacy_api_gds_callers + 1 },
116
+ baselines,
117
+ );
118
+ assert.equal(worse.violations.length, 1, 'one more caller must RED the build');
119
+ assert.match(worse.violations[0], /legacy_api_gds_callers regressed/);
120
+ assert.match(worse.violations[0], /api-alias-caller-scan/, 'the hint must name the tool that lists the offenders');
121
+
122
+ // …a fall is a ratchet-down warning, not a failure.
123
+ const better = ratchets.compareToBaselines(
124
+ { legacy_api_gds_callers: baselines.legacy_api_gds_callers - 1 },
125
+ baselines,
126
+ );
127
+ assert.equal(better.violations.length, 0);
128
+ assert.equal(better.warnings.length, 1);
129
+ assert.match(better.warnings[0], /ratchet-down available/);
130
+ });