Files
novalon-website/src/lib/constants/metrics-basis.test.ts
T
zhangxiang 4dcc15ebd0 fix(metrics): 数字口径 basis 结构强制,删除详情页虚构佐证
复评 P0 收尾:把「每个数字带口径」从个别页自觉升级为结构性强制。
- metrics-basis.ts:METRIC_BASES / METRIC_BASIS_NOTES + resolveMetricBasis
  (缺失·非法·大小写错一律回落最弱 target)+ weakestBasis(一组数字取最弱者聚合)
  + FORBIDDEN_PROOF_PHRASES;MetricsBasisNote 收 basis prop
- 类型层 Product.metrics / DataProof / ServiceMetric / Solution.outcomes /
  DataMetric / about keyMetrics 全部加 basis?;9 处调用点改为 basis 驱动
- content-types.ts 新增共享 metricBasisField(select)展开进 9 处字段定义;
  trustSignals 有意跳过(已有自己的 source)。seed 显式声明 55 条 target
- 删除字面虚构佐证:详情页「每一项指标都源自真实客户案例」、「经过实战验证」×3、
  「来自真实客户的使用数据」、「经实际场景验证」;给此前完全裸奔的 3 个共享
  数字渲染器(DataProofSection / detail-trust / detail-product-value)补上角注
- metrics-basis.test 3→16 例:basis 解析正控制、weakestBasis 次序、seed 全量
  显式声明 + 覆盖证明(防空跑假绿)+ 禁任何条目自称 verified + 禁词全源码扫描
- 文档同源:DESIGN.md 立 The Declared-Basis Rule;PRODUCT.md 常驻禁令;
  CONTEXT.md 记决策并警示 admin 字段需重跑 seed 才激活

边界:CMS 字段定义存 ContentModel.fields(DB 内 JSON),basis 下拉须重跑 seed
才会出现在 admin 表单;本次 DB 与 seed 数据值零改动,渲染端对缺 basis 兜底 target。

Gates: type-check 0 · jest 1594/132 · contrast 7/7 · headings 10/10
lint 0e/128w(与 stash 基线逐行比对,零新增告警)
2026-09-20 11:50:58 +08:00

184 lines
6.9 KiB
TypeScript
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.
/**
* 数字口径机械防线(2026-09-19 critique P0 → 结构强制收尾):
* 1) 「目标口径,非既成结果」必须来自 METRICS_BASIS_NOTE 单一真源,不得散写字面量;
* 2) 每个呈现结果型数字的渲染源(含详情页共享数字组件)必须接入 MetricsBasisNote
* 3) 走共享数字样式渲染的指标必须声明 basistarget|team-history|verified),
* 且口径解析对缺失/非法值保守回落到最弱的 target;
* 4) 禁止再引入「数字源自真实客户案例 / 经过实战验证」式虚构佐证(本站零公开客户案例)。
* 扫描范围:src/app + src/components 的 .ts/.tsx(排除测试文件与 _archive 归档)。
*/
import { readdirSync, readFileSync, statSync } from 'fs';
import { join } from 'path';
import {
METRICS_BASIS_NOTE,
METRIC_BASES,
METRIC_BASIS_NOTES,
FORBIDDEN_PROOF_PHRASES,
resolveMetricBasis,
weakestBasis,
} from './metrics-basis';
import { PRODUCTS } from './products';
import { SOLUTIONS } from './solutions';
import { STANDALONE_PRODUCTS } from '../../../prisma/seeds/standalone-products';
const SOURCE_ROOTS = ['src/app', 'src/components'];
/** 呈现结果型数字、必须接入口径角注的页面/组件源文件。 */
const REQUIRED_ANNOTATED_SOURCES = [
'src/app/(marketing)/products/products-content-v3.tsx',
'src/app/(marketing)/solutions/solutions-content-v3.tsx',
'src/app/(marketing)/services/services-content-v3.tsx',
'src/app/(marketing)/home-content-v15.tsx',
// 详情页同样以大数字样式渲染 dataProofs,不得裸奔
'src/components/content/sections.tsx',
'src/components/detail/detail-trust-section.tsx',
'src/components/detail/detail-product-value.tsx',
];
function collectSourceFiles(dir: string, acc: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (entry === '_archive') continue;
const stat = statSync(full);
if (stat.isDirectory()) {
collectSourceFiles(full, acc);
} else if (/\.(ts|tsx)$/.test(entry) && !/\.test\.tsx?$/.test(entry)) {
acc.push(full);
}
}
return acc;
}
/** 把三处 seed 里的全部数字条目摊平,用于口径声明完整性断言。 */
function allSeedMetricBases(): Array<string | undefined> {
const bases: Array<string | undefined> = [];
for (const p of [...PRODUCTS, ...STANDALONE_PRODUCTS]) {
(p.metrics ?? []).forEach((m) => bases.push(m.basis));
(p.dataProofs ?? []).forEach((d) => bases.push(d.basis));
}
for (const s of SOLUTIONS) {
(s.outcomes ?? []).forEach((o) => bases.push(o.basis));
(s.dataProofs ?? []).forEach((d) => bases.push(d.basis));
}
return bases;
}
describe('结果型数字口径角注防线', () => {
it('METRICS_BASIS_NOTE 固定为「目标口径,非既成结果」', () => {
expect(METRICS_BASIS_NOTE).toBe('目标口径,非既成结果');
});
it('METRICS_BASIS_NOTE 等于最弱口径 target 的文案', () => {
expect(METRICS_BASIS_NOTE).toBe(METRIC_BASIS_NOTES.target);
});
it('生产源码不得硬编码口径角注字面量(必须引用单一真源)', () => {
const violations: string[] = [];
for (const root of SOURCE_ROOTS) {
for (const file of collectSourceFiles(root)) {
if (file.endsWith('lib/constants/metrics-basis.ts')) continue;
const lines = readFileSync(file, 'utf-8').split('\n');
lines.forEach((line, i) => {
if (line.includes(METRICS_BASIS_NOTE)) {
violations.push(`${file}:${i + 1}`);
}
});
}
}
expect(violations).toEqual([]);
});
it('结果型数字页面均接入 MetricsBasisNote', () => {
for (const file of REQUIRED_ANNOTATED_SOURCES) {
const src = readFileSync(file, 'utf-8');
expect({ file, used: src.includes('<MetricsBasisNote') }).toEqual({
file,
used: true,
});
}
});
});
describe('basis 口径解析', () => {
it('枚举固定为 target / team-history / verified', () => {
expect(METRIC_BASES).toEqual(['target', 'team-history', 'verified']);
});
it('三个口径均有文案,无空串', () => {
for (const basis of METRIC_BASES) {
expect(METRIC_BASIS_NOTES[basis].length).toBeGreaterThan(0);
}
});
it('合法口径原样解析(正控制,防匹配零的假绿)', () => {
expect(resolveMetricBasis('verified')).toBe('verified');
expect(resolveMetricBasis('team-history')).toBe('team-history');
expect(resolveMetricBasis('target')).toBe('target');
});
it('缺失/非法/大小写错误一律保守回落到 target', () => {
expect(resolveMetricBasis(undefined)).toBe('target');
expect(resolveMetricBasis(null)).toBe('target');
expect(resolveMetricBasis('VERIFIED')).toBe('target');
expect(resolveMetricBasis('已验证')).toBe('target');
expect(resolveMetricBasis('verified ')).toBe('target');
});
it('weakestBasis 取最弱口径:任一 target 存在即整体为 target', () => {
expect(weakestBasis(['verified', 'team-history'])).toBe('team-history');
expect(weakestBasis(['verified', 'target'])).toBe('target');
expect(weakestBasis(['verified', 'verified'])).toBe('verified');
});
it('weakestBasis 空数组与含未声明项均落到 target', () => {
expect(weakestBasis([])).toBe('target');
expect(weakestBasis(['verified', undefined])).toBe('target');
});
});
describe('seed 数字必须显式声明口径', () => {
it('存在可检出的数字条目(覆盖证明,防空跑)', () => {
expect(allSeedMetricBases().length).toBeGreaterThan(40);
});
it('每条 metrics / outcomes / dataProofs 均声明 basis', () => {
const missing = allSeedMetricBases().filter((b) => !b);
expect(missing).toEqual([]);
});
it('声明的 basis 全部合法', () => {
const invalid = allSeedMetricBases().filter(
(b) => !(METRIC_BASES as readonly string[]).includes(String(b)),
);
expect(invalid).toEqual([]);
});
it('当前无任何条目自称 verified(零编造:尚无客户实测数据)', () => {
const claimed = allSeedMetricBases().filter((b) => b === 'verified');
expect(claimed).toEqual([]);
});
});
describe('虚构佐证禁词防线', () => {
it('正控制:禁词表非空且每条均可被检出', () => {
expect(FORBIDDEN_PROOF_PHRASES.length).toBeGreaterThan(0);
for (const phrase of FORBIDDEN_PROOF_PHRASES) {
expect(`示例文案${phrase}结尾`).toContain(phrase);
}
});
it('生产源码不含「真实客户案例 / 实战验证」式虚构佐证', () => {
const violations: string[] = [];
for (const root of SOURCE_ROOTS) {
for (const file of collectSourceFiles(root)) {
const lines = readFileSync(file, 'utf-8').split('\n');
lines.forEach((line, i) => {
const hit = FORBIDDEN_PROOF_PHRASES.find((p) => line.includes(p));
if (hit) violations.push(`${file}:${i + 1} ${hit}`);
});
}
}
expect(violations).toEqual([]);
});
});