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:
2026-09-20 11:50:58 +08:00
parent 84e01eb8e8
commit 4dcc15ebd0
27 changed files with 332 additions and 97 deletions
+122 -4
View File
@@ -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) 走共享数字样式渲染的指标必须声明 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 } 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([]);
});
});