@bongos/core 1.19.257 → 1.19.258

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.
@@ -0,0 +1,118 @@
1
+ // src/bongos/logger.js — the core structured logger (task 1003130, goal 1000076;
2
+ // docs/audits/ai-dev-redteam-baseline.md §5).
3
+ //
4
+ // The audit's logging finding: zero logging libraries, ~635 console.* calls in
5
+ // server paths, no request correlation, and NO redaction — a key echoed into a
6
+ // log line had no scrubber between it and journald. This is the fix for the
7
+ // CORE server path: one pino instance, request-correlation via
8
+ // AsyncLocalStorage, and redaction of the ADR 0022 secret-bearing header/field
9
+ // shapes. Modules migrate off console incrementally under the no-console
10
+ // ratchet (fitness-ratchets console_call_count); this module is what they
11
+ // migrate ONTO.
12
+ //
13
+ // Why pino: the Node structured-logging standard — JSON to stdout, low
14
+ // overhead, first-class redaction. It carries NO install scripts, so it is
15
+ // safe under the `.npmrc ignore-scripts=true` policy (ADR 0180 §2).
16
+ //
17
+ // The logger writes newline-delimited JSON to stdout; the process manager
18
+ // (systemd/journald) owns rotation and shipping. Level is LOG_LEVEL (default
19
+ // 'info'); set 'debug' to see the request-start lines.
20
+
21
+ 'use strict';
22
+
23
+ const { AsyncLocalStorage } = require('node:async_hooks');
24
+ const crypto = require('node:crypto');
25
+ const pino = require('pino');
26
+
27
+ // One request-scoped store holding { reqId }. A pino mixin reads it so EVERY
28
+ // line emitted while handling a request carries the id, without threading it
29
+ // through call signatures.
30
+ const requestContext = new AsyncLocalStorage();
31
+
32
+ // Redaction — the secret-bearing shapes from ADR 0022 that realistically reach
33
+ // a log line via a logged request/header/body object. pino's `redact` replaces
34
+ // the value with "[Redacted]"; paths are static (never built from input — the
35
+ // injection caveat in pino's docs). This is defense in depth behind the
36
+ // scrubber and the prevent hook, not the only guard.
37
+ const REDACT_PATHS = [
38
+ 'req.headers.authorization',
39
+ 'req.headers.cookie',
40
+ 'req.headers["x-gds-session"]',
41
+ 'req.headers["x-api-key"]',
42
+ 'headers.authorization',
43
+ 'headers.cookie',
44
+ // top-level field names (a bare path; pino's `*.x` matches one level DOWN, not the root)
45
+ 'authorization',
46
+ 'token',
47
+ 'password',
48
+ 'secret',
49
+ 'apiKey',
50
+ 'api_key',
51
+ 'sessionToken',
52
+ 'githubToken',
53
+ // …and the same names one level under any key
54
+ '*.authorization',
55
+ '*.token',
56
+ '*.password',
57
+ '*.secret',
58
+ '*.apiKey',
59
+ '*.api_key',
60
+ '*.sessionToken',
61
+ '*.githubToken',
62
+ ];
63
+
64
+ // Exported so a test can build a logger with the IDENTICAL config onto a
65
+ // capture stream — pino writes through sonic-boom straight to the fd, so the
66
+ // config, not the singleton's stdout, is what a hermetic test asserts against.
67
+ const pinoOptions = {
68
+ level: process.env.LOG_LEVEL || 'info',
69
+ // A leaked secret is a compromise; a redaction false-positive in a log is
70
+ // cosmetic — so redaction is broad, mirroring scrubSecrets' recall-first posture.
71
+ redact: { paths: REDACT_PATHS, censor: '[Redacted]' },
72
+ base: undefined, // drop pid/hostname noise; the process manager adds host context
73
+ timestamp: pino.stdTimeFunctions.isoTime,
74
+ mixin() {
75
+ const store = requestContext.getStore();
76
+ return store && store.reqId ? { reqId: store.reqId } : {};
77
+ },
78
+ };
79
+
80
+ const logger = pino(pinoOptions);
81
+
82
+ // Run `fn` inside a fresh request context stamped with `reqId` (a caller-supplied
83
+ // inbound id, or a fresh short hex). Everything logged inside — including the
84
+ // error middleware — carries the same reqId.
85
+ function runWithRequestId(reqId, fn) {
86
+ return requestContext.run({ reqId: reqId || crypto.randomBytes(8).toString('hex') }, fn);
87
+ }
88
+
89
+ function currentRequestId() {
90
+ const store = requestContext.getStore();
91
+ return store ? store.reqId : null;
92
+ }
93
+
94
+ // Express middleware: adopt an inbound correlation id (or mint one), expose it
95
+ // on req/res + the `x-request-id` response header, and run the rest of the
96
+ // request inside the AsyncLocalStorage context so every log line correlates.
97
+ function requestIdMiddleware(req, res, next) {
98
+ const inbound = req.headers['x-request-id'];
99
+ const reqId = (typeof inbound === 'string' && inbound.length > 0 && inbound.length <= 128)
100
+ ? inbound
101
+ : crypto.randomBytes(8).toString('hex');
102
+ req.reqId = reqId;
103
+ res.setHeader('x-request-id', reqId);
104
+ requestContext.run({ reqId }, () => next());
105
+ }
106
+
107
+ // requestContext stays module-private (nothing outside needs the raw ALS).
108
+ // pinoOptions/REDACT_PATHS/runWithRequestId/currentRequestId are the logging
109
+ // toolkit — used by tests today (via dynamic require, invisible to knip's static
110
+ // scan) and by future non-HTTP correlation callers.
111
+ module.exports = {
112
+ logger,
113
+ pinoOptions,
114
+ requestIdMiddleware,
115
+ runWithRequestId,
116
+ currentRequestId,
117
+ REDACT_PATHS,
118
+ };
@@ -40,6 +40,7 @@
40
40
  // view useful; that information is lost at the SQL layer.
41
41
 
42
42
  const db = require('../db');
43
+ const { logger } = require('../logger');
43
44
 
44
45
  const REDACTION_TOKENS = ['token', 'password', 'key', 'secret'];
45
46
  const REDACTED = '<redacted>';
@@ -205,7 +206,7 @@ function auditMiddleware(opts = {}) {
205
206
  } catch (err) {
206
207
  // redaction failures are themselves audit-failures: log + continue.
207
208
  // We still attach the finish handler so a row goes in WITH an empty body.
208
- console.error('[audit] redact_failed', err);
209
+ logger.error({ err }, '[audit] redact_failed');
209
210
  snapshotBody = { _redact_failed: true };
210
211
  }
211
212
  const ip = clientIp(req);
@@ -251,12 +252,12 @@ function auditMiddleware(opts = {}) {
251
252
  .catch((err) => {
252
253
  // SWALLOW. Audit failures must not surface to the caller and must
253
254
  // not affect future requests. Console-error is the only effect.
254
- console.error('[audit] insert_failed', {
255
+ logger.error({
255
256
  route,
256
257
  method,
257
258
  status,
258
259
  err: err && err.message ? err.message : String(err),
259
- });
260
+ }, '[audit] insert_failed');
260
261
  });
261
262
  });
262
263
 
@@ -23,6 +23,7 @@
23
23
  'use strict';
24
24
 
25
25
  const { ApiError, isApiError, envelope, statusForCode } = require('../api-errors');
26
+ const { logger } = require('../logger');
26
27
 
27
28
  // attachFail — decorate res with fail(). Mounted once, before the routers.
28
29
  function attachFail(req, res, next) {
@@ -83,10 +84,10 @@ function errorEnvelopeHandler(err, req, res, next) {
83
84
  return res.status(framework.status).json(envelope(framework.code));
84
85
  }
85
86
 
86
- // Anything else is an unexpected server fault. Log with a stable tag (so it's
87
- // greppable in prod logs) and answer a leak-free internal envelope — the raw
88
- // message/stack is never sent to the client.
89
- console.error('[gds] unhandled', req && req.method, req && req.originalUrl, err);
87
+ // Anything else is an unexpected server fault. Log structured (auto-correlated
88
+ // to the request via the logger's reqId mixin, redaction applied) and answer a
89
+ // leak-free internal envelope — the raw message/stack is never sent to the client.
90
+ logger.error({ method: req && req.method, url: req && req.originalUrl, err }, '[gds] unhandled');
90
91
  return res.status(500).json(envelope('internal_error'));
91
92
  }
92
93
 
@@ -311,6 +311,7 @@ const MODULE_GLOBS = {
311
311
  'package.json', 'package-lock.json',
312
312
  'scripts/gds/fitness.js', 'scripts/gds/docs-entropy.js',
313
313
  'scripts/gds/redteam-patrol.js', 'scripts/gds/audit-gate.js',
314
+ 'scripts/gds/fitness-ratchets.js', 'scripts/gds/control-manifest.js',
314
315
  ],
315
316
  };
316
317
 
@@ -39,6 +39,7 @@ const express = require('express');
39
39
  const { buildGdsRouter } = require('./routes');
40
40
  const { attachFail } = require('./middleware/error-envelope');
41
41
  const metrics = require('./metrics');
42
+ const { logger } = require('./logger');
42
43
  const gdsAuth = require('./auth');
43
44
  const { buildStampedHtml } = require('../asset-version');
44
45
  const { buildInfo } = require('../build-info');
@@ -289,7 +290,7 @@ function resolveOpenapiJson({ candidates = [], generate } = {}) {
289
290
  const out = generate();
290
291
  if (out) return out;
291
292
  } catch (e) {
292
- console.error('[docs] in-process OpenAPI generation failed:', e && e.message);
293
+ logger.error({ err: e && e.message }, '[docs] in-process OpenAPI generation failed');
293
294
  }
294
295
  }
295
296
  return '';
package/src/module-api.js CHANGED
@@ -941,8 +941,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
941
941
  // landed since 1.19.255 with no explicit bump. run 32208475455. (task 1002620)
942
942
  // 1.19.257 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
943
943
  // landed since 1.19.256 with no explicit bump. run 32209295358. (task 1002620)
944
+ // 1.19.258 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
945
+ // landed since 1.19.257 with no explicit bump. run 32284962866. (task 1002620)
944
946
  // ---------------------------------------------------------------------------
945
- const CORE_VERSION = '1.19.257'; // CI auto-patch carrier (ADR 0161); see changelog above
947
+ const CORE_VERSION = '1.19.258'; // CI auto-patch carrier (ADR 0161); see changelog above
946
948
 
947
949
  // A namespaced logger so a module's log lines are attributable + consistent.
948
950
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -49,6 +49,7 @@ const { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGloba
49
49
  const { isApiShapedPath, apiNotFoundBody } = require('./bongos/api-path-404');
50
50
  const moduleLoader = require('./module-loader/loader');
51
51
  const { isModuleEnabled } = require('./modules');
52
+ const { logger, requestIdMiddleware } = require('./bongos/logger');
52
53
 
53
54
  const PORT = Number(process.env.PORT) || 3000;
54
55
  const HOST = process.env.HOST || '127.0.0.1';
@@ -157,6 +158,12 @@ function buildPlatformApp() {
157
158
  // the OAuth callbacks the GDS auth router performs.
158
159
  app.set('trust proxy', 1);
159
160
 
161
+ // Request correlation FIRST (task 1003130): adopt/mint an x-request-id and run
162
+ // the rest of the request inside the AsyncLocalStorage context, so every
163
+ // structured log line — including the central error handler's — carries the
164
+ // same reqId. Mounted before any surface so nothing logs uncorrelated.
165
+ app.use(requestIdMiddleware);
166
+
160
167
  // The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
161
168
  mountInternalSurfaces(app);
162
169
 
@@ -263,7 +270,7 @@ if (require.main === module) {
263
270
  const app = buildPlatformApp();
264
271
  const httpServer = http.createServer(app);
265
272
  httpServer.listen(PORT, HOST, () => {
266
- console.log(`Cloud Bongos platform (GDS-only, no game; ADR 0062 R51) listening on ${HOST}:${PORT}`);
273
+ logger.info({ host: HOST, port: PORT }, 'Cloud Bongos platform (GDS-only, no game; ADR 0062 R51) listening');
267
274
  startBackgroundServices();
268
275
  });
269
276
 
@@ -288,7 +295,7 @@ if (require.main === module) {
288
295
  // up with a logged error strictly dominates dying. A rejection that leaves genuinely
289
296
  // corrupt state is not a shape this codebase produces — the DB work is transactional.
290
297
  process.on('unhandledRejection', (reason) => {
291
- console.error('[platform] unhandled promise rejection (staying up; this is a BUG — the owning await needs its own catch):', reason);
298
+ logger.error({ reason }, '[platform] unhandled promise rejection (staying up; this is a BUG — the owning await needs its own catch)');
292
299
  });
293
300
  }
294
301
 
@@ -140,7 +140,7 @@ test('a process-level unhandledRejection backstop is registered and does not exi
140
140
  assert.match(src, /process\.on\('unhandledRejection'/, 'the backstop must exist');
141
141
  const handler = src.slice(src.indexOf("process.on('unhandledRejection'"));
142
142
  const body = handler.slice(0, handler.indexOf('});') + 3);
143
- assert.match(body, /console\.error/, 'it must log');
143
+ assert.match(body, /console\.error|logger\.error/, 'it must log (console or the structured logger — task 1003130 moved this to logger.error)');
144
144
  assert.doesNotMatch(body, /process\.exit/,
145
145
  'it must NOT exit — one process serves every request, so staying up dominates dying');
146
146
  });
@@ -117,17 +117,34 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
117
117
  assert.ok(res.scheduledTasks > 0, 'at least one scheduled-task file materialized');
118
118
  const rt = join(dir, '.claude', 'scheduled-tasks');
119
119
  assert.ok(existsSync(rt), '.claude/scheduled-tasks created in the instance');
120
- // A prose/judgment body's core invocation is bongosified so a consumer-layout model can run it.
121
- const stale = readFileSync(join(rt, 'stale-claim-release', 'SKILL.md'), 'utf8');
122
- assert.ok(stale.includes('bongos exec scripts/gds/pulse.js'), 'body core-invocation rewritten to bongos exec');
123
- assert.ok(!stale.includes('node scripts/gds/pulse.js'), 'no bare relative node scripts/gds left in the body');
124
- // A deterministic routine's `.claude/...` companion-step command is NOT a core invocation → preserved.
125
- const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
126
- assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
127
- // Companion .js resolves its core require()s via the materialized _core-resolve.js sibling (task 2139 pattern).
128
- const check = readFileSync(join(rt, 'diagram-drift', 'check.js'), 'utf8');
129
- assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
130
- assert.ok(!check.includes("require('../../../scripts/gds/"), 'no bare ../../../scripts/gds require left in the companion');
120
+ // Every materialized routine's core invocation is bongosified — asserted over
121
+ // WHATEVER routines the tree carries (the neutral core carries redteam-weekly;
122
+ // the founder core also carries stale-claim-release/diagram-drift). No routine
123
+ // body may keep a bare `node scripts/gds/…` core invocation.
124
+ let sawBongosified = false;
125
+ for (const name of readdirSync(rt)) {
126
+ const skillPath = join(rt, name, 'SKILL.md');
127
+ if (!existsSync(skillPath)) continue;
128
+ const body = readFileSync(skillPath, 'utf8');
129
+ assert.ok(!/\bnode scripts\/gds\//.test(body), `${name}: bare relative node scripts/gds left in the body`);
130
+ if (body.includes('bongos exec scripts/gds/')) sawBongosified = true;
131
+ }
132
+ assert.ok(sawBongosified, 'at least one routine had its core invocation rewritten to bongos exec');
133
+ // Founder-specific fixtures — assert their finer behavior only when present, so
134
+ // this coverage stays live on the founder core without demanding those routines
135
+ // exist in the neutral core (which carries a different routine set).
136
+ const stalePath = join(rt, 'stale-claim-release', 'SKILL.md');
137
+ if (existsSync(stalePath)) {
138
+ assert.ok(readFileSync(stalePath, 'utf8').includes('bongos exec scripts/gds/pulse.js'), 'body core-invocation rewritten to bongos exec');
139
+ }
140
+ const ddCheck = join(rt, 'diagram-drift', 'check.js');
141
+ if (existsSync(ddCheck)) {
142
+ const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
143
+ assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
144
+ const check = readFileSync(ddCheck, 'utf8');
145
+ assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
146
+ assert.ok(!check.includes("require('../../../scripts/gds/"), 'no bare ../../../scripts/gds require left in the companion');
147
+ }
131
148
  rmSync(dir, { recursive: true, force: true });
132
149
  });
133
150
 
@@ -0,0 +1,60 @@
1
+ // tests/control_manifest.mjs — the assurance-of-assurance check (task 1003131,
2
+ // goal 1000076). Proves the manifest passes on the real tree AND actually
3
+ // CATCHES the two failure modes it exists to catch: a control file that
4
+ // vanishes, and a control that comes unwired. The check is itself a control —
5
+ // so it too must be provably able to fail.
6
+ import assert from 'node:assert/strict';
7
+ import { test } from 'node:test';
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const cm = require(path.join(ROOT, 'scripts', 'gds', 'control-manifest.js'));
15
+
16
+ test('the live tree passes: every goal-1000076 control is present and wired', () => {
17
+ const r = cm.checkControlsExist();
18
+ assert.equal(r.hardFail, false, `unexpected violations:\n ${r.violations.join('\n ')}`);
19
+ assert.ok(cm.CONTROLS.length >= 8, 'the manifest covers the shipped controls');
20
+ });
21
+
22
+ test('a vanished control file is caught', () => {
23
+ const controls = [{ label: 'x', file: '.claude/hooks/secret-gate.js' }];
24
+ const gone = cm.evaluate(controls, { exists: () => false, read: () => '' });
25
+ assert.equal(gone.length, 1);
26
+ assert.match(gone[0], /control file missing/);
27
+ });
28
+
29
+ test('a present-but-UNWIRED control is caught (the settings.json un-hook case)', () => {
30
+ const controls = [{
31
+ label: 'secret-gate hook', file: '.claude/hooks/secret-gate.js',
32
+ wiredIn: { file: '.claude/settings.json', needle: 'secret-gate.js' },
33
+ }];
34
+ // file exists, but the settings no longer reference it
35
+ const unwired = cm.evaluate(controls, {
36
+ exists: (f) => f === '.claude/hooks/secret-gate.js' || f === '.claude/settings.json',
37
+ read: () => '{ "hooks": {} }',
38
+ });
39
+ assert.equal(unwired.length, 1);
40
+ assert.match(unwired[0], /not wired in/);
41
+ });
42
+
43
+ test('a missing companion file (baseline/config) is caught even when the main file exists', () => {
44
+ const controls = [{
45
+ label: 'audit gate', file: 'scripts/gds/audit-gate.js',
46
+ alsoExists: ['config/audit-baseline.json'],
47
+ }];
48
+ const v = cm.evaluate(controls, { exists: (f) => f === 'scripts/gds/audit-gate.js', read: () => '' });
49
+ assert.equal(v.length, 1);
50
+ assert.match(v[0], /companion file missing/);
51
+ });
52
+
53
+ test('all-present + all-wired yields zero violations', () => {
54
+ const controls = [{
55
+ label: 'ok', file: 'a', alsoExists: ['b'],
56
+ wiredIn: { file: 'c', needle: 'NEEDLE' },
57
+ }];
58
+ const v = cm.evaluate(controls, { exists: () => true, read: () => 'has NEEDLE inside' });
59
+ assert.deepEqual(v, []);
60
+ });
@@ -0,0 +1,91 @@
1
+ // tests/fitness_ratchets.mjs — the quality-ratchet mechanics (task 1003129,
2
+ // goal 1000076). Hermetic: exercises the pure metric + compare functions on
3
+ // synthetic inputs; the repo-wide sweep runs via checkQualityRatchets inside
4
+ // tests/fitness.mjs, and the knip half is a CI-only step by design.
5
+ import assert from 'node:assert/strict';
6
+ import { test } from 'node:test';
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const rat = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
14
+
15
+ test('ratchet compare: worse fails, equal passes, better nudges, unknown warns', () => {
16
+ const base = { a: 10, b: 5 };
17
+ assert.equal(rat.compareToBaselines({ a: 10, b: 5 }, base).violations.length, 0, 'equal passes');
18
+ const worse = rat.compareToBaselines({ a: 11, b: 5 }, base);
19
+ assert.equal(worse.violations.length, 1);
20
+ assert.match(worse.violations[0], /a regressed: 11 > baseline 10/);
21
+ const better = rat.compareToBaselines({ a: 9, b: 5 }, base);
22
+ assert.equal(better.violations.length, 0);
23
+ assert.match(better.warnings[0], /ratchet-down available/);
24
+ const unknown = rat.compareToBaselines({ c: 1 }, base);
25
+ assert.equal(unknown.violations.length, 0, 'uninitialized metric warns, never fails');
26
+ assert.match(unknown.warnings[0], /no baseline for "c"/);
27
+ assert.equal(rat.compareToBaselines({ a: null }, base).warnings.length, 0, 'a skipped metric (null) is silent');
28
+ });
29
+
30
+ test('duplication: identical 30-line blocks in TWO files count; one file alone does not', () => {
31
+ const block = Array.from({ length: rat.WINDOW }, (_, i) => `const v${i} = compute(${i});`).join('\n');
32
+ const one = new Map([['a.js', block]]);
33
+ assert.equal(rat.duplicateWindowCount(one), 0, 'same-file windows are not cross-file duplication');
34
+ const two = new Map([['a.js', block], ['b.js', `// header\n${block}`]]);
35
+ assert.ok(rat.duplicateWindowCount(two) >= 1, 'the shared block registers');
36
+ const clean = new Map([['a.js', block], ['b.js', block.replaceAll('compute', 'derive')]]);
37
+ assert.equal(rat.duplicateWindowCount(clean), 0, 'a changed body is not a duplicate');
38
+ });
39
+
40
+ test('normalizeLines drops blanks and comment lines so cosmetic edits cannot dodge the hash', () => {
41
+ const lines = rat.normalizeLines('a();\n\n // comment\n b();\n/* block */\n * cont\n');
42
+ assert.deepEqual(lines, ['a();', 'b();']);
43
+ });
44
+
45
+ test('cycle counting: a 2-node loop is one SCC; a diamond is none; self-requires ignored', () => {
46
+ const loop = new Map([['a.js', ['b.js']], ['b.js', ['a.js']]]);
47
+ assert.equal(rat.countCycles(loop), 1);
48
+ const diamond = new Map([['a.js', ['b.js', 'c.js']], ['b.js', ['d.js']], ['c.js', ['d.js']], ['d.js', []]]);
49
+ assert.equal(rat.countCycles(diamond), 0);
50
+ const tri = new Map([['a.js', ['b.js']], ['b.js', ['c.js']], ['c.js', ['a.js']], ['x.js', []]]);
51
+ assert.equal(rat.countCycles(tri), 1, 'a 3-cycle is one component, not three');
52
+ });
53
+
54
+ test('requireGraph resolves ./x, ./x.js and ./x/index.js and skips unknown targets', () => {
55
+ const contents = new Map([
56
+ ['src/a.js', "const b = require('./b'); const i = require('./lib'); const gone = require('./nope');"],
57
+ ['src/b.js', ''],
58
+ ['src/lib/index.js', ''],
59
+ ]);
60
+ const edges = rat.requireGraph(contents);
61
+ assert.deepEqual(edges.get('src/a.js').sort(), ['src/b.js', 'src/lib/index.js']);
62
+ });
63
+
64
+ test('size metrics: count and excess ratchet independently', () => {
65
+ const files = ['big.js', 'small.js'];
66
+ const read = (f) => (f === 'big.js' ? 'x\n'.repeat(rat.OVERSIZE_LINES + 100) : 'x\n');
67
+ const m = rat.sizeMetrics(files, read);
68
+ assert.equal(m.oversized_file_count, 1);
69
+ assert.ok(m.excess_lines_total >= 100, `excess tracks growth over the threshold (got ${m.excess_lines_total})`);
70
+ });
71
+
72
+ test('console counting only sees server paths', () => {
73
+ assert.equal(rat.isServerPath('src/bongos/auth.js'), true);
74
+ assert.equal(rat.isServerPath('modules/lifecycle/db.js'), true);
75
+ assert.equal(rat.isServerPath('modules/hall-ui/public/builders.js'), false, 'browser surfaces excluded');
76
+ assert.equal(rat.isServerPath('scripts/gds/ship.js'), false, 'CLI stdout is legitimate UX');
77
+ assert.equal(rat.isServerPath('modules/security/tests/floor.mjs'), false);
78
+ const n = rat.consoleCallCount(new Map([
79
+ ['src/x.js', 'console.log(1); console.error(2);'],
80
+ ['scripts/gds/y.js', 'console.log(1);'],
81
+ ]));
82
+ assert.equal(n, 2);
83
+ });
84
+
85
+ test('the committed baseline file parses and covers every hermetic metric', () => {
86
+ const b = require(path.join(ROOT, 'config', 'fitness-baselines.json'));
87
+ for (const key of ['oversized_file_count', 'excess_lines_total', 'duplicate_window_count',
88
+ 'require_cycle_count', 'console_call_count', 'doc_stale_links', 'doc_broken_anchors', 'doc_duplicate_prose']) {
89
+ assert.ok(Number.isInteger(b.metrics[key]), `baseline missing ${key}`);
90
+ }
91
+ });
@@ -0,0 +1,89 @@
1
+ // tests/logger.mjs — the core structured logger (task 1003130, goal 1000076).
2
+ // Proves the three done-when properties without a live server: request-id
3
+ // correlation via AsyncLocalStorage, secret redaction on the logged shapes,
4
+ // and the middleware's adopt-or-mint + x-request-id echo. Hermetic — captures
5
+ // pino's stdout stream, no network.
6
+ import assert from 'node:assert/strict';
7
+ import { test } from 'node:test';
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
14
+ const log = require(path.join(ROOT, 'src', 'bongos', 'logger.js'));
15
+ const pino = require(path.join(ROOT, 'node_modules', 'pino'));
16
+ const { Writable } = require('node:stream');
17
+
18
+ // pino writes through sonic-boom directly to the fd, so we can't intercept the
19
+ // singleton's stdout. Instead build a logger with the IDENTICAL exported config
20
+ // onto a capture stream — same redact paths + same reqId mixin (the mixin closes
21
+ // over the module's ALS, so runWithRequestId still correlates).
22
+ function capture(fn) {
23
+ const lines = [];
24
+ const stream = new Writable({ write(chunk, _enc, cb) { lines.push(chunk.toString()); cb(); } });
25
+ const testLogger = pino(log.pinoOptions, stream);
26
+ fn(testLogger);
27
+ return lines.filter((l) => l.trim().startsWith('{')).map((l) => JSON.parse(l));
28
+ }
29
+
30
+ test('every line emitted inside runWithRequestId carries the same reqId', () => {
31
+ const out = capture((l) => {
32
+ log.runWithRequestId('req-abc', () => {
33
+ l.info('first');
34
+ l.warn('second');
35
+ assert.equal(log.currentRequestId(), 'req-abc');
36
+ });
37
+ l.info('outside'); // no context → no reqId
38
+ });
39
+ const inside = out.filter((l) => l.msg === 'first' || l.msg === 'second');
40
+ assert.equal(inside.length, 2);
41
+ assert.ok(inside.every((l) => l.reqId === 'req-abc'), 'correlated lines share the id');
42
+ const outside = out.find((l) => l.msg === 'outside');
43
+ assert.equal(outside.reqId, undefined, 'a line with no request context carries no reqId');
44
+ });
45
+
46
+ test('a secret in a logged header/field is redacted, not emitted', () => {
47
+ const out = capture((l) => {
48
+ l.error({
49
+ req: { headers: { authorization: 'Bearer sk-ant-verysecretvalue012345', cookie: 'gds_session=abc' } },
50
+ token: 'ghp_shouldnotappear0000000000000000000000',
51
+ note: 'this stays',
52
+ }, 'boom');
53
+ });
54
+ const line = out.find((l) => l.msg === 'boom');
55
+ const blob = JSON.stringify(line);
56
+ assert.ok(!blob.includes('sk-ant-verysecretvalue'), 'authorization header redacted');
57
+ assert.ok(!blob.includes('ghp_shouldnotappear'), 'token field redacted');
58
+ assert.ok(!blob.includes('gds_session=abc'), 'cookie redacted');
59
+ assert.ok(blob.includes('[Redacted]'), 'redaction marker present');
60
+ assert.ok(blob.includes('this stays'), 'non-secret fields survive');
61
+ });
62
+
63
+ test('REDACT_PATHS covers the ADR 0022 header/field shapes', () => {
64
+ for (const p of ['req.headers.authorization', 'req.headers.cookie', 'token', '*.token', '*.password']) {
65
+ assert.ok(log.REDACT_PATHS.includes(p), `expected redact path ${p}`);
66
+ }
67
+ });
68
+
69
+ test('requestIdMiddleware adopts a valid inbound id and echoes it; mints otherwise', () => {
70
+ const mk = (headers) => {
71
+ const req = { headers };
72
+ const res = { _h: {}, setHeader(k, v) { this._h[k.toLowerCase()] = v; } };
73
+ let ran = false;
74
+ log.requestIdMiddleware(req, res, () => { ran = true; });
75
+ return { req, res, ran };
76
+ };
77
+ const adopted = mk({ 'x-request-id': 'inbound-123' });
78
+ assert.equal(adopted.req.reqId, 'inbound-123');
79
+ assert.equal(adopted.res._h['x-request-id'], 'inbound-123');
80
+ assert.ok(adopted.ran);
81
+
82
+ const minted = mk({});
83
+ assert.match(minted.req.reqId, /^[0-9a-f]{16}$/, 'mints a 16-hex id when none supplied');
84
+ assert.equal(minted.res._h['x-request-id'], minted.req.reqId);
85
+
86
+ // an absurdly long inbound id is rejected in favor of a minted one (header-injection guard)
87
+ const huge = mk({ 'x-request-id': 'x'.repeat(500) });
88
+ assert.match(huge.req.reqId, /^[0-9a-f]{16}$/, 'over-long inbound id is not adopted');
89
+ });