/**
* Free-form prompt entry point for the Baseline pillar.
*
* npm run ask -- "How much revenue did ACME and Globex report in 2024?"
*
* The prompt is interpreted into a structured JobRequest (entities,
* metric, period) using simple keyword matching against the entities
* the mock retrieval layer knows about. The full Baseline pipeline
* runs against that JobRequest and writes a fresh JSON + HTML report
* under output/.
*/
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { runAnalysis } from '../src/orchestrator.js';
import type { JobRequest } from '../src/types.js';
import { renderReport } from './render-report.js';
import { renderFlow } from './render-flow.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTPUT_DIR = path.resolve(__dirname, '..', 'output');
/** Entities the mock retrieval layer can return raw payloads for. */
const KNOWN_ENTITIES: { id: string; aliases: string[] }[] = [
{ id: 'ACME', aliases: ['ACME Corp', 'Acme'] },
{ id: 'GLOBEX', aliases: ['Globex SE', 'Globex'] },
{ id: 'INITECH', aliases: ['Initech, Inc.', 'Initech'] },
];
/** Metric keywords → canonical metric key. */
const METRIC_KEYWORDS: { key: string; definition: string; keywords: string[] }[] = [
{
key: 'revenue',
definition: 'Total revenue from operations for the period',
keywords: ['revenue', 'sales', 'net revenues', 'total revenue', 'top line'],
},
];
function buildJobRequest(prompt: string): JobRequest {
const lower = prompt.toLowerCase();
const matchedEntities = KNOWN_ENTITIES.filter(e =>
[e.id, ...e.aliases].some(a => lower.includes(a.toLowerCase())),
);
const entities = matchedEntities.length > 0 ? matchedEntities : KNOWN_ENTITIES;
const matchedMetric =
METRIC_KEYWORDS.find(m => m.keywords.some(k => lower.includes(k))) ?? METRIC_KEYWORDS[0]!;
const yearMatch = prompt.match(/\b(19|20)\d{2}\b/);
const period = yearMatch ? `FY-${yearMatch[0]}` : 'FY-2024';
return {
analysisId: `ask-${Date.now().toString(36)}`,
question: prompt,
entities,
targetMetrics: [
{ key: matchedMetric.key, definition: matchedMetric.definition, unit: 'USD_MM' },
],
sources: ['mock'],
period,
seedMappings: [
{ sourceLabel: 'Net revenues', targetKey: 'revenue' },
{ sourceLabel: 'Total revenue', targetKey: 'revenue' },
],
};
}
async function main(): Promise<void> {
const prompt = process.argv.slice(2).join(' ').trim();
if (!prompt) {
// eslint-disable-next-line no-console
console.error('Usage: npm run ask -- "<your question>"');
process.exit(1);
}
const job = buildJobRequest(prompt);
// eslint-disable-next-line no-console
console.log('Prompt:', JSON.stringify(prompt));
// eslint-disable-next-line no-console
console.log('Interpreted JobRequest:');
// eslint-disable-next-line no-console
console.log(
` entities: ${job.entities.map(e => e.id).join(', ')}\n` +
` metric: ${job.targetMetrics[0]?.key}\n` +
` period: ${job.period}\n` +
` sources: ${job.sources.join(', ')}`,
);
const result = await runAnalysis(job);
await mkdir(OUTPUT_DIR, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const file = path.join(OUTPUT_DIR, `ask-${stamp}.json`);
const audit = { ...result, jobRequest: job };
await writeFile(file, JSON.stringify(audit, null, 2), 'utf8');
const htmlPath = await renderReport(file);
const flowPath = await renderFlow(file);
// eslint-disable-next-line no-console
console.log(`\nAudit JSON: ${path.relative(process.cwd(), file)}`);
// eslint-disable-next-line no-console
console.log(`HTML report: ${path.relative(process.cwd(), htmlPath)}`);
// eslint-disable-next-line no-console
console.log(`Flow page: ${path.relative(process.cwd(), flowPath)}`);
if (result.ok && result.finalHandoff) {
const recs = (result.finalHandoff.payload as { records?: { canonicalEntity: string; canonicalMetric: string; period: string; value: number | null; unit: string }[] }).records ?? [];
// eslint-disable-next-line no-console
console.log('\nAnswer:');
for (const r of recs) {
// eslint-disable-next-line no-console
console.log(
` ${r.canonicalEntity} ${r.canonicalMetric} ${r.period}: ${r.value === null ? '—' : r.value.toLocaleString()} ${r.unit}`,
);
}
} else if (result.failure) {
// eslint-disable-next-line no-console
console.log('\nRun did not complete:');
// eslint-disable-next-line no-console
console.log(` ${result.failure.category}: ${result.failure.reason}`);
process.exitCode = 1;
}
}
main().catch(err => {
// eslint-disable-next-line no-console
console.error('Unhandled error:', err);
process.exit(1);
});