复评 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 基线逐行比对,零新增告警)
184 lines
6.9 KiB
TypeScript
184 lines
6.9 KiB
TypeScript
/**
|
||
* 数字口径机械防线(2026-09-19 critique P0 → 结构强制收尾):
|
||
* 1) 「目标口径,非既成结果」必须来自 METRICS_BASIS_NOTE 单一真源,不得散写字面量;
|
||
* 2) 每个呈现结果型数字的渲染源(含详情页共享数字组件)必须接入 MetricsBasisNote;
|
||
* 3) 走共享数字样式渲染的指标必须声明 basis(target|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([]);
|
||
});
|
||
});
|