mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 17:41:09 +02:00
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>
155 lines
5 KiB
JavaScript
155 lines
5 KiB
JavaScript
#!/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();
|