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 10:38:23 +08:00
parent 82accb7cee
commit 3ed3afd819
27 changed files with 332 additions and 97 deletions
@@ -3,6 +3,7 @@
import { motion } from 'framer-motion';
import { CTAButton } from '@/components/ui/cta-button';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis, type MetricBasis } from '@/lib/constants/metrics-basis';
import { ContentUnavailableState } from '@/components/ui/content-unavailable';
import {
Trophy,
@@ -35,7 +36,7 @@ interface AboutData {
promiseText?: string;
coreValues?: { number: string; title: string; description: string }[];
milestones?: { year: string; title: string; description: string; highlight: string }[];
keyMetrics?: { value: string; label: string; sub: string }[];
keyMetrics?: { value: string; label: string; sub: string; basis?: MetricBasis }[];
certifications?: { name: string; description: string }[];
partners?: string[];
ctaTitle?: string;
@@ -147,7 +148,7 @@ function MetricsStrip({ data }: { data: AboutData }) {
</motion.div>
))}
</div>
<MetricsBasisNote className="mt-8 text-center lg:text-left" />
<MetricsBasisNote basis={weakestBasis(metrics.map((m) => m.basis))} className="mt-8 text-center lg:text-left" />
</div>
</section>
);
+3 -2
View File
@@ -14,6 +14,7 @@ import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { COMPANY_INFO, TRUST_SIGNALS, EARLY_ACCESS } from '@/lib/constants';
import { canonicalCtaLabel, CTA_LABELS } from '@/lib/constants/cta-labels';
import { canonicalTrustSource } from '@/lib/constants/company';
import { weakestBasis } from '@/lib/constants/metrics-basis';
import { useSiteConfig } from '@/lib/site-config';
import { useReducedMotion } from '@/hooks/use-reduced-motion';
import { type NewsItem } from '@/lib/constants/news';
@@ -296,7 +297,7 @@ function HeroStatsBand({ stats }: { stats?: Record<string, any>[] }) {
))}
</div>
</ScrollReveal>
<MetricsBasisNote className="mt-6" />
<MetricsBasisNote basis={weakestBasis(displayStats.map((s) => s.basis))} className="mt-6" />
</div>
</section>
);
@@ -558,7 +559,7 @@ function ServicesSection({ services, pageCopy }: { services?: Record<string, any
</div>
))}
</div>
<MetricsBasisNote />
<MetricsBasisNote basis={weakestBasis(((service as any).metrics as any[]).map((m) => m.basis))} />
</div>
)}
<div className="mt-auto pt-2">
+1 -1
View File
@@ -6,7 +6,7 @@ import ProductsContentV3 from './products-content-v3';
export const metadata: Metadata = {
title: `产品矩阵 - ${COMPANY_INFO.displayName}`,
description: `覆盖企业数字化全场景的产品矩阵,从数据智能到业务协同,每一款产品都经过实战验证。`,
description: `覆盖企业数字化核心场景的自研产品矩阵,从业务系统到数据智能,套装与专业产品线互为补充。`,
};
export const revalidate = 3600;
@@ -325,7 +325,7 @@ export default function ProductDetailContentV3({ product }: ProductDetailContent
{product.methodology && product.methodology.length > 0 && (
<MethodologyFramework
title="产品方法论"
subtitle="经过实战验证的产品设计理念与实施框架"
subtitle="我们做产品的设计理念与实施框架"
layers={product.methodology}
/>
)}
@@ -341,12 +341,13 @@ export default function ProductDetailContentV3({ product }: ProductDetailContent
<L3EmptyFallback caseStudies={product.caseStudies} certifications={product.certifications} dataProofs={product.dataProofs} />
{product.dataProofs && product.dataProofs.length > 0 && (
<DataProofSection
title="可衡量的成果"
subtitle="用数据说话,每一项指标都源自真实客户案例"
title="可量化的设计目标"
subtitle="以下指标为产品设计目标口径,用于约束自研交付标准;公司尚无已交付客户案例可提供实测数据。"
metrics={product.dataProofs.map((dp) => ({
value: dp.value,
label: dp.metric,
description: dp.description,
basis: dp.basis,
}))}
/>
)}
@@ -10,6 +10,7 @@ import { BentoItem } from '@/components/sections/bento-grid';
import { ArrowRight, ArrowUpRight, Package, Cpu, TrendingUp, Users, Settings, FileText } from 'lucide-react';
import type { Product } from '@/lib/constants/products';
import { CTA_LABELS } from '@/lib/constants/cta-labels';
import { weakestBasis } from '@/lib/constants/metrics-basis';
import { cn } from '@/lib/utils';
const EASE_SPRING = { type: 'spring', stiffness: 300, damping: 30 } as const;
@@ -146,7 +147,7 @@ function ProductCard({ product, size = 'small' }: { product: Product; size?: 'la
))}
</div>
{product.metrics && product.metrics.length > 0 && product.status !== '已发布' && (
<MetricsBasisNote className="-mt-2 mb-4" />
<MetricsBasisNote basis={weakestBasis(product.metrics.map((m) => m.basis))} className="-mt-2 mb-4" />
)}
<div className="flex flex-wrap gap-2">
@@ -319,7 +320,7 @@ function SuiteCombosSection({ products }: { products: Product[] }) {
推荐产品组合
</h2>
<p className="text-text-secondary text-lg mt-6 leading-relaxed">
经过实战验证的产品组合,发挥 1+1 > 2 的协同效应
套装与专业产品线按场景互补,覆盖从核心业务系统到数据分析的关键链路
</p>
</ScrollReveal>
@@ -178,7 +178,7 @@ export function StandaloneProductClient({ item }: StandaloneProductClientProps)
<h2 className="text-2xl font-bold text-ink">技术参数</h2>
</div>
<p className="text-text-secondary max-w-2xl">
关键性能指标与技术指标,经实际场景验证。
关键性能指标与技术规格,为产品设计目标口径,尚无第三方实测报告。
</p>
</motion.div>
@@ -6,12 +6,13 @@ import { StaticLink } from '@/components/ui/static-link';
import { CTAButton } from '@/components/ui/cta-button';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { CTA_LABELS, canonicalCtaLabel } from '@/lib/constants/cta-labels';
import type { Service } from '@/lib/constants/services';
import type { Service, ServiceMetric } from '@/lib/constants/services';
import { weakestBasis } from '@/lib/constants/metrics-basis';
const EASE_OUT = [0.22, 1, 0.36, 1] as const;
/** Fallback metrics keyed by service ID — used when CMS data does not provide metrics */
const SERVICE_UI_META: Record<string, { metrics: { value: string; label: string }[] }> = {
const SERVICE_UI_META: Record<string, { metrics: ServiceMetric[] }> = {
consulting: { metrics: [{ value: '10+', label: '人核心团队' }, { value: '6', label: '行业场景' }] },
software: { metrics: [{ value: '6', label: '款自研产品' }, { value: 'A+', label: '代码质量' }] },
data: { metrics: [{ value: '99.9%', label: '系统可用性' }, { value: '24/7', label: '运维支持' }] },
@@ -231,7 +232,9 @@ function ServicesGridSection({ services, pageCopy }: { services?: Service[]; pag
</div>
))}
</div>
{service.metrics.length > 0 && <MetricsBasisNote className="mb-6" />}
{service.metrics.length > 0 && (
<MetricsBasisNote basis={weakestBasis(service.metrics.map((m) => m.basis))} className="mb-6" />
)}
<div className="inline-flex items-center gap-2 text-sm font-semibold text-text-secondary group-hover:text-brand-ink transition-colors duration-300">
<span>了解详情</span>
@@ -6,6 +6,7 @@ import { StaticLink } from '@/components/ui/static-link';
import { CTAButton } from '@/components/ui/cta-button';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { CTA_LABELS, canonicalCtaLabel } from '@/lib/constants/cta-labels';
import { weakestBasis } from '@/lib/constants/metrics-basis';
import { type Solution } from '@/lib/constants/solutions';
import type { Product } from '@/lib/constants/products';
@@ -135,7 +136,7 @@ function SolutionCard({ solution, index, products, featured = false }: { solutio
))}
</div>
{solution.outcomes && solution.outcomes.length > 0 && (
<MetricsBasisNote className="-mt-2 mb-4" />
<MetricsBasisNote basis={weakestBasis(solution.outcomes.map((o) => o.basis))} className="-mt-2 mb-4" />
)}
<div className="pt-4 border-t border-border-primary">
+5
View File
@@ -5,6 +5,8 @@ import { ScrollReveal, StaggerReveal } from '@/components/ui/scroll-reveal';
import { EASE_OUT } from '@/components/ui/page-decoration';
import { CheckCircle2, ChevronDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis, type MetricBasis } from '@/lib/constants/metrics-basis';
import { useState } from 'react';
export interface MethodologyLayer {
@@ -318,6 +320,7 @@ export interface DataMetric {
description?: string;
trend?: string;
trendUp?: boolean;
basis?: MetricBasis;
}
export interface DataProofSectionProps {
@@ -328,6 +331,7 @@ export interface DataProofSectionProps {
export function DataProofSection({ title, subtitle, metrics }: DataProofSectionProps) {
if (!metrics || metrics.length === 0) return null;
const basis = weakestBasis(metrics.map((m) => m.basis));
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-primary">
@@ -375,6 +379,7 @@ export function DataProofSection({ title, subtitle, metrics }: DataProofSectionP
))}
</StaggerReveal>
</div>
<MetricsBasisNote basis={basis} className="mt-6" />
</div>
</section>
);
@@ -15,6 +15,8 @@ import {
type LucideIcon,
} from 'lucide-react';
import type { Product } from '@/lib/constants/products';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis } from '@/lib/constants/metrics-basis';
interface ProductValueSectionProps {
product: Product;
@@ -250,11 +252,11 @@ export function ProductValueSection({ product }: ProductValueSectionProps) {
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
效果数据
设计目标
</span>
</div>
<h3 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[0.95]">
用真实效果证明价值
以可量化的目标约束交付
</h3>
</div>
@@ -275,6 +277,10 @@ export function ProductValueSection({ product }: ProductValueSectionProps) {
);
})}
</div>
<MetricsBasisNote
basis={weakestBasis(product.dataProofs.map((proof) => proof.basis))}
className="mt-8"
/>
</motion.div>
)}
</div>
@@ -5,6 +5,8 @@ import { useRef, useEffect, useState } from 'react';
import { CaseStudyCard } from './detail-case-study';
import { CertificationList } from './detail-certification';
import type { CaseStudy, DataProof, Certification } from '@/lib/constants/products';
import { MetricsBasisNote } from '@/components/ui/metrics-basis-note';
import { weakestBasis } from '@/lib/constants/metrics-basis';
interface DetailTrustSectionProps {
caseStudies?: CaseStudy[];
@@ -84,13 +86,13 @@ export function DetailTrustSection({
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-px bg-brand" />
<span className="text-[13px] tracking-[0.12em] font-semibold text-brand-ink">
用真实效果证明价值
目标口径
</span>
</div>
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-black text-ink tracking-tight leading-[0.95]">
来自真实客户的使用数据,
以设计目标约束交付,
<br />
让效果可衡量、可验证
在首批共创中转为可核验数据
</h2>
</motion.div>
@@ -114,6 +116,10 @@ export function DetailTrustSection({
</motion.div>
))}
</div>
<MetricsBasisNote
basis={weakestBasis(dataProofs.map((proof) => proof.basis))}
className="mt-8"
/>
</div>
)}
+6 -4
View File
@@ -435,14 +435,14 @@ describe('ProductValueSection', () => {
it('should render data proofs when available', async () => {
const { ProductValueSection } = await import('./detail-product-value');
render(<ProductValueSection product={mockProduct} />);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('以可量化的目标约束交付')).toBeInTheDocument();
});
it('should not render data proofs section when empty', async () => {
const { ProductValueSection } = await import('./detail-product-value');
const productWithoutData = { ...mockProduct, dataProofs: [] };
render(<ProductValueSection product={productWithoutData} />);
expect(screen.queryByText('用真实效果证明价值')).not.toBeInTheDocument();
expect(screen.queryByText('以可量化的目标约束交付')).not.toBeInTheDocument();
});
});
@@ -462,8 +462,10 @@ describe('DetailTrustSection', () => {
dataProofs={mockProduct.dataProofs}
/>
);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('目标口径')).toBeInTheDocument();
expect(screen.getByText(mockProduct.dataProofs[0]!.metric)).toBeInTheDocument();
// 未声明 basis 的 dataProofs 必须自动落最弱口径角注,不得裸奔
expect(screen.getByTestId('metrics-basis-note')).toHaveTextContent('目标口径,非既成结果');
});
it('should render case studies', async () => {
@@ -500,7 +502,7 @@ describe('DetailTrustSection', () => {
accentColor="#C41E3A"
/>
);
expect(screen.getByText('用真实效果证明价值')).toBeInTheDocument();
expect(screen.getByText('目标口径')).toBeInTheDocument();
expect(screen.getByText('他们已经实现了转型突破')).toBeInTheDocument();
expect(screen.getByText('资质认证')).toBeInTheDocument();
});
@@ -9,7 +9,7 @@
* - 内容源于 `EARLY_ACCESS` 常量(company.ts L119),单一真源。
*
* 与 L3SignalsSlot 关系:EarlyAccessNotice 是 L3EmptyFallback 的次要兜底块,
* 在 signals(4 类信号卡)下方呈现——回答「为什么没有真实客户案例」这个问题。
* 在 signals(4 类信号卡)下方呈现——回答「为什么还没有可公开案例」这个问题。
*/
import Link from 'next/link';
+11 -4
View File
@@ -1,14 +1,21 @@
import { cn } from '@/lib/utils';
import { METRICS_BASIS_NOTE } from '@/lib/constants/metrics-basis';
import { METRIC_BASIS_NOTES, resolveMetricBasis, type MetricBasis } from '@/lib/constants/metrics-basis';
/**
* 结果型数字(提升百分比 / 可用性 / 达标率等)下方的口径角注。
* 全站统一文案,见 METRICS_BASIS_NOTE。供 solutions / services / home / about / products 复用。
* 文案按数字声明的 `basis` 口径解析,未声明一律按最弱的 target 处理,见 METRIC_BASIS_NOTES。
* 供 solutions / services / home / about / products 与详情页 DataProofSection 复用。
*/
export function MetricsBasisNote({ className }: { className?: string }) {
export function MetricsBasisNote({
className,
basis = 'target',
}: {
className?: string;
basis?: MetricBasis | string | null;
}) {
return (
<p className={cn('text-xs text-text-muted', className)} data-testid="metrics-basis-note">
{METRICS_BASIS_NOTE}
{METRIC_BASIS_NOTES[resolveMetricBasis(basis)]}
</p>
);
}
+25
View File
@@ -1,5 +1,22 @@
import type { FieldDefinition } from './types';
/**
* 数字口径声明字段——所有以大字数字样式呈现的指标(metrics / outcomes / dataProofs)必须携带。
* 渲染端按此自动附口径角注;留空一律按最弱的 target 处理,见 lib/constants/metrics-basis.ts。
* 只有具备可核验实测出处(报告、监控、已交付客户数据)时才应选 verified。
*/
const metricBasisField: FieldDefinition = {
name: 'basis',
type: 'select',
label: '数据口径',
description: '留空按「目标口径」处理并自动标注;仅在有可核验实测出处时选「已验证」',
options: [
{ label: '目标口径(设计/承诺值,非既成结果)', value: 'target' },
{ label: '团队历史项目成果(非本公司交付数据)', value: 'team-history' },
{ label: '已验证(有可核验实测出处)', value: 'verified' },
],
};
export interface ContentTypeConfig {
model: {
id: string;
@@ -164,6 +181,7 @@ const serviceFields: FieldDefinition[] = [
fields: [
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'label', type: 'text', label: '标签', required: true },
{ ...metricBasisField },
],
},
{
@@ -186,6 +204,7 @@ const serviceFields: FieldDefinition[] = [
{ name: 'metric', type: 'text', label: '指标名称', required: true },
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'description', type: 'text', label: '描述', required: true },
{ ...metricBasisField },
],
},
{
@@ -363,6 +382,7 @@ const productFields: FieldDefinition[] = [
fields: [
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'label', type: 'text', label: '标签', required: true },
{ ...metricBasisField },
],
},
{
@@ -385,6 +405,7 @@ const productFields: FieldDefinition[] = [
{ name: 'metric', type: 'text', label: '指标名称', required: true },
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'description', type: 'text', label: '描述', required: true },
{ ...metricBasisField },
],
},
{
@@ -548,6 +569,7 @@ const standaloneProductFields: FieldDefinition[] = [
fields: [
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'label', type: 'text', label: '标签', required: true },
{ ...metricBasisField },
],
},
{
@@ -570,6 +592,7 @@ const standaloneProductFields: FieldDefinition[] = [
{ name: 'metric', type: 'text', label: '指标名称', required: true },
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'description', type: 'text', label: '描述', required: true },
{ ...metricBasisField },
],
},
{
@@ -755,6 +778,7 @@ const solutionFields: FieldDefinition[] = [
fields: [
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'label', type: 'text', label: '标签', required: true },
{ ...metricBasisField },
],
},
{
@@ -1180,6 +1204,7 @@ const aboutPageFields: FieldDefinition[] = [
{ name: 'value', type: 'text', label: '数值', required: true },
{ name: 'label', type: 'text', label: '标签', required: true },
{ name: 'sub', type: 'text', label: '副标题' },
{ ...metricBasisField },
],
},
{
+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) 走共享数字样式渲染的指标必须声明 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([]);
});
});
+56 -6
View File
@@ -1,9 +1,59 @@
/**
* METRICS_BASIS_NOTE — 全站「结果型数字」口径角注单一真源。
* 数字口径(metric basis)单一真源。
*
* 2026-09-19 critique P0:solutions / services / home / about 把「30% 库存周转提升」
* 「99.9% 可用性」这类目标值/能力值以既成结果口吻呈现,缺出处即构成误导。
* 与 CTA_LABELS 同属渲染入口的诚实性防线:数字保留,但强制附口径说明。
* 根治方式是补充真实案例数据或 admin 修正内容,此处为最后一道防线。
* 2026-09-19 critique P0:solutions / services / home / about / products 把「30% 库存周转提升」
* 「99.9% 系统可用率」这类目标值/能力值以既成结果口吻呈现,缺出处即构成误导。
*
* 本文件把「每个数字必须声明口径」从个别页的自觉升级为结构约束:数字携带 `basis`,
* 渲染端据此自动附角注;未声明或声明非法一律按最弱口径 `target` 处理(保守默认,
* 宁可多标注也不裸奔)。真实出处由业务在 admin 录入,代码零虚构。
*/
export const METRICS_BASIS_NOTE = '目标口径,非既成结果';
/** 由弱到强:聚合一组数字时取最弱者,保证角注不夸大。 */
export const METRIC_BASES = ['target', 'team-history', 'verified'] as const;
export type MetricBasis = (typeof METRIC_BASES)[number];
const BASIS_STRENGTH: Record<MetricBasis, number> = {
target: 0,
'team-history': 1,
verified: 2,
};
/** 各口径对应的角注文案。 */
export const METRIC_BASIS_NOTES: Record<MetricBasis, string> = {
target: '目标口径,非既成结果',
'team-history': '团队历史项目成果,非本公司交付数据',
verified: '已有可核验的实测出处',
};
/** 全站默认口径角注(= 最弱口径 target 的文案)。 */
export const METRICS_BASIS_NOTE = METRIC_BASIS_NOTES.target;
/** 把数据里的任意值归一为合法口径;缺失/非法一律回落到最弱的 `target`。 */
export function resolveMetricBasis(basis?: string | null): MetricBasis {
return (METRIC_BASES as readonly string[]).includes(String(basis))
? (basis as MetricBasis)
: 'target';
}
/** 一组数字的聚合口径:取最弱的一个。 */
export function weakestBasis(bases: Array<MetricBasis | string | undefined | null>): MetricBasis {
if (bases.length === 0) return 'target';
return bases.map(resolveMetricBasis).reduce(
(weakest, cur) => (BASIS_STRENGTH[cur] < BASIS_STRENGTH[weakest] ? cur : weakest),
resolveMetricBasis(bases[0]),
);
}
/**
* 虚构佐证禁词表:本站 2026-01 成立、零公开客户案例(见 early-access-notice),
* 因此任何「数字源自真实客户案例 / 经过实战验证」的措辞都是编造佐证。
* 由 metrics-basis.test.ts 在 src/app + src/components 全源码扫描强制。
*/
export const FORBIDDEN_PROOF_PHRASES = [
'源自真实客户案例',
'真实客户案例',
'经过实战验证',
'实战验证',
] as const;
+4 -1
View File
@@ -1,3 +1,5 @@
import type { MetricBasis } from './metrics-basis';
export interface CaseStudy {
client: string;
industry: string;
@@ -10,6 +12,7 @@ export interface DataProof {
metric: string;
value: string;
description: string;
basis?: MetricBasis;
}
export interface Certification {
@@ -78,7 +81,7 @@ export interface Product {
techStack?: TechStackCategory[];
faqs?: FAQItem[];
scenario?: string;
metrics?: { value: string; label: string }[];
metrics?: { value: string; label: string; basis?: MetricBasis }[];
bundle: 'enterprise' | 'standalone';
capabilities?: string[];
}
+1
View File
@@ -37,6 +37,7 @@ export interface FAQItem {
export interface ServiceMetric {
value: string;
label: string;
basis?: import('./metrics-basis').MetricBasis;
}
export interface Service {
+1 -1
View File
@@ -24,7 +24,7 @@ export interface Solution {
dataProofs?: import('./products').DataProof[];
certifications?: import('./products').Certification[];
painPoints?: string[];
outcomes?: { value: string; label: string }[];
outcomes?: { value: string; label: string; basis?: import('./metrics-basis').MetricBasis }[];
recommendedProducts?: string[];
}