Files
tilbudgivern/backend/scripts/run-carpenter-quote-benchmark.js
T

183 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { runCarpenterQuoteBenchmark } = require('../src/benchmarks/carpenterQuoteBenchmarkRunner');
const METRIC_LABELS = {
packageScore: 'Package match',
materialsScore: 'Materials',
hoursScore: 'Labor hours',
priceScore: 'Pricing',
keywordScore: 'Keywords',
historyScore: 'History'
};
function formatRelativeDate(value) {
if (!value) {
return 'ikke registreret';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return 'ukendt';
}
const daysAgo = Math.max(0, Math.round((Date.now() - date.getTime()) / (1000 * 60 * 60 * 24)));
const suffix = daysAgo === 0 ? 'i dag' : `${daysAgo} dag${daysAgo === 1 ? '' : 'e'} siden`;
return `${date.toISOString()} (${suffix})`;
}
function formatDiff(value) {
const numeric = Number.isFinite(value) ? value : 0;
const prefix = numeric >= 0 ? '+' : '';
return `${prefix}${numeric.toFixed(1)}`;
}
function formatMetricLabel(metric) {
return METRIC_LABELS[metric] || metric;
}
function logCaseSummary(result) {
const { benchmarkCase, evaluation } = result;
const metadata = benchmarkCase.ordrestyringMetadata || {};
const orderStatus = benchmarkCase.orderCases?.[0]?.status || 'ukendt';
const materialCoverage = evaluation.materialCoverage || {};
const coveragePercent = typeof materialCoverage.coverage === 'number'
? `${Math.round(materialCoverage.coverage * 100)}%`
: '0%';
const laborDiff = evaluation.generated.laborHours - evaluation.reference.laborHours;
const priceDiff = evaluation.generated.subtotal - evaluation.reference.subtotal;
console.log('='.repeat(84));
console.log(`CASE ${benchmarkCase.id} :: ${benchmarkCase.title}`);
console.log(` Order status: ${orderStatus}`);
console.log(` Ordrestyring status: ${metadata.last_status || 'ikke registreret'}`);
console.log(` Last price sync: ${formatRelativeDate(metadata.last_price_list_synced_at)}`);
console.log(` Score: ${evaluation.overallScore}/100 (${evaluation.difficulty})`);
console.log(` Package: ${evaluation.generated.packageId || 'ingen'} -> ref ${evaluation.reference.packageId || 'ingen'}`);
console.log(` Labor hours: ${evaluation.generated.laborHours}h vs ${evaluation.reference.laborHours}h (${formatDiff(laborDiff)} fra budget)`);
console.log(` Subtotal: ${evaluation.generated.subtotal} vs ${evaluation.reference.subtotal} (${formatDiff(priceDiff)})`);
console.log(` Materials matched: ${materialCoverage.matched}/${materialCoverage.expected} (${coveragePercent} coverage)`);
console.log(' Score breakdown:');
Object.entries(evaluation.metrics || {}).forEach(([metric, value]) => {
console.log(` ${formatMetricLabel(metric)}: ${value}/100`);
});
console.log(` Easy: ${evaluation.easyParts.join('; ') || 'ingen tydelige styrker'}`);
console.log(` Hard: ${evaluation.hardParts.join('; ') || 'ingen tydelige svagheder'}`);
if (evaluation.missingKeywords?.length) {
console.log(` Missing keywords: ${evaluation.missingKeywords.join(', ')}`);
}
console.log('');
}
function logScoreComparison(report) {
const average = report.summary.averageScore;
console.log('SCORE COMPARISON');
console.log(' Case ID Score Δ vs avg Difficulty Title');
report.results.forEach(result => {
const { caseId, difficulty } = result.evaluation;
const delta = (result.evaluation.overallScore - average);
const deltaLabel = delta >= 0 ? `+${delta.toFixed(1)}` : delta.toFixed(1);
const paddedCase = caseId.padEnd(15);
const paddedScore = `${result.evaluation.overallScore}`.padEnd(7);
const paddedDelta = deltaLabel.padEnd(11);
console.log(` ${paddedCase} ${paddedScore} ${paddedDelta} ${difficulty.padEnd(10)} ${result.benchmarkCase.title}`);
});
console.log('');
}
function buildNextSteps(report) {
const suggestions = [];
const seen = new Set();
const addSuggestion = suggestion => {
if (!seen.has(suggestion)) {
seen.add(suggestion);
suggestions.push(suggestion);
}
};
const metricAdvice = {
packageScore: 'Sørg for at Smart Pakke matcher referencepakkerne og gør det tydeligt i anbefalingen.',
materialsScore: 'Forstærk materialeforslaget ved at matche historiens mængder og prissætning tættere.',
hoursScore: 'Kalibrer timebudgettet, så det følger Ordrestyrings timeværdier mere præcist.',
priceScore: 'Reducer subtotal-afvigelsen ved at justere prissatser og rabatter tættere på historikken.',
keywordScore: 'Få de krævede kundeord med i tilbudsteksten for bedre tekstlig relevans.',
historyScore: 'Gør historie-matcherne mere relevante (samme kunde, samme pakke).'
};
report.summary.gaps.forEach(gap => {
addSuggestion(metricAdvice[gap.metric] || `Styrk ${formatMetricLabel(gap.metric)} (${gap.averageMetric}/100).`);
});
const staleThresholdMs = 1000 * 60 * 60 * 24 * 14;
const staleCases = report.results
.filter(result => {
const metadata = result.benchmarkCase.ordrestyringMetadata || {};
if (!metadata.last_price_list_synced_at) {
return false;
}
const syncDate = new Date(metadata.last_price_list_synced_at);
return !Number.isNaN(syncDate.getTime()) && (Date.now() - syncDate.getTime() > staleThresholdMs);
})
.map(result => `${result.benchmarkCase.id} (${formatRelativeDate(result.benchmarkCase.ordrestyringMetadata.last_price_list_synced_at)})`);
if (staleCases.length) {
addSuggestion(`Opdater prislisten for case(r) ${staleCases.join(', ')} for at få friske budgetter.`);
}
const hardestCase = report.results.reduce((worst, next) => {
return next.evaluation.overallScore < worst.evaluation.overallScore ? next : worst;
}, report.results[0]);
if (hardestCase && hardestCase.evaluation.overallScore < 80) {
addSuggestion(`Dyk ned i ${hardestCase.benchmarkCase.id} (${hardestCase.benchmarkCase.title}) – svære elementer: ${hardestCase.evaluation.hardParts.join('; ') || 'ikke specificeret'}.`);
}
if (!suggestions.length) {
addSuggestion('Ingen tydelige gaps; fortsæt med at overvåge de nuværende benchmark-mål.');
}
return suggestions;
}
function logAggregatedSummary(report) {
console.log('='.repeat(84));
console.log('AGGREGATED SUMMARY');
console.log(` Average score: ${report.summary.averageScore}/100`);
if (report.summary.strengths.length) {
const strengths = report.summary.strengths.map(item => `${formatMetricLabel(item.metric)} (${item.averageMetric}/100)`);
console.log(` Strengths: ${strengths.join(', ')}`);
} else {
console.log(' Strengths: ingen metrics over benchmarkmålet endnu');
}
if (report.summary.gaps.length) {
const gaps = report.summary.gaps.map(item => `${formatMetricLabel(item.metric)} (${item.averageMetric}/100)`);
console.log(` Gaps: ${gaps.join(', ')}`);
} else {
console.log(' Gaps: ingen metrics under gap-tærsklen');
}
console.log('');
logScoreComparison(report);
const nextSteps = buildNextSteps(report);
console.log('NEXT IMPROVEMENT SUGGESTIONS');
nextSteps.forEach((suggestion, index) => {
console.log(` ${index + 1}. ${suggestion}`);
});
console.log('');
}
async function main() {
const report = await runCarpenterQuoteBenchmark();
console.log('='.repeat(84));
console.log('TILBUDSGIVEREN CARPENTER CONTINUOUS EVALUATION');
console.log('='.repeat(84));
console.log(`Generated: ${report.generatedAt}`);
console.log('');
report.results.forEach(result => logCaseSummary(result));
logAggregatedSummary(report);
}
main().catch(error => {
console.error('Benchmark failed:', error);
process.exitCode = 1;
});