判据 ⑥ 跑通:A4 成本定价全链 —— 补齐取价基准这一环

A4 卡在 S3 底价自检要 `deviation`,而 S1「取价基准」调的是历史价工具,
那个工具**根本不回价格**:声明说出 deviation,工具给不出,于是永远缺一环。

按业务本意补齐:
  · `crm.history-price-read` 增回 `lastPrice`(上次成交单价)—— 一个不回价的
    「历史价工具」取不出价格基准;无历史的新料号回 0,由下游按「无基准」处置,
    不凭空编一个偏离率。
  · 新增纯工具 `quote-pricing.basis`:基准价 ↔ 拟报价 → 偏离率。
    偏离率要等 S2 定出拟报价才算得出来,所以落在 S2 之后(新步 S2B)。
  · A4 声明:S1 改落 `last_price`,新增 S2B,台账加 `last_price` 列。

顺带修:样例集一份喂所有工具,`quote-pricing.basis` 的必填 `quotedPrice` 不在其中,
新工具过不了两审 → 活动包安装被拒 → 旧资产继续生效,现象是「改了没反应」。
样例改为各家必填的并集。

现在 A4 跑完整条:取价基准 → DMN 定价(利润模型 P-01)→ 算偏离率 →
DMN 底价自检(底价门禁 P-02,处置=直接放行)→ 条件跳步 → 报价草案验收放行。

gates: tsc / eslint / depcruise / 397 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 05:14:01 +08:00
co-authored by Claude Opus 5
parent 9201f6b094
commit c17ccaf049
7 changed files with 105 additions and 10 deletions
+19 -1
View File
@@ -119,6 +119,7 @@ export async function seedQuote(container: Container): Promise<void> {
HISTORY_TOOL,
MAIL_TOOL,
CLASSIFY_TOOL,
pureTool('quote-pricing.basis', '取价基准:报价相对历史成交价的偏离率,供底价门禁 P-02 求值', 'quote-pricing', ['quotedPrice']),
pureTool('quote-pricing.margin', '利润模型 P-01 的确定性求值:客户等级×物料类型×数量档 → 目标毛利区间', 'quote-pricing', ['customerGrade']),
pureTool('quote-pricing.floor-gate', '底价门禁 P-02 的确定性求值:偏离底价 ±20% 即止', 'quote-pricing', ['deviation']),
pureTool('quote-archive.profile', '客户画像更新:把成交行为写成标签,供 P-01 的客户等级因子取用', 'quote-archive', ['customerCode']),
@@ -129,7 +130,8 @@ export async function seedQuote(container: Container): Promise<void> {
contract,
samples: [
{
input: { pn: 'LM358DR', historyDays: 30, customerGrade: '普通客户', deviation: 0, to: 'x@y.com', subject: 't', customerCode: 'C001' },
// 样例集一份喂所有工具:并集覆盖各家必填,少一个键就有工具过不了两审
input: { pn: 'LM358DR', historyDays: 30, customerGrade: '普通客户', deviation: 0, to: 'x@y.com', subject: 't', customerCode: 'C001', quotedPrice: 9.2, lastPrice: 8.4 },
expectShape: { type: 'object' },
},
],
@@ -272,6 +274,14 @@ export async function seedQuote(container: Container): Promise<void> {
{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } },
// records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行
{ target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } },
// 逐条办的料号:MVP 单条 RFQ,取受理归一出的那一条
{ target: 'pn', source: { from: 'output-port', port: 'intake', path: 'pn' } },
// P-01 利润模型的三个因子:物料类型就是 A1 打的标(A/B/C),真实取值;
// 客户等级与数量档在 MVP 取默认档 —— 接 CRM 后改为读 shared.customer 与按 EAU 分档,
// P-01 决策表本就有兜底行('-' = 任意),默认档不会让求值失真。
{ target: 'materialType', source: { from: 'output-port', port: 'intake', path: 'type_tag' } },
{ target: 'customerGrade', source: { from: 'constant', value: '普通客户' } },
{ target: 'volumeTier', source: { from: 'constant', value: '大批量' } },
],
reopen: false,
},
@@ -302,6 +312,14 @@ export async function seedQuote(container: Container): Promise<void> {
{ target: 'customerCode', source: { from: 'initiation', path: 'customerCode' } },
// records 来自 A1 的输出口(受理归一的产物),不是起单入参 —— 起单时还没有这些行
{ target: 'records', source: { from: 'output-port', port: 'intake', path: 'records' } },
// 逐条办的料号:MVP 单条 RFQ,取受理归一出的那一条
{ target: 'pn', source: { from: 'output-port', port: 'intake', path: 'pn' } },
// P-01 利润模型的三个因子:物料类型就是 A1 打的标(A/B/C),真实取值;
// 客户等级与数量档在 MVP 取默认档 —— 接 CRM 后改为读 shared.customer 与按 EAU 分档,
// P-01 决策表本就有兜底行('-' = 任意),默认档不会让求值失真。
{ target: 'materialType', source: { from: 'output-port', port: 'intake', path: 'type_tag' } },
{ target: 'customerGrade', source: { from: 'constant', value: '普通客户' } },
{ target: 'volumeTier', source: { from: 'constant', value: '大批量' } },
],
reopen: true,
},
+23 -5
View File
@@ -13,19 +13,36 @@ import type { ToolHandler } from '@modules/toolbox';
const historyPriceRead: ToolHandler = async ({ input }) => {
const pn = String(input['pn'] ?? '');
// 录制样本:演示用的历史价情报(真 CRM 接入时换 endpoint,契约不变)
const table: Record<string, { historyDays: number; marketStable: boolean; lifecycle: string }> = {
LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产' },
NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产' },
TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL' },
// 历史价情报含**上次成交单价** —— 没有价的「历史价工具」是取不出价格基准的
const table: Record<string, { historyDays: number; marketStable: boolean; lifecycle: string; lastPrice: number }> = {
LM358DR: { historyDays: 30, marketStable: true, lifecycle: '在产', lastPrice: 8.4 },
NE555P: { historyDays: 240, marketStable: false, lifecycle: '在产', lastPrice: 3.25 },
TL072CDR: { historyDays: 15, marketStable: true, lifecycle: 'EOL', lastPrice: 21.6 },
};
const hit = table[pn];
if (hit === undefined) {
// 无历史 → 走 B(重估后报);这不是错误
return { ok: true, output: { historyDays: 9999, marketStable: false, lifecycle: '未知', hit: false } };
// 无历史 → 走 B(重估后报);没有价格基准,基准价回 0 由下游按「无基准」处置
return { ok: true, output: { historyDays: 9999, marketStable: false, lifecycle: '未知', hit: false, lastPrice: 0 } };
}
return { ok: true, output: { ...hit, hit: true } };
};
/**
* 取价基准 —— **纯工具**:报价相对基准价的偏离率。
* 「能算的不问 LLM」:同输入必同结论。无基准价(新料号)时偏离率记 0,
* 由底价门禁 P-02 按「无基准」放行 —— 不能凭空编一个偏离率出来。
*/
const basis: ToolHandler = async ({ input }) => {
const lastPrice = Number(input['lastPrice'] ?? 0);
const quotedPrice = Number(input['quotedPrice'] ?? 0);
if (!Number.isFinite(lastPrice) || lastPrice <= 0) {
return { ok: true, output: { deviation: 0, basis: 'none', lastPrice: 0 } };
}
const deviation = (quotedPrice - lastPrice) / lastPrice;
return { ok: true, output: { deviation: Math.round(deviation * 10_000) / 10_000, basis: 'history', lastPrice } };
};
/** 三分类规则 P-03 的决策表 —— 与活动包里的资产同一份规则 */
const P03_RULES = [
{ id: 'p03-c', when: ['-', '-', '"EOL","缺货","要求替代"'], then: { type: 'C', play: '转案处理' } },
@@ -109,6 +126,7 @@ const mailSend: ToolHandler = async ({ input, connection }) => {
export const FIRST_PARTY_TOOLS: ReadonlyArray<readonly [string, ToolHandler]> = [
['crm.history-price-read', historyPriceRead],
['quote-intake.classify', classify],
['quote-pricing.basis', basis],
['quote-pricing.margin', margin],
['quote-pricing.floor-gate', floorGate],
['quote-archive.profile', profile],