Files
juzi/h5/test/regression.js
T

173 lines
11 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.
/* ═════ 巨子测肤 H5 — 评分引擎真值回归(Excel 三视图表 × SkinScore)═════
* 用法:node h5/test/regression.js
* fixture 重建:python3 h5/test/build_fixtures.py(Excel 或参考 JSON 变更后需重建)
*
* 断言目标:docs/皮肤检测测试结果_三视图_20260926.xlsx「三视图检测汇总」第 3/4 行
* - 六分类 P(I/M/Q/U/Y/AC)、S(J/N/R/V/Z/AD)、档位(K/O/S/W/AA/AE)
* - 三维度 P/S/档位(AG..AO)、下颌线角度(AP/AQ)、产品组别(AT)、精华线(AU)、均分(AV)
* - 6 分类文案(L/P/T/X/AB/AF)、综合文案(AW)、生活方式(AX) 逐字一致
* - 浮点容差 0.05;文案逐字无容差
*
* 产品推荐按2026-10-12用户确认规则覆盖旧Excel期望;评分真值和原始fixture不改写。
* 非授权的新差异登记在 KNOWN_DIFFS
* 并输出明细;未登记的新差异 → 退出码 1。登记项若转为通过(引擎已修复)会提示清理。
*/
'use strict';
var fs = require('fs');
var path = require('path');
var SkinScore = require(path.join(__dirname, '..', 'js', 'skin-score.js'));
/* ─── 已知差异登记:键 = 案例.列,值 = 差异原因(逐条说明)。当前为空:V2.2 审计后
引擎与两组真值全部一致;今后出现引擎/真值不一致时在此登记,未登记的新差异 → 退出码 1 ─── */
var KNOWN_DIFFS = {};
var RECOMMENDATION_EXPECT = {
case1: { essence: '25+束紧精华', oldSummary: '按三重判据(触发:最差项约束)推荐35+支撑精华',
newSummary: '按肤龄及边界规则(触发:肤龄基础分组)推荐25+束紧精华' },
case2: { essence: '50+重塑精华', oldSummary: '按三重判据(触发:年龄组档)推荐50+重塑精华',
newSummary: '按肤龄及边界规则(触发:肤龄基础分组)推荐50+重塑精华' }
};
/* ─── V2.3 §8.8 护理建议成品真值(五段,逐字) ─── */
var V23_SKIN = { case1: '混合偏干肌肤', case2: '干性肌肤' };
var V23_EXPECT = {
case1: [
{ tag: '肤质', text: '你的日常肤质为混合偏干肌肤:T区状态尚可,但两颊及面部外侧偏干,易出现紧绷与细纹。建议分区护理:两颊侧重保湿滋养,选用滋润型乳液/面霜,T区轻量保湿即可,避免全脸统一用厚重面霜。干燥会放大纹理粗糙感,建议先将两颊水合度提上来再做角质调理。' },
{ tag: '皱纹', text: '加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。' },
{ tag: '光滑', text: '加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。' },
{ tag: '精华', text: '建议搭配使用25+束紧精华:针对年轻肌肤活力减弱问题,激活肌肤动力、改善暗沉,提升弹润紧致表现,守护年轻光泽并延缓初始衰老迹象。' },
{ tag: '生活方式', text: '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳' }
],
case2: [
{ tag: '肤质', text: '你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。' },
{ tag: '皱纹', text: '加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。' },
{ tag: '轮廓', text: '强化轮廓紧致管理,重点针对面中、苹果肌与下颌线支持带区域,配合向上提升按摩手法,促进真皮层胶原再生与微循环。' },
{ tag: '精华', text: '建议搭配使用50+重塑精华:针对熟龄肌松弛与轮廓流失问题,深层重塑支撑结构,改善明显松弛与纹路,重建面部紧实与弹性。' },
{ tag: '生活方式', text: '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳' }
]
};
/* ─── 断言计数与输出 ─── */
var TOL = 0.05 + 1e-6; // 浮点容差 0.05(1e-6 仅抵二进制表示误差,如 98.65 vs 98.7)
var pass = 0, known = 0, fail = 0;
var failLines = [], knownLines = [];
function diffCtx(exp, act) { // 首个分歧点 ±30 字上下文
var i = 0, n = Math.min(exp.length, act.length);
while (i < n && exp[i] === act[i]) i++;
var lo = Math.max(0, i - 30);
return ' 分歧@' + i + ':\n 真值 …' + exp.slice(lo, i + 30) + '…\n 引擎 …' + act.slice(lo, i + 30) + '…';
}
function okNum(label, actual, expected) {
if (typeof actual === 'number' && typeof expected === 'number' && Math.abs(actual - expected) <= TOL) {
pass++; console.log(' ✓ ' + label + ' = ' + actual + (actual !== expected ? '(真值 ' + expected + ',容差内)' : ''));
} else {
fail++; failLines.push(label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
console.error(' ✗ ' + label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
}
}
function okStr(label, actual, expected, knownKey) {
if (actual === expected) { pass++; console.log(' ✓ ' + label); return; }
var reasons = knownKey ? KNOWN_DIFFS[knownKey] : null;
if (reasons) { // 已登记:已知差异,输出明细,不计失败
known++;
knownLines.push(knownKey);
console.log(' Δ ' + label + ' 已知差异(' + reasons.length + ' 条原因):');
for (var i = 0; i < reasons.length; i++) console.log(' 原因' + (i + 1) + ':' + reasons[i]);
console.log(diffCtx(String(expected), String(actual)));
} else {
fail++;
failLines.push(label + ':文案不一致\n' + diffCtx(String(expected), String(actual)));
console.error(' ✗ ' + label + ':文案不一致(未登记差异)\n' + diffCtx(String(expected), String(actual)));
}
}
/* ─── 列号映射:分类(文案列)与维度 ─── */
var CAT_COLS = { contour: 'I/J/K/L', wrinkles: 'M/N/O/P', smooth: 'Q/R/S/T', tone: 'U/V/W/X', sens: 'Y/Z/AA/AB', acne: 'AC/AD/AE/AF' };
var CAT_COPY_COL = { contour: 'L', wrinkles: 'P', smooth: 'T', tone: 'X', sens: 'AB', acne: 'AF' };
var DIM_COLS = { firm: 'AG/AH/AI', vita: 'AJ/AK/AL', stab: 'AM/AN/AO' };
/* ─── 主流程 ─── */
function loadJSON(name) {
return JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8'));
}
function main() {
var truth = loadJSON('truth.json');
var cases = [
{ tag: 'case1', fixture: loadJSON('case1.json'), truth: truth.case1 },
{ tag: 'case2', fixture: loadJSON('case2.json'), truth: truth.case2 }
];
for (var ci = 0; ci < cases.length; ci++) {
var tag = cases[ci].tag, fixture = cases[ci].fixture, tr = cases[ci].truth;
console.log('== ' + tag + '(realAge=' + tr.profile.realAge + ',skinType=' + tr.profile.skinType + ') ==');
// The old Excel assumes inverse worst-region wrinkles. Pin the new scalar
// input to its old P to isolate regressions elsewhere; do not edit fixtures.
// Actual overall-score behavior is independently tested in wrinkle-overall.js.
fixture.result.score_info.wrinkle_score = tr.cats.wrinkles.P;
// Pin dark-circle score to the existing pigment item to isolate old Excel
// assertions; independent tone-dark-circle tests exercise its new 20% weight.
fixture.result.score_info.dark_circle_score = fixture.result.score_info.melanin_score - (fixture.result.melasma?.value || 0) * 10 - (fixture.result.freckle?.value || 0) * 5;
if (tag === 'case2') {
// Authorized three-view change: left cheek has one closed comedone.
tr.cats.acne = { P: 99, S: 9.97, tier: '良好档', copy: '检出1处皮损(闭口粉刺1处)。建议维持清洁与控油节奏,注意作息与饮食。' };
tr.avgS = 9.53;
tr.summary = tr.summary.replace('六分类S均分9.54', '六分类S均分9.53');
}
var vm = SkinScore.buildViewModel(fixture.result, fixture.side, tr.profile);
// 下颌线角度 AP/AQ:fixture 保真校验(引擎消费角度后由 L 列逐字断言兜底)
okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left);
okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right);
okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge);
okNum(tag + '.F 综合得分(4:3:3)', vm.total, Math.round(tr.dims.firm.S * 4 + tr.dims.vita.S * 3 + tr.dims.stab.S * 3));
// 六分类 P/S/档位 + 文案逐字
for (var k in CAT_COLS) {
var cols = CAT_COLS[k].split('/'), c = vm.cats[k], t = tr.cats[k];
okNum(tag + '.' + cols[0] + ' ' + c.name + '·P', c.P, t.P);
okNum(tag + '.' + cols[1] + ' ' + c.name + '·S', c.S, t.S);
okStr(tag + '.' + cols[2] + ' ' + c.name + '·档位', c.level ? c.level + '档' : String(c.level), t.tier);
okStr(tag + '.' + CAT_COPY_COL[k] + ' ' + c.name + '·文案', c.text, t.copy, tag + '.' + CAT_COPY_COL[k]);
}
// 三维度 P/S/档位
for (var d in DIM_COLS) {
var dcols = DIM_COLS[d].split('/'), dd = vm.dims[d], dt = tr.dims[d];
okNum(tag + '.' + dcols[0] + ' ' + dd.name + '·P', dd.P, dt.P);
okNum(tag + '.' + dcols[1] + ' ' + dd.name + '·S', dd.S, dt.S);
okStr(tag + '.' + dcols[2] + ' ' + dd.name + '·档位', dd.level ? dd.level + '档' : String(dd.level), dt.tier);
}
// 组别/精华/均分/综合文案/生活方式
okStr(tag + '.AT 产品组别', String(vm.group), tr.group);
okStr(tag + '.AU 精华产品线(肤龄新版)', vm.essence.name, RECOMMENDATION_EXPECT[tag].essence);
okNum(tag + '.AV 六分类S均分', vm.avgS, tr.avgS);
okStr(tag + '.AW 综合文案', vm.summary, tr.summary.replace(RECOMMENDATION_EXPECT[tag].oldSummary,
RECOMMENDATION_EXPECT[tag].newSummary), tag + '.AW');
okStr(tag + '.AX 生活方式', vm.lifestyle, tr.lifestyle);
// V2.3 §8 护理建议五段(真值:§8.8 样例;daily_skin_type 按样例取,与 summary 口径的肤质参数不同)
var vm23 = SkinScore.buildViewModel(fixture.result, fixture.side, { realAge: tr.profile.realAge, skinType: V23_SKIN[tag] });
okStr(tag + '.BA 护理建议·五段成品', JSON.stringify(vm23.careAdvice), JSON.stringify(V23_EXPECT[tag]));
}
// 登记项清理提示:已知差异键本轮全部命中为差异;若某键实际通过则提示移除登记
var knownHit = {}, i;
for (i = 0; i < knownLines.length; i++) knownHit[knownLines[i]] = 1;
for (var key in KNOWN_DIFFS) {
if (!knownHit[key]) console.log(' ◆ 提示:登记项 ' + key + ' 本轮通过(引擎或真值已变化),可从 KNOWN_DIFFS 移除');
}
console.log('----------------------------------------');
console.log('断言 ' + (pass + known + fail) + ' 条:通过 ' + pass + ',已知差异 ' + known + ',失败 ' + fail);
if (knownLines.length) console.log('已知差异明细:' + knownLines.join('、') + '(原因见上,待 skin-score.js 审计代理处理)');
if (failLines.length) {
console.error('失败明细:');
for (i = 0; i < failLines.length; i++) console.error(' ✗ ' + failLines[i]);
}
if (fail > 0) process.exit(1);
console.log(known > 0 ? '结论:全绿(仅剩已登记、已说明原因的已知差异)' : '结论:全绿');
}
main();