feat(w05): 差异评估两枚纯工具 —— 逐参数四态比对与推荐定级(R-2026.07)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-26 23:17:43 +08:00
co-authored by Claude Fable 5
parent 63034f5ab2
commit f52f0168df
4 changed files with 369 additions and 2 deletions
@@ -0,0 +1,129 @@
/**
* ACT-WORK-05 差异评估的两枚纯工具 —— 「能算的不问 LLM」:同输入必同结论。
* 用例逐条对应《ACT-WORK-05-活动流程与工单说明》§八的示例矩阵:
* A XL2596S-5.0 条件推荐(使能阈值未披露 → UNKNOWN)/ B LM2596S-ADJ 备选(可调版靠外围配成 5V)/
* C 国产 DCDC 不推荐(输入上限 32V < 客户 40V,硬约束 MISMATCH)。
*/
import { describe, expect, it } from 'vitest';
import { FIRST_PARTY_TOOLS } from '../tools/first-party.ts';
const handler = (name: string) => {
const found = FIRST_PARTY_TOOLS.find(([n]) => n === name);
if (found === undefined) throw new Error(`没有工具 ${name}`);
return found[1];
};
/** ToolHandler 的入参形状见 `apps/api/src/modules/toolbox/internal/adapter/toolbox-store.ts`:纯工具不用连接目标 */
const call = async (name: string, input: Record<string, unknown>) => {
const out = await handler(name)({ input, connection: {}, operation: undefined, idempotencyKey: undefined });
if (!out.ok) throw new Error(`工具失败:${JSON.stringify(out)}`);
return out.output as Record<string, unknown>;
};
type Item = { parameterName: string; compareResult: string; differenceStatus: string; riskLevel: string };
type Result = { candidateInternalPn: string; items: Item[]; counts: Record<string, number> };
const pair = (parameterName: string, originalValue: string, candidateValue: string, constraintType: 'hard' | 'soft', extra: Record<string, string> = {}) => ({
parameterName,
originalValue,
candidateValue,
constraintType,
evidenceRef: 'P-08 · P-10 Rev H',
...extra,
});
describe('compare-parameters:逐参数四态', () => {
it('候选温域更宽 → covers → MATCH 低风险;固定 vs 可调 → configurable → CONDITIONAL;未披露 → UNKNOWN', async () => {
const out = await call('part-substitution-assessment.compare-parameters', {
candidates: [
{
candidateInternalPn: 'XL2596S-5.0',
pairs: [
pair('工作温度', '-40~+85', '-40~+125', 'hard', { unit: '℃' }),
pair('输出电压', '5.0', '5.0', 'hard', { unit: 'V' }),
pair('使能阈值', '1.4', '未披露', 'hard', { unit: 'V' }),
pair('静态电流', '5', '4.2', 'soft', { unit: 'mA' }),
],
},
{
candidateInternalPn: 'LM2596S-ADJ',
pairs: [pair('输出电压', '5.0', '1.23~37', 'hard', { unit: 'V' })],
},
],
});
const results = out['results'] as Result[];
const a = results[0]!;
const b = results[1]!;
expect(out['ruleVersion']).toBe('R-2026.07');
const by = (r: Result, name: string) => r.items.find((i) => i.parameterName === name);
expect(by(a, '工作温度')).toMatchObject({ compareResult: 'covers', differenceStatus: 'match', riskLevel: 'low' });
expect(by(a, '输出电压')).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' });
expect(by(a, '使能阈值')).toMatchObject({ compareResult: 'unknown', differenceStatus: 'unknown', riskLevel: 'medium' });
expect(by(a, '静态电流')).toMatchObject({ differenceStatus: 'conditional' }); // 软约束数值不同 → 条件
expect(a.counts).toMatchObject({ match: 2, unknown: 1, conditional: 1, mismatch: 0, hardMismatch: 0 });
expect(by(b, '输出电压')).toMatchObject({ compareResult: 'configurable', differenceStatus: 'conditional' });
expect(b.counts['configurable']).toBe(1);
});
it('硬约束范围变窄 → MISMATCH 高风险;客户约束(requirement)不满足也是 MISMATCH', async () => {
const out = await call('part-substitution-assessment.compare-parameters', {
candidates: [
{
candidateInternalPn: 'CN-DCDC-32V',
pairs: [
pair('输入电压范围', '4.5~40', '4.5~32', 'hard', { unit: 'V', requirement: '>= 40' }),
pair('封装', 'TO-263-5', 'TO-263-5', 'hard'),
],
},
],
});
const c = (out['results'] as Result[])[0]!;
expect(c.items[0]).toMatchObject({ compareResult: 'narrower', differenceStatus: 'mismatch', riskLevel: 'high' });
expect(c.items[1]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match' });
expect(c.counts).toMatchObject({ mismatch: 1, hardMismatch: 1, match: 1 });
});
it('硬约束相等但没有证据引用 → 不判 MATCH,降为 CONDITIONAL(硬约束项未经证据支持不得判 MATCH)', async () => {
const out = await call('part-substitution-assessment.compare-parameters', {
candidates: [{ candidateInternalPn: 'X', pairs: [{ parameterName: '封装', originalValue: 'TO-263-5', candidateValue: 'TO-263-5', constraintType: 'hard' }] }],
});
const x = (out['results'] as Result[])[0]!;
expect(x.items[0]).toMatchObject({ compareResult: 'equal', differenceStatus: 'conditional' });
});
it('数值带单位/括号注记也认得:「5.0(固定)」「3A」「150 kHz」;生命周期按活跃度比', async () => {
const out = await call('part-substitution-assessment.compare-parameters', {
candidates: [{ candidateInternalPn: 'X', pairs: [pair('输出电压', '5.0(固定)', '5.0 V', 'hard'), pair('输出电流', '3A', '3', 'hard'), pair('开关频率', '150 kHz', '150', 'hard'), pair('生命周期', 'nrnd', 'active', 'soft')] }],
});
const x = (out['results'] as Result[])[0]!;
// 生命周期不是数值:候选比原料更活(nrnd → active)算 covers → MATCH,替 NRND 料本就是为了这个
expect(x.items.map((i) => i.differenceStatus)).toEqual(['match', 'match', 'match', 'match']);
expect(x.items[3]?.compareResult).toBe('covers');
});
});
describe('recommend:推荐等级四档', () => {
it('硬约束 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐;只有 configurable → 备选;全 MATCH → 推荐', async () => {
const out = await call('part-substitution-assessment.recommend', {
candidates: [
{ candidateInternalPn: 'C', counts: { match: 5, mismatch: 1, unknown: 0, conditional: 0, hardMismatch: 1, softMismatch: 0, configurable: 0 } },
{ candidateInternalPn: 'A', counts: { match: 5, mismatch: 0, unknown: 1, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } },
{ candidateInternalPn: 'B', counts: { match: 6, mismatch: 0, unknown: 0, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } },
{ candidateInternalPn: 'D', counts: { match: 7, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 } },
],
});
const results = out['results'] as Array<{ candidateInternalPn: string; recommendationLevel: string; riskLevel: string }>;
expect(results.map((r) => [r.candidateInternalPn, r.recommendationLevel, r.riskLevel])).toEqual([
['C', 'not_recommended', 'high'],
['A', 'conditional', 'medium'],
['B', 'alternate', 'medium'],
['D', 'recommended', 'low'],
]);
});
it('既有 UNKNOWN 又有 configurable:UNKNOWN 优先 → 条件推荐(验证闭环前不给备选)', async () => {
const out = await call('part-substitution-assessment.recommend', {
candidates: [{ candidateInternalPn: 'E', counts: { match: 1, mismatch: 0, unknown: 1, conditional: 1, hardMismatch: 0, softMismatch: 0, configurable: 1 } }],
});
expect((out['results'] as Array<{ recommendationLevel: string }>)[0]?.recommendationLevel).toBe('conditional');
});
});
+3 -2
View File
@@ -12,6 +12,7 @@ import type { Container } from './container.ts';
import { installRealActivities } from './seed-activities.ts'; import { installRealActivities } from './seed-activities.ts';
import { installSlots } from './seed-mvp-slots.ts'; import { installSlots } from './seed-mvp-slots.ts';
import { seedMvpFlow } from './seed-mvp-flow.ts'; import { seedMvpFlow } from './seed-mvp-flow.ts';
import { W05_TOOLS } from './tools-w05.ts';
/** 第一方功能工具:查历史价 —— MVP 用录制回放替身代真 CRM(README:mock 只在外界) */ /** 第一方功能工具:查历史价 —— MVP 用录制回放替身代真 CRM(README:mock 只在外界) */
const HISTORY_TOOL = { const HISTORY_TOOL = {
@@ -328,7 +329,7 @@ export async function seedMvp(container: Container): Promise<void> {
log(`① LLM 实例已绑定:deepseek-v4-flash · deepseek-v4-pro${apiKey === undefined ? '(环境无密钥 —— 两条都是未绑定,去治理台补)' : ''}`); log(`① LLM 实例已绑定:deepseek-v4-flash · deepseek-v4-pro${apiKey === undefined ? '(环境无密钥 —— 两条都是未绑定,去治理台补)' : ''}`);
/* ② 功能工具注册两审 + 逐槽位白名单 */ /* ② 功能工具注册两审 + 逐槽位白名单 */
for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL]) { for (const contract of [HISTORY_TOOL, MAIL_TOOL, CLASSIFY_TOOL, PLACEHOLDER_TOOL, COVERAGE_SCORE_TOOL, RANK_KEY_PERSONS_TOOL, ...W05_TOOLS]) {
const verdict = await container.toolbox.register({ const verdict = await container.toolbox.register({
contract, contract,
samples: [ samples: [
@@ -359,7 +360,7 @@ export async function seedMvp(container: Container): Promise<void> {
for (const [activityId, tools] of Object.entries(ALLOWLIST)) { for (const [activityId, tools] of Object.entries(ALLOWLIST)) {
await container.toolbox.setAllowlist(activityId, tools); await container.toolbox.setAllowlist(activityId, tools);
} }
log('② 功能工具已注册并入白名单(读类 1 · 写类 1 · 纯工具 4,逐活动授权)'); log('② 功能工具已注册并入白名单(读类 1 · 写类 1 · 纯工具 6,逐活动授权)');
/* /*
* ③ 活动装入 —— 两批: * ③ 活动装入 —— 两批:
+87
View File
@@ -0,0 +1,87 @@
/**
* ACT-WORK-05 差异评估的两枚纯工具契约(活动私有作用域)。
* 两处消费:`seed-mvp.ts`(db:seed 全套播种)与 `seed-activities.ts`(activities:install 装活动时顺带注册 ——
* 「安装即用」包含它声明过的纯工具能调得动;从前 E04 的工具只在 db:seed 里注册,开发机上只跑
* activities:install 的人第一次跑活动就撞「工具不存在」)。
*/
const PURE = {
effect: 'read' as const,
errors: [] as Array<{ code: string; retryable: boolean; message: string }>,
idempotent: true,
timeoutMs: 5_000,
retries: 0,
connectionKeys: [] as string[],
pure: true,
ownerActivityId: 'part-substitution-assessment',
};
/** 逐参数比对 —— 出参键与台账列同名(difference_status / risk_level),模型照抄 */
export const COMPARE_PARAMETERS_TOOL = {
...PURE,
name: 'part-substitution-assessment.compare-parameters',
displayName: '替代料逐参数比对',
version: '1.0.0',
description:
'按硬/软约束逐参数比对原 PN 与候选 PN,输出四态 difference_status(match/mismatch/unknown/conditional)、' +
'compare_result、risk_level 与各候选的计数。一次可比一批候选(candidates 数组),每对参数可带客户约束 requirement(如 ">= 40")与证据引用 evidenceRef;' +
'硬约束没有证据引用不判 match。规则版本 R-2026.07。',
input: {
type: 'object',
properties: {
candidates: {
type: 'array',
description: '[{candidateInternalPn, pairs:[{parameterName, originalValue, candidateValue, unit?, constraintType:"hard"|"soft", requirement?, evidenceRef?}]}]',
},
},
},
output: {
type: 'object',
properties: {
ruleVersion: { type: 'string' },
results: {
type: 'array',
description: '[{candidateInternalPn, items:[{parameterName, unit, constraintType, compareResult, differenceStatus, riskLevel, note}], counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]',
},
},
},
};
/** 推荐定级 —— 出参 recommendationLevel / riskLevel / reason 直接落 P-11 */
export const RECOMMEND_TOOL = {
...PURE,
name: 'part-substitution-assessment.recommend',
displayName: '替代推荐定级',
version: '1.0.0',
description:
'按每个候选的比对计数定推荐等级:硬约束 mismatch>0 → not_recommended;unknown>0 → conditional;' +
'只有可调配成(configurable)→ alternate;其余 conditional → conditional;全 match → recommended。',
input: {
type: 'object',
properties: {
candidates: { type: 'array', description: '[{candidateInternalPn, counts:{match,mismatch,unknown,conditional,hardMismatch,softMismatch,configurable}}]' },
},
},
output: {
type: 'object',
properties: {
ruleVersion: { type: 'string' },
results: { type: 'array', description: '[{candidateInternalPn, recommendationLevel, riskLevel, reason}]' },
},
},
};
export const W05_TOOLS = [COMPARE_PARAMETERS_TOOL, RECOMMEND_TOOL] as const;
/** 试运行审的样例(两件共用一份:并集覆盖各家入参) */
export const W05_TOOL_SAMPLE = {
input: {
candidates: [
{
candidateInternalPn: 'XL2596S-5.0',
pairs: [{ parameterName: '输出电压', originalValue: '5.0', candidateValue: '5.0', unit: 'V', constraintType: 'hard', evidenceRef: 'P-08' }],
counts: { match: 1, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 },
},
],
},
expectShape: { type: 'object' },
};
+150
View File
@@ -332,6 +332,154 @@ const slotPlaceholder: ToolHandler = async ({ input }) => {
}; };
}; };
/* ── ACT-WORK-05 差异评估:逐参数比对 —— 纯工具(活动私有作用域)─────────────────
*
* 「能算的不问 LLM」:同输入必同结论。规则与活动包 manifest 里的 D01/D02 两张 DMN 同一份;
* 这里是求值器,那里是随包冻结的文档。`RULE_VERSION` 随结论落账(rule_version)。
*
* 比对语义(按硬/软约束):
* equal / covers(候选范围 ⊇ 原范围) → match,低风险;
* configurable(原是定值、候选是含它的可调范围)→ conditional(要改外围);
* narrower / differs / partial → 硬:mismatch 高风险;软:conditional 中风险;
* 任一侧未披露 → unknown 中风险;
* 硬约束 equal/covers 但没给 evidenceRef → conditional(硬约束未经证据支持不得判 MATCH);
* requirement(客户约束,如 ">= 40")候选不满足 → 一律 mismatch 高风险。
*/
export const SUBSTITUTION_RULE_VERSION = 'R-2026.07';
type Parsed = { kind: 'number'; value: number } | { kind: 'range'; lo: number; hi: number } | { kind: 'text'; value: string } | { kind: 'unknown' };
type CompareResult = 'equal' | 'covers' | 'configurable' | 'narrower' | 'partial' | 'differs' | 'unknown';
type DifferenceStatus = 'match' | 'mismatch' | 'unknown' | 'conditional';
type RiskLevel = 'low' | 'medium' | 'high';
type ParamPair = {
parameterName: string;
originalValue: string;
candidateValue: string;
unit?: string;
constraintType: 'hard' | 'soft';
requirement?: string;
evidenceRef?: string;
};
type CompareCounts = { match: number; mismatch: number; unknown: number; conditional: number; hardMismatch: number; softMismatch: number; configurable: number };
const UNKNOWN_WORDS = /^(未披露|未知|未给出|unknown|n\/?a|tbd|待定|-|—|–)$/i;
const NUM = '[-+]?\\d+(?:\\.\\d+)?';
const RANGE_RE = new RegExp(`^(${NUM})\\s*(?:~|~|-|–|—|至|to)\\s*(${NUM})`, 'i');
const NUM_RE = new RegExp(`^(${NUM})`);
export function parseParamValue(raw: unknown): Parsed {
const s = String(raw ?? '').trim().replace(/\s+/g, ' ');
if (s === '' || UNKNOWN_WORDS.test(s)) return { kind: 'unknown' };
const r = RANGE_RE.exec(s);
if (r !== null) {
const lo = Number(r[1]);
const hi = Number(r[2]);
if (Number.isFinite(lo) && Number.isFinite(hi)) return { kind: 'range', lo: Math.min(lo, hi), hi: Math.max(lo, hi) };
}
const n = NUM_RE.exec(s);
if (n !== null && Number.isFinite(Number(n[1]))) return { kind: 'number', value: Number(n[1]) };
return { kind: 'text', value: s.toLowerCase() };
}
function compareParsed(o: Parsed, c: Parsed): CompareResult {
if (o.kind === 'unknown' || c.kind === 'unknown') return 'unknown';
if (o.kind === 'range' && c.kind === 'range') {
if (c.lo === o.lo && c.hi === o.hi) return 'equal';
if (c.lo <= o.lo && c.hi >= o.hi) return 'covers';
if (c.lo >= o.lo && c.hi <= o.hi) return 'narrower';
return c.hi < o.lo || c.lo > o.hi ? 'differs' : 'partial';
}
if (o.kind === 'number' && c.kind === 'number') return Math.abs(o.value - c.value) <= 1e-9 ? 'equal' : 'differs';
if (o.kind === 'number' && c.kind === 'range') return c.lo <= o.value && o.value <= c.hi ? 'configurable' : 'differs';
if (o.kind === 'range' && c.kind === 'number') return o.lo <= c.value && c.value <= o.hi ? 'narrower' : 'differs';
if (o.kind === 'text' && c.kind === 'text') return o.value === c.value ? 'equal' : 'differs';
return 'differs';
}
/** 客户约束:">= 40" / "<= 85" / "-40~85"(候选必须盖住);解析不了当没给 */
function satisfiesRequirement(requirement: string | undefined, c: Parsed): boolean | undefined {
if (requirement === undefined || requirement.trim() === '' || c.kind === 'unknown' || c.kind === 'text') return undefined;
const s = requirement.trim();
const ge = new RegExp(`^(?:>=|≥)\\s*(${NUM})$`).exec(s);
const le = new RegExp(`^(?:<=|≤)\\s*(${NUM})$`).exec(s);
const hi = c.kind === 'range' ? c.hi : c.value;
const lo = c.kind === 'range' ? c.lo : c.value;
if (ge !== null) return hi >= Number(ge[1]);
if (le !== null) return lo <= Number(le[1]);
const req = parseParamValue(s);
if (req.kind === 'range') return lo <= req.lo && hi >= req.hi;
if (req.kind === 'number') return lo <= req.value && hi >= req.value;
return undefined;
}
/** 生命周期不是数值:候选比原料「更活」算 covers(替 NRND 料本就是为了这个),更差才是 differs */
const LIFECYCLE_ORDER: Record<string, number> = { active: 3, nrnd: 2, risk: 2, eol: 1, discontinued: 0 };
function compareLifecycle(o: string, c: string): CompareResult | undefined {
const ro = LIFECYCLE_ORDER[o.trim().toLowerCase()];
const rc = LIFECYCLE_ORDER[c.trim().toLowerCase()];
if (ro === undefined || rc === undefined) return undefined;
return rc === ro ? 'equal' : rc > ro ? 'covers' : 'differs';
}
function judgePair(p: ParamPair): { compareResult: CompareResult; differenceStatus: DifferenceStatus; riskLevel: RiskLevel; note: string } {
const o = parseParamValue(p.originalValue);
const c = parseParamValue(p.candidateValue);
const hard = p.constraintType === 'hard';
let compareResult = compareParsed(o, c);
if (/生命周期|lifecycle/i.test(p.parameterName)) compareResult = compareLifecycle(p.originalValue, p.candidateValue) ?? compareResult;
const req = satisfiesRequirement(p.requirement, c);
if (req === false) {
// 客户约束不满足:比对结果照实留(narrower/differs),结论一律 MISMATCH 高风险
return { compareResult: compareResult === 'unknown' ? 'differs' : compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: `不满足客户约束 ${p.requirement ?? ''}` };
}
if (compareResult === 'unknown') return { compareResult, differenceStatus: 'unknown', riskLevel: 'medium', note: '一侧未披露:标 UNKNOWN,挂验证任务或补来源' };
if (compareResult === 'equal' || compareResult === 'covers') {
if (hard && (p.evidenceRef === undefined || p.evidenceRef.trim() === '')) {
return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '硬约束缺证据引用:未经证据支持不判 MATCH' };
}
return { compareResult, differenceStatus: 'match', riskLevel: 'low', note: compareResult === 'covers' ? '候选范围盖住原范围' : '一致' };
}
if (compareResult === 'configurable') return { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '候选可调,须改外围把它配成原值' };
if (compareResult === 'partial') return { compareResult, differenceStatus: 'conditional', riskLevel: hard ? 'medium' : 'low', note: '范围部分重叠,须按应用边界确认' };
compareResult = compareResult === 'narrower' ? 'narrower' : 'differs';
return hard
? { compareResult, differenceStatus: 'mismatch', riskLevel: 'high', note: '硬约束不一致' }
: { compareResult, differenceStatus: 'conditional', riskLevel: 'medium', note: '软约束不一致,须评估影响' };
}
const compareParameters: ToolHandler = async ({ input }) => {
const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; pairs: ParamPair[] }>) : [];
const results = candidates.map((cand) => {
const counts: CompareCounts = { match: 0, mismatch: 0, unknown: 0, conditional: 0, hardMismatch: 0, softMismatch: 0, configurable: 0 };
const items = (Array.isArray(cand.pairs) ? cand.pairs : []).map((p) => {
const j = judgePair(p);
counts[j.differenceStatus] += 1;
if (j.differenceStatus === 'mismatch') counts[p.constraintType === 'hard' ? 'hardMismatch' : 'softMismatch'] += 1;
if (j.compareResult === 'configurable') counts.configurable += 1;
return { parameterName: p.parameterName, unit: p.unit ?? '', constraintType: p.constraintType, ...j };
});
return { candidateInternalPn: cand.candidateInternalPn, items, counts };
});
return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } };
};
/**
* 推荐定级(D02,FIRST):硬 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐(等验证闭环);
* 有 configurable(改外围配得成)→ 备选;其余 conditional → 条件推荐;全 MATCH → 推荐。与 manifest 里的 D02 表同一份规则。
*/
const recommend: ToolHandler = async ({ input }) => {
const candidates = Array.isArray(input['candidates']) ? (input['candidates'] as Array<{ candidateInternalPn: string; counts: Partial<CompareCounts> }>) : [];
const results = candidates.map((cand) => {
const n = (k: keyof CompareCounts) => Number(cand.counts?.[k] ?? 0);
if (n('hardMismatch') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'not_recommended', riskLevel: 'high', reason: `硬约束 MISMATCH ${n('hardMismatch')} 项:硬约束失败直接不推荐` };
if (n('unknown') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `UNKNOWN ${n('unknown')} 项待验证闭环,通过后可升级推荐` };
if (n('configurable') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'alternate', riskLevel: 'medium', reason: `${n('configurable')} 项须改外围配成原值(另有 ${n('conditional') - n('configurable')} 项条件差异):客户接受改设计则启用` };
if (n('conditional') > 0) return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'conditional', riskLevel: 'medium', reason: `CONDITIONAL ${n('conditional')} 项须按应用边界确认` };
return { candidateInternalPn: cand.candidateInternalPn, recommendationLevel: 'recommended', riskLevel: 'low', reason: `${n('match')} 项全部 MATCH` };
});
return { ok: true, output: { ruleVersion: SUBSTITUTION_RULE_VERSION, results } };
};
export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> = [ export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> = [
['crm.history-price-read', historyPriceRead], ['crm.history-price-read', historyPriceRead],
['quote-intake.classify', classify], ['quote-intake.classify', classify],
@@ -343,4 +491,6 @@ export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> =
['slot.placeholder', slotPlaceholder], ['slot.placeholder', slotPlaceholder],
['crm-relationship-coverage.recompute-score', recomputeScore], ['crm-relationship-coverage.recompute-score', recomputeScore],
['crm-relationship-coverage.rank-key-persons', rankKeyPersons], ['crm-relationship-coverage.rank-key-persons', rankKeyPersons],
['part-substitution-assessment.compare-parameters', compareParameters],
['part-substitution-assessment.recommend', recommend],
]; ];