mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
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:
parent
52af8c0cec
commit
4d91e2daad
2 changed files with 156 additions and 0 deletions
|
|
@ -33,6 +33,7 @@
|
|||
"audit:complexity": "node scripts/audit-complexity.mjs",
|
||||
"audit:map": "node scripts/build-complexity-map.mjs",
|
||||
"audit:i18n-coverage": "node scripts/audit-i18n-coverage.mjs",
|
||||
"audit:port-drift": "node scripts/audit-port-drift.mjs",
|
||||
"generate:dockerfiles": "node scripts/generate-dockerfiles.mjs",
|
||||
"setup:env": "node scripts/generate-env.mjs",
|
||||
"setup:secrets": "node scripts/setup-secrets.mjs",
|
||||
|
|
|
|||
155
scripts/audit-port-drift.mjs
Normal file
155
scripts/audit-port-drift.mjs
Normal 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();
|
||||
Loading…
Add table
Add a link
Reference in a new issue