#!/usr/bin/env node
// @lifecycle canonical - Verifies action metadata matches implementation.
/**
* Action Inventory Verification
*
* Validates that action-metadata TypeScript definitions match the actual
* implementation in MCP tool handlers. Ensures metadata stays synchronized
* with code changes.
*/
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SRC_DIR = path.join(__dirname, '..', 'src');
const DIST_DIR = path.join(__dirname, '..', 'dist');
function extractSwitchCases(source, anchor) {
const anchorIndex = source.indexOf(anchor);
if (anchorIndex === -1) {
throw new Error(`Unable to locate anchor "${anchor}"`);
}
const sliced = source.slice(anchorIndex);
const switchMatch = sliced.match(/switch\s*\([^)]+\)\s*{([\s\S]*?)}/);
if (!switchMatch) {
throw new Error(`Unable to locate switch body after anchor "${anchor}"`);
}
const body = switchMatch[1];
const matches = body.match(/case\s+["']([^"']+)["']/g) || [];
return matches.map((match) => match.replace(/case\s+["']([^"']+)["'].*/, '$1'));
}
async function verifyPromptResource() {
// Import metadata from compiled TypeScript
const { promptResourceMetadata } = await import(
path.join(DIST_DIR, 'mcp', 'metadata', 'definitions', 'prompt-resource.js')
);
const filePath = path.join(
SRC_DIR,
'mcp',
'tools',
'resource-manager',
'prompt',
'prompt-resource-service.ts'
);
const source = await readFile(filePath, 'utf8');
const actionsInCode = new Set(extractSwitchCases(source, 'switch (action)'));
const actionsInMetadata = new Set(promptResourceMetadata.data.actions.map((action) => action.id));
const missing = [...actionsInCode].filter((id) => !actionsInMetadata.has(id));
if (missing.length > 0) {
throw new Error(`prompt resource metadata is missing actions: ${missing.join(', ')}`);
}
}
async function verifySystemControl() {
// Import metadata from compiled TypeScript
const { systemControlMetadata } = await import(
path.join(DIST_DIR, 'mcp', 'metadata', 'definitions', 'system-control.js')
);
const filePath = path.join(SRC_DIR, 'mcp', 'tools', 'system-control.ts');
const source = await readFile(filePath, 'utf8');
const actionsInCode = new Set(extractSwitchCases(source, 'switch (action)'));
const operationsInMetadata = new Set(
systemControlMetadata.data.operations.map((op) => op.id.split(':')[0])
);
const missing = [...actionsInCode].filter((id) => !operationsInMetadata.has(id));
if (missing.length > 0) {
throw new Error(`system_control metadata is missing action groups: ${missing.join(', ')}`);
}
}
async function verifyPromptEngine() {
// Import metadata from compiled TypeScript
const { promptEngineMetadata } = await import(
path.join(DIST_DIR, 'mcp', 'metadata', 'definitions', 'prompt-engine.js')
);
const filePath = path.join(SRC_DIR, 'shared', 'types', 'execution.ts');
const source = await readFile(filePath, 'utf8');
const interfaceMatch = source.match(/export interface McpToolRequest\s*{([\s\S]*?)}/);
if (!interfaceMatch) {
throw new Error('Unable to locate McpToolRequest interface');
}
// Match field names but exclude fields typed as `never` (blocked parameters)
const fieldRegex = /readonly\s+([a-zA-Z0-9_]+)\??:\s*([^;]+);/g;
const fields = new Set();
let match;
while ((match = fieldRegex.exec(interfaceMatch[1])) !== null) {
const fieldName = match[1];
const fieldType = match[2].trim();
// Skip fields typed as 'never' - these are intentionally blocked
if (fieldType !== 'never') {
fields.add(fieldName);
}
}
const parameterNames = new Set(promptEngineMetadata.data.parameters.map((param) => param.name));
const missing = [...fields].filter((name) => !parameterNames.has(name));
if (missing.length > 0) {
throw new Error(`prompt_engine metadata missing parameters: ${missing.join(', ')}`);
}
}
async function main() {
// Check if dist exists
try {
await readFile(path.join(DIST_DIR, 'index.js'), 'utf8');
} catch {
console.warn('⚠️ dist/ not found. Run `npm run build` first.');
console.log('✅ Skipping action inventory verification (no build)');
return;
}
// Check if individual definition files exist (they won't in bundled builds)
const testFile = path.join(DIST_DIR, 'mcp', 'metadata', 'definitions', 'prompt-resource.js');
try {
await readFile(testFile, 'utf8');
} catch {
console.warn('⚠️ Bundled build detected (individual definition files not available)');
console.log('✅ Skipping action inventory verification (bundled mode)');
return;
}
await Promise.all([verifyPromptResource(), verifySystemControl(), verifyPromptEngine()]);
console.log('✅ Action inventory verified');
}
main().catch((error) => {
console.error('❌ Action inventory verification failed');
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});