183 lines
7.4 KiB
JavaScript
183 lines
7.4 KiB
JavaScript
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;
|
||
});
|