Archived
差异比对三刀:客户约束不再一票否决 · 入参由基座从台账取 · 「下限~上限」一律读成范围
①**两颗一模一样的输入电压范围判成 MISMATCH**(用户送来 0bddd862):客户写「45V 以内」→ requirement `>= 45`,
而原 PN 与候选上限都是 40V;`judgePair` 只看候选过不过客户线、不看原 PN 自己过不过 ——
需求边界与在用料的差距记在了候选头上,硬约束 MISMATCH → 整颗不推荐。差异评估比的是「换上去会不会比现在差」:
候选与原 PN 相等 / 盖住、且客户线原 PN 也过不去 → 仍按比对结果判(MATCH,风险中),备注点名
「需求边界,请回头核对需求」;候选更窄且过不了线仍 MISMATCH(3 形有测)。
②**入参由基座从台账取,模型只报料号与客户约束**(用户:「这个做差异评估比对的 harness 肯定有问题」):
同一单痕里模型念出「输入上限 32V 低于客户 40V」—— 那是 S3 剧本示例的原话、不是这单的数;
工具入参只存哈希,喂了哪几对值事后查不到;写回时还把工具的 `equal` 改成 `less`。两头都靠模型抄、两头都没有基座核。
改三层:工具运行时把 `taskId / activityId` 交给处理器(`ToolHandler` 入参加两个可选键);
`compare-parameters` 给 `originalInternalPn + candidates[].candidateInternalPn(+ requirements)`,
参数行从 `parts.parameter`、封装 / 生命周期从 `parts.profile`、证据引用从 `parts.document` 由工具现读任务视图
(本单草稿也算),出参每项带原值与证据,写盘照抄;老形状(自带 pairs)照接,试运行走它;
container 用 `compareParametersWith(ledger)` 顶掉静态名单那份;≤800 字的工具入参整份进 `result` 痕(审得了才谈得上认真)。
**基座能读到的事实,不让模型转述给工具。**
③**「下限~上限」一律是范围**(用户:「4.5~40 / -40~+85 这种我怕模型搞不清楚」):实测解析器真有洞 ——
单位写进值里(`4.5V~40V` / `-40℃~+85℃`)被读成单个数,Unicode 减号、`…`、`≥45 / 40 以内` 读成文本。
分隔符认 ~ ~ - – — … 至 到 to、单位可夹在值里、单边界读成半开范围;出参每项 `readAs:{original,candidate}`
写明「范围 4.5~40 / 定值 5 / 下界 ≥ 45 / 文本」—— 比对依据可见,模型照它写影响、不自己重新解释值;
契约描述与 S3 / P-08 剧本明写读法(示例用中性数 1~10,不用真数据里的 4.5~40 —— 模型会把示例当这单的值)。
工具 1.0.0 → 1.3.0(注册一名一行、upsert 换版本,handler 在进程内按名绑,装完稳定版重建即生效);
W05 包 1.6.2 → 1.6.4:S3 第一拍只报料号与约束、示例只留占位形,第三拍 compare_result 照抄,S4 重比只改 requirements。
13 工具测 + 包内 89。在途单要人在审核卡上「重做」并写明意见才会重比。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
* C 国产 DCDC 不推荐(输入上限 32V < 客户 40V,硬约束 MISMATCH)。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FIRST_PARTY_TOOLS } from '../tools/first-party.ts';
|
||||
import { FIRST_PARTY_TOOLS, compareParametersWith, describeParsed, parseParamValue } from '../tools/first-party.ts';
|
||||
import type { ParameterLedger } from '../tools/first-party.ts';
|
||||
import type { TaskId } from '@contracts';
|
||||
|
||||
const handler = (name: string) => {
|
||||
const found = FIRST_PARTY_TOOLS.find(([n]) => n === name);
|
||||
@@ -113,6 +115,34 @@ describe('compare-parameters:逐参数四态', () => {
|
||||
expect(c.counts).toMatchObject({ mismatch: 1, hardMismatch: 1, match: 1 });
|
||||
});
|
||||
|
||||
/*
|
||||
* 2026-08-27 晚,用户送来 0bddd862:客户写「输入电压在 45V 以内」→ requirement ">= 45",而原 PN 与候选的输入
|
||||
* 上限都是 40V。从前客户约束一票否决:两颗一模一样却判硬约束 MISMATCH、整颗不推荐。差异评估比的是
|
||||
* 「换上去会不会比现在差」—— 原 PN 自己也过不了的客户线,是需求边界的事,不是替代差异。
|
||||
*/
|
||||
it('候选与原 PN 相等、客户约束两颗都不满足 → 仍是 MATCH(风险中、备注点名需求边界),不记在候选头上', async () => {
|
||||
const out = await call('part-substitution-assessment.compare-parameters', {
|
||||
candidates: [
|
||||
{
|
||||
candidateInternalPn: 'LM2596S-ADJ',
|
||||
pairs: [
|
||||
pair('输入电压范围', '4.5~40', '4.5~40', 'hard', { unit: 'V', requirement: '>= 45' }),
|
||||
// 候选盖住原范围、客户线两颗都过不去 —— 同理
|
||||
pair('工作温度', '-40~85', '-40~125', 'hard', { unit: '℃', requirement: '-55~125' }),
|
||||
// 候选比原 PN 窄且过不了客户线 —— 仍是 MISMATCH
|
||||
pair('输出电流', '3', '2', 'hard', { unit: 'A', requirement: '>= 3' }),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const c = (out['results'] as Result[])[0]!;
|
||||
expect(c.items[0]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match', riskLevel: 'medium' });
|
||||
expect((c.items[0] as Item & { note: string }).note).toMatch(/需求边界/);
|
||||
expect(c.items[1]).toMatchObject({ compareResult: 'covers', differenceStatus: 'match' });
|
||||
expect(c.items[2]).toMatchObject({ compareResult: 'differs', differenceStatus: 'mismatch', riskLevel: 'high' });
|
||||
expect(c.counts).toMatchObject({ match: 2, mismatch: 1, hardMismatch: 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' }] }],
|
||||
@@ -132,6 +162,123 @@ describe('compare-parameters:逐参数四态', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* **比对入参由基座从台账取**(2026-08-27 晚,用户:「要让它认真比对每一条,然后再输出结论」):
|
||||
* 模型只报料号与客户约束;参数 / 封装 / 生命周期 / 证据引用由工具从 parts.* 现读。
|
||||
*/
|
||||
describe('compare-parameters:按料号从台账取参数', () => {
|
||||
const rows: Record<string, Array<Record<string, unknown>>> = {
|
||||
'parts.parameter|LM2596S-5.0': [
|
||||
{ internal_pn: 'LM2596S-5.0', param_name: '输入电压范围', param_value: '4.5~40', param_unit: 'V', constraint_type: 'hard' },
|
||||
{ internal_pn: 'LM2596S-5.0', param_name: '输出电压', param_value: '5.0', param_unit: 'V', constraint_type: 'hard' },
|
||||
{ internal_pn: 'LM2596S-5.0', param_name: '使能阈值', param_value: '1.4', param_unit: 'V', constraint_type: 'hard' },
|
||||
],
|
||||
'parts.parameter|LM2596S-ADJ': [
|
||||
{ internal_pn: 'LM2596S-ADJ', param_name: '输入电压范围', param_value: '4.5~40', param_unit: 'V', constraint_type: 'hard' },
|
||||
{ internal_pn: 'LM2596S-ADJ', param_name: '输出电压', param_value: '1.23~37', param_unit: 'V', constraint_type: 'hard' },
|
||||
// 候选多出来的参数不进比对(原 PN 定义比对集),只在 extraCandidateParams 里点名
|
||||
{ internal_pn: 'LM2596S-ADJ', param_name: '反馈电压', param_value: '1.23', param_unit: 'V', constraint_type: 'soft' },
|
||||
],
|
||||
'parts.profile|LM2596S-5.0': [{ internal_pn: 'LM2596S-5.0', package: 'TO-263-5', lifecycle_status: 'nrnd' }],
|
||||
'parts.profile|LM2596S-ADJ': [{ internal_pn: 'LM2596S-ADJ', package: 'TO-263-5', lifecycle_status: 'active' }],
|
||||
'parts.document|LM2596S-ADJ': [{ internal_pn: 'LM2596S-ADJ', document_type: 'datasheet', document_version: 'Rev H (2023-09)', document_ref: 'https://x/lm2596.pdf' }],
|
||||
};
|
||||
const seen: string[] = [];
|
||||
const ledger: ParameterLedger = {
|
||||
rows: async (taskId, address, where) => {
|
||||
seen.push(`${taskId ?? '-'}:${address.domain}.${address.object}|${where['internal_pn']}`);
|
||||
return rows[`${address.domain}.${address.object}|${where['internal_pn']}`] ?? [];
|
||||
},
|
||||
};
|
||||
const tool = compareParametersWith(ledger);
|
||||
const run = async (input: Record<string, unknown>, taskId?: TaskId) => {
|
||||
const out = await tool({ input, connection: {}, operation: undefined, idempotencyKey: undefined, taskId });
|
||||
return out;
|
||||
};
|
||||
|
||||
it('只给料号 + 客户约束 → 每一对都是台账原值;封装 / 生命周期从档案来;证据引用从资料记录来;读的是**这一单**的任务视图', async () => {
|
||||
const out = await run({ originalInternalPn: 'LM2596S-5.0', candidates: [{ candidateInternalPn: 'LM2596S-ADJ', requirements: { 输入电压范围: '>= 45' } }] }, 't-1' as TaskId);
|
||||
expect(out.ok).toBe(true);
|
||||
const r = (out.output!['results'] as Array<Record<string, unknown>>)[0]!;
|
||||
expect(r['source']).toBe('ledger');
|
||||
const items = r['items'] as Array<Record<string, unknown>>;
|
||||
const byName = Object.fromEntries(items.map((i) => [i['parameterName'] as string, i]));
|
||||
expect(byName['输入电压范围']).toMatchObject({ originalValue: '4.5~40', candidateValue: '4.5~40', unit: 'V', constraintType: 'hard', requirement: '>= 45', evidenceRef: 'P-08 · P-10 Rev H (2023-09)', differenceStatus: 'match' });
|
||||
expect(byName['输出电压']).toMatchObject({ originalValue: '5.0', candidateValue: '1.23~37', compareResult: 'configurable', differenceStatus: 'conditional' });
|
||||
// 原 PN 有、候选没有 → 候选值空 → UNKNOWN
|
||||
expect(byName['使能阈值']).toMatchObject({ originalValue: '1.4', candidateValue: '', differenceStatus: 'unknown' });
|
||||
expect(byName['封装']).toMatchObject({ originalValue: 'TO-263-5', candidateValue: 'TO-263-5', constraintType: 'hard', differenceStatus: 'match' });
|
||||
expect(byName['生命周期']).toMatchObject({ originalValue: 'nrnd', candidateValue: 'active', constraintType: 'soft', compareResult: 'covers', differenceStatus: 'match' });
|
||||
expect(r['extraCandidateParams']).toEqual(['反馈电压']);
|
||||
expect(r['counts']).toMatchObject({ match: 3, conditional: 1, unknown: 1, mismatch: 0 });
|
||||
expect(seen.every((s) => s.startsWith('t-1:'))).toBe(true);
|
||||
});
|
||||
|
||||
it('原 PN 台账里没有 → 如实报错,不比一半;没给 originalInternalPn → 拒', async () => {
|
||||
const missing = await run({ originalInternalPn: 'NOPE', candidates: [{ candidateInternalPn: 'LM2596S-ADJ' }] });
|
||||
expect(missing.ok).toBe(false);
|
||||
expect(missing.error?.message).toMatch(/NOPE/);
|
||||
const noOriginal = await run({ candidates: [{ candidateInternalPn: 'LM2596S-ADJ' }] });
|
||||
expect(noOriginal.ok).toBe(false);
|
||||
expect(noOriginal.error?.message).toMatch(/originalInternalPn/);
|
||||
});
|
||||
|
||||
it('自带 pairs 的老形状照旧接(试运行 / 老调用方),source 标 pairs', async () => {
|
||||
const out = await run({ candidates: [{ candidateInternalPn: 'X', pairs: [pair('输出电压', '5.0', '5.0', 'hard')] }] });
|
||||
expect(out.ok).toBe(true);
|
||||
const r = (out.output!['results'] as Array<Record<string, unknown>>)[0]!;
|
||||
expect(r['source']).toBe('pairs');
|
||||
expect((r['items'] as Array<Record<string, unknown>>)[0]).toMatchObject({ originalValue: '5.0', candidateValue: '5.0', differenceStatus: 'match' });
|
||||
});
|
||||
|
||||
it('没有台账的那份(静态名单)遇到不带 pairs 的入参 → 如实说这条通道读不到台账', async () => {
|
||||
const out = await compareParametersWith()({ input: { originalInternalPn: 'A', candidates: [{ candidateInternalPn: 'B' }] }, connection: {}, operation: undefined, idempotencyKey: undefined });
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.error?.message).toMatch(/读不到台账/);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 2026-08-28 用户:「4.5~40 / -40~+85 这种我怕模型搞不清楚,harness 里强调一下是范围值」。
|
||||
* 解析器把各种写法都认成范围,出参 readAs 把「读作什么」摆回去 —— 比对依据可见,模型不必自己再解释一遍值。
|
||||
*/
|
||||
describe('值的读法:「下限~上限」一律是范围', () => {
|
||||
it('分隔符 / 正负号 / 单位写进值里 / Unicode 减号 / 省略号 / 中文「至到」都认成范围;单边界读成半开范围;定值一个数', () => {
|
||||
const range = (v: string) => parseParamValue(v);
|
||||
for (const v of ['4.5~40', '4.5~40', '4.5-40', '4.5 ~ 40', '4.5V~40V', '4.5V ~ 40V (典型)', '4.5…40', '4.5...40', '4.5至40', '4.5到40', '4.5 to 40']) {
|
||||
expect(range(v), v).toEqual({ kind: 'range', lo: 4.5, hi: 40 });
|
||||
}
|
||||
for (const v of ['-40~+85', '-40~85', '−40~+85', '-40℃~+85℃', '−40 ℃ … +85 ℃', '-40 - 85', '-40 to +85']) {
|
||||
expect(range(v), v).toEqual({ kind: 'range', lo: -40, hi: 85 });
|
||||
}
|
||||
expect(range('≥45')).toEqual({ kind: 'range', lo: 45, hi: Number.POSITIVE_INFINITY });
|
||||
expect(range('45 以上')).toEqual({ kind: 'range', lo: 45, hi: Number.POSITIVE_INFINITY });
|
||||
expect(range('<= 40')).toEqual({ kind: 'range', lo: Number.NEGATIVE_INFINITY, hi: 40 });
|
||||
expect(range('40V 以内')).toEqual({ kind: 'range', lo: Number.NEGATIVE_INFINITY, hi: 40 });
|
||||
expect(range('40')).toEqual({ kind: 'number', value: 40 });
|
||||
expect(range('5.0(固定)')).toEqual({ kind: 'number', value: 5 });
|
||||
expect(range('150 kHz')).toEqual({ kind: 'number', value: 150 });
|
||||
expect(range('TO-263-5')).toEqual({ kind: 'text', value: 'to-263-5' });
|
||||
});
|
||||
|
||||
it('readAs 说人话:范围 / 定值 / 单边界 / 文本;纯文本(封装)不啰嗦,像范围却没读成的才提示怎么写', () => {
|
||||
expect(describeParsed(parseParamValue('-40~+85'))).toBe('范围 -40~85');
|
||||
expect(describeParsed(parseParamValue('5.0'))).toBe('定值 5');
|
||||
expect(describeParsed(parseParamValue('≥45'))).toBe('下界 ≥ 45');
|
||||
expect(describeParsed(parseParamValue('TO-263-5'))).toBe('文本「to-263-5」');
|
||||
expect(describeParsed(parseParamValue('约 4.5 ~ 40'))).toMatch(/请写成 下限~上限/);
|
||||
});
|
||||
|
||||
it('两颗都写 4.5V~40V(单位在值里)→ 读成同一段范围 → equal / MATCH,出参 readAs 摆出「范围 4.5~40」', async () => {
|
||||
const out = await call('part-substitution-assessment.compare-parameters', {
|
||||
candidates: [{ candidateInternalPn: 'X', pairs: [pair('输入电压范围', '4.5V~40V', '4.5 ~ 40 V', 'hard', { unit: 'V' }), pair('工作温度', '-40℃~+85℃', '−40~+85', 'hard', { unit: '℃' })] }],
|
||||
});
|
||||
const items = (out['results'] as Array<{ items: Array<Item & { readAs: { original: string; candidate: string } }> }>)[0]!.items;
|
||||
expect(items[0]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match', readAs: { original: '范围 4.5~40', candidate: '范围 4.5~40' } });
|
||||
expect(items[1]).toMatchObject({ compareResult: 'equal', differenceStatus: 'match', readAs: { original: '范围 -40~85', candidate: '范围 -40~85' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('recommend:推荐等级四档', () => {
|
||||
it('硬约束 MISMATCH → 不推荐;有 UNKNOWN → 条件推荐;只有 configurable → 备选;全 MATCH → 推荐', async () => {
|
||||
const out = await call('part-substitution-assessment.recommend', {
|
||||
|
||||
Reference in New Issue
Block a user