@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.
- package/.bongos-core.json +62 -37
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0314-module-protection-is-per-path-not-per-module.md +7 -2
- package/docs/adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md +96 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +62 -2
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +25 -25
- package/docs/copy-registry.json +29 -29
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/self-host.md +1 -1
- package/modules/hall-ui/public/ideas.css +25 -0
- package/modules/hall-ui/public/ideas.html +12 -0
- package/modules/hall-ui/public/ideas.js +115 -2
- package/modules/ideas/landing-preview.js +180 -0
- package/modules/ideas/routes/inbox.js +47 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +213 -0
- package/scripts/gds/fitness-ratchets.js +31 -0
- package/scripts/gds/provision-net.js +41 -0
- package/scripts/gds/provision.js +11 -3
- package/scripts/hall-preview/server.js +14 -1
- package/src/bongos/module-scope-map.js +43 -5
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +130 -0
- package/tests/idea_landing_preview.mjs +337 -0
- package/tests/idea_spark_hall.mjs +183 -0
- package/tests/module-scope-map.mjs +87 -3
- package/tests/provision_caddy_wiring.mjs +76 -2
- 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
|
};
|
package/scripts/gds/provision.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
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
|
|
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.
|
|
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
|
+
});
|