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 基线逐行比对,零新增告警)
This commit is contained in:
@@ -1,12 +1,25 @@
|
||||
/**
|
||||
* 结果型数字口径机械防线(2026-09-19 critique P0):
|
||||
* 「目标口径,非既成结果」必须来自 METRICS_BASIS_NOTE 单一真源;
|
||||
* 且每个呈现结果型数字的营销页必须接入 MetricsBasisNote。
|
||||
* 数字口径机械防线(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 } from './metrics-basis';
|
||||
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'];
|
||||
|
||||
@@ -16,6 +29,10 @@ const REQUIRED_ANNOTATED_SOURCES = [
|
||||
'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[] {
|
||||
@@ -32,11 +49,29 @@ function collectSourceFiles(dir: string, acc: string[] = []): string[] {
|
||||
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) {
|
||||
@@ -63,3 +98,86 @@ describe('结果型数字口径角注防线', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user