chore(services): add port-drift audit

Each services/*/CLAUDE.md declares `## Port: NNNN` — the authoritative
per-service port spec (docs/PORT_SCHEMA.md is explicitly partially
aspirational). This audit verifies:

  1. Declared port appears as a literal in the service's own source
     (catches: moved port in code but forgot to update CLAUDE.md).
  2. No two services claim the same port (catches: accidental
     collision when scaffolding new services).

Current state: ✓ 15 services, all declared ports found in code, zero
collisions (mana-auth/geocoding/stt/tts/image-gen/voice-bot/mail/
credits/user/subscriptions/analytics/events/news-ingester/ai/research).

Report-only; not a CI gate. Run with `pnpm run audit:port-drift`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-22 17:22:37 +02:00
parent 52af8c0cec
commit 4d91e2daad
2 changed files with 156 additions and 0 deletions

View file

@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* Audit port declarations: each services/*\/CLAUDE.md declares `## Port:
* NNNN`. Verify that the same port appears as a literal in the service's
* source code, and that no two services claim the same port.
*
* docs/PORT_SCHEMA.md is explicitly "partially aspirational" as of
* 2026-04-08, so the authoritative source is each service's own
* CLAUDE.md. This audit catches drift where a service moves its port in
* code but forgets to update its CLAUDE.md, or where two services
* accidentally claim the same number.
*
* Checks:
* 1. Parse `## Port: NNNN` from every `services/*\/CLAUDE.md`.
* 2. Grep the service directory (ignoring node_modules / dist / build /
* .turbo / __pycache__) for the literal port number.
* - Pass : declared port found in code.
* - Flag : not found either a doc typo or outdated doc.
* 3. Cross-check: build a port service map and flag collisions.
*
* Usage:
* node scripts/audit-port-drift.mjs
*/
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..');
const SERVICES_DIR = join(REPO_ROOT, 'services');
const SKIP_DIRS = new Set([
'node_modules',
'dist',
'build',
'.turbo',
'.svelte-kit',
'__pycache__',
'.venv',
'venv',
'.next',
'target', // rust
]);
function parseCLAUDEPort(mdPath) {
const src = readFileSync(mdPath, 'utf8');
const m = src.match(/^##\s+Port:\s+(\d{4,5})\s*$/m);
return m ? Number(m[1]) : null;
}
/** True if any tracked source file under `dir` contains `port` as a
* digit-boundary match (avoids matching 3050 inside 30500, etc.). */
function portAppearsIn(dir, port) {
const portStr = String(port);
const re = new RegExp(`\\b${portStr}\\b`);
function walk(d) {
for (const ent of readdirSync(d, { withFileTypes: true })) {
if (ent.name.startsWith('.') && ent.name !== '.env.example') continue;
if (SKIP_DIRS.has(ent.name)) continue;
const p = join(d, ent.name);
if (ent.isDirectory()) {
if (walk(p)) return true;
} else if (ent.isFile()) {
// Only scan source-ish files; skip binaries + lockfiles.
if (/\.(ts|tsx|js|mjs|cjs|go|py|env|env\.example|yaml|yml|toml|json|md)$/.test(ent.name)) {
try {
const src = readFileSync(p, 'utf8');
if (re.test(src)) return true;
} catch {
// Unreadable file — skip.
}
}
}
}
return false;
}
return walk(dir);
}
function audit() {
const entries = readdirSync(SERVICES_DIR, { withFileTypes: true }).filter((e) => e.isDirectory());
const declared = []; // { service, port, mdPath }
const missingCLAUDE = []; // service names
for (const ent of entries) {
const mdPath = join(SERVICES_DIR, ent.name, 'CLAUDE.md');
if (!existsSync(mdPath)) {
missingCLAUDE.push(ent.name);
continue;
}
const port = parseCLAUDEPort(mdPath);
if (port === null) continue; // service without a single-port concept (e.g. library service)
declared.push({ service: ent.name, port, mdPath });
}
const drifts = [];
for (const d of declared) {
const serviceDir = join(SERVICES_DIR, d.service);
const found = portAppearsIn(serviceDir, d.port);
if (!found) drifts.push(d);
}
const byPort = new Map();
for (const d of declared) {
if (!byPort.has(d.port)) byPort.set(d.port, []);
byPort.get(d.port).push(d.service);
}
const collisions = [...byPort.entries()].filter(([, list]) => list.length > 1);
console.log(`\n── Port drift audit ───────────────────────────────────\n`);
console.log(`Services with CLAUDE.md Port declaration: ${declared.length}`);
console.log(`Services without CLAUDE.md: ${missingCLAUDE.length}`);
console.log('');
if (drifts.length === 0 && collisions.length === 0) {
console.log(`✓ No drift: every declared port appears in its own service's source.`);
console.log(`✓ No collisions: all ${declared.length} port assignments unique.`);
}
if (drifts.length > 0) {
console.log(`✗ Drift: declared port not found in service source (${drifts.length}):\n`);
for (const d of drifts) {
console.log(
` ${d.service} declared :${d.port} — not found in ${d.mdPath.slice(REPO_ROOT.length + 1)}`
);
}
console.log('');
}
if (collisions.length > 0) {
console.log(`✗ Collisions: multiple services claim the same port (${collisions.length}):\n`);
for (const [port, list] of collisions) {
console.log(` :${port} ${list.join(', ')}`);
}
console.log('');
}
console.log(`Port map:\n`);
for (const d of declared.slice().sort((a, b) => a.port - b.port)) {
console.log(` :${d.port} ${d.service}`);
}
console.log('');
if (missingCLAUDE.length > 0) {
console.log(`Services without CLAUDE.md (no port declared):`);
for (const name of missingCLAUDE) console.log(` ${name}`);
console.log('');
}
// Report-only: never exit non-zero. This is diagnostic, not a gate.
}
audit();