This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/activities/quote-intake/src/case-model.ts
T
ChaiHaoandClaude Opus 5 ade0fa5a94 A2-A7 全部补齐到验收线;顺手把几处「门拦住却不给路」的坑填了
活动:A2 选型 / A3 寻源 / A4 定价 / A6 议价 / A7 归档五件各补成真活动包 ——
契约宽进(input.required 空,归一在活动内)、台账必填声明(含行级 requiredWhen)、
入场哨兵与 L1/L2 判据一律读基座数、两份件(在办可编辑 + 办结静态快照)、
三案例常备、包内自测。e2e 各三案例 released、行动被拒 0、发口正确、提交闸每单真拦。

基座(共享面,改一处影响所有活动,故每处都带回归):
· 重发判断走在站上场之前 —— 人答完那步常已收掉,readyToWork 先抛「不能再往它上面挂活」,
  于是「重发已答过的卡 = 无事发生」这条唯一活路在最需要时够不着,A1 基线当场熔断;
· 决策卡补幂等兜底(与执行工单同一把尺):头两次无事发生,第三次起才算被拒;
· 输出没成形先就地重问一次,原话进工作日志、不再摆上卡面;
· 工具调用读数 tool_<名>_calls —— 「机检只读基座数」这条纪律第一次覆盖到工具;
· 执行卡 steps/standard 补进出参 schema(从前能到卡面全靠厂商 JSON 模式宽松);
· 件的就地更新改逐节点 morph,保存先把新值镜像进只读那份 —— 不闪、不等回声。

A1(基线,全程守住):措辞三档对齐(必填/建议/选填)、禁止拿历史价当客户目标价、
卡骨架从步骤剧本搬进说明书(压成一拍连做时剧本没摊开,会发出没有指导书的裸卡)。

契约两处**新增可选字段**(非破坏):FieldDeclaration.requiredWhen、工单 finalHtml;
api-report 同批更新。docs/adr 与 docs/archive 的 13 份文件在工作树里不见了,
本次予以恢复而非提交删除。

门禁:typecheck / lint / depcruise / test(1366)全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:28:31 +08:00

765 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A1 询价受理的案例模型 —— **双表示同版本**:CMMN XML(机器面)与案例说明书(叙事面)。
*
* 业务出处:`docs/MVP示例工作流.html` §1.1 的那张步骤流程图,原样照搬:
*
* ● 接流程起点 → S1 接收登记 → D1 判断完整度
* ├─ 不完整 → S2 追问补全信息 ─┐
* └─ 完整 ───────────────────┴→ S3 逐条解析归一
* → S4 查历史价 → S5 逐条定状况标 → S6 定去向与派单 → G1
*
* **最长路径就是这六步**(常路五步 —— 字段齐就不走 S2),末尾加一张受理放行(终局唯放行)。
*
* ── 建模纪律:图上有几步就是几项 ──────────────────────────────────────
*
* 这份模型**重写过一次**。上一版自作主张加了两个阶段(收料归一 / 定性分级)、两个里程碑、
* 一个裁量项、一个用户事件监听器,12 个计划项去装 6 步的活。业务上一分没多,
* 卡壳的花样却多了一堆:阶段被当成写的落点(收了阶段,里面的项就永远入不了场)、
* 里程碑要不要收、项该先 start 还是先干、编出不存在的项 id……
*
* 实测账很清楚:**每一条「行动被拒」都是结构上的,没有一条是业务判断上的** ——
* 模型在跟 harness 较劲,不是在办事;一单烧掉二三十万 token 才收口,还常常收不了口。
*
* 所以:**图上有几步就是几项,不多建一件**。阶段、里程碑、裁量项都是 CMMN 有的东西,
* 但「有」不等于「该用」—— 它们要能换来业务上说得出的好处,否则就只是给 Agent 添的坑。
*/
export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">
<case id="intake">
<!--
案卷项的 id **就是任务视图里的键名** —— 前提的 onPart 认的是「这个键有值了」。
起单入参(subject)与台账列(line_total / missing_key_total / history_days /
untyped_total / need_sourcing)都会摊平到任务视图顶层,所以照它们命名。
名字对不上就等于前提永不满足:整案第一步都动不了(踩过)。
开案即有的只有那封信(客户邮箱 / 主题 / 邮件内容 / 邮件附件)。**询价单号不在其列** ——
它是 S1 从信里认出来的,认出来才写得动台账(所有台账对象都以 rfq_ref 为主键)。
-->
<caseFileModel>
<caseFileItem id="subject" name="来件主题(开案即有)"/>
<!-- ① 定位客户:认出来(或人建出来)才算数 -->
<caseFileItem id="customer_total" name="客户已确认"/>
<caseFileItem id="customer_new_total" name="新客户(需建档)"/>
<caseFileItem id="customer_asked_total" name="已就建档问过人"/>
<caseFileItem id="line_total" name="登记出的条目数"/>
<!-- ② 归一 RFQ:必填缺 = PN / 年用量;选填缺 = 品牌 / 备注 -->
<!--
缺必填的行数由**基座**按台账声明的 required 现数(quote_intake_record_missing)——
不读活动自报的 missing_key_total:那个数要模型自己重算,漏算一次这道门就永远开不了
(连跑十轮里三轮死在这)。自报的两个数照旧写,它们是给报告和人看的。
-->
<caseFileItem id="quote_intake_record_missing" name="缺必填的行数(基座现数)"/>
<caseFileItem id="quote_intake_record_rows" name="受理记录行数(基座现数)"/>
<caseFileItem id="missing_key_total" name="缺必填字段条数(PN/年用量)"/>
<caseFileItem id="missing_minor_total" name="缺选填字段条数(品牌/备注)"/>
<caseFileItem id="asked_total" name="已就缺口问过人的次数"/>
<caseFileItem id="answered_total" name="人答完并已落表的次数"/>
<!-- ③ 受理需求交人审定:这一单**经过人的手**了没有 -->
<caseFileItem id="confirmed_total" name="受理需求已经人审定"/>
<caseFileItem id="history_days" name="历史价判据"/>
<caseFileItem id="untyped_total" name="未打标条数"/>
<caseFileItem id="need_sourcing" name="去向旗"/>
</caseFileModel>
<casePlanModel id="plan" name="询价受理" autoComplete="true">
<!-- D1 判断完整度在图上是一个菱形,在这里是两条前提:先认客户,再看归一齐没齐 -->
<!--
① 客户认出来了 —— 归一那一步的入场前提。
先认客户再解析正文,不是先后顺序的洁癖:归一出来的每一条都要落在
某个客户名下(record.customer_code),客户没定位就写不动那一列。
-->
<sentry id="s_customer">
<onPart id="op_cu" caseFileItemRef="customer_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_cu"><condition>customer_total &gt; 0 and (customer_new_total = 0 or customer_asked_total &gt; 0)</condition></ifPart>
</sentry>
<!--
② 拆完了 —— 交人审定那一步的入场前提。只看「有没有拆出行来」:
缺不缺字段不影响该不该给人看,缺口是**卡上要说清的事**,不是拦住这张卡的理由。
读**基座数的行数**,不读自报的 line_total(2026-08-02 用户实测送来的单,
「机检只读基座数」第四次被违反现形):模型把 record 三行在 S1 拍就写了،
却没记 line_total(那本是 S2 的账),S2 空手收步 —— 这道门于是永远不开,
引擎报「依赖断点」,模型被推去发补全卡绕路,两张多余的卡把 4 次人环
预算烧光,整单 exhausted。行落了没有,基座数得出来;门不该等一个
要模型自己记的数。
-->
<sentry id="s_parsed">
<onPart id="op_ps" caseFileItemRef="quote_intake_record_rows"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_ps"><condition>quote_intake_record_rows &gt; 0</condition></ifPart>
</sentry>
<!--
③ **人审定过了** —— 后面查历史价/打标的入场前提。
必填(PN/年用量)缺一条都不算完(它是验收标准);而**这一单必须经过人的手**:
confirmed_total 由「人把受理需求那张卡交回来」这件事产生 —— 详见 S3_confirm 的注释。
从前这里写的是「missing_minor_total = 0 or answered_total > 0」:
**只在缺东西的时候才叫人**。于是一封字段齐全的询价进来,这个条件当场为真,
引擎在下一拍开头把归一那一步自己收了 —— 人一眼没看过受理需求,
下一次被叫到已经是终局的验收放行(用户 2026-08-01 当场指出:
「我都没确认需求 它就让我验收了」)。**确认不是缺口的补丁,是这一步本身。**
-->
<sentry id="s_complete">
<onPart id="op_cp" caseFileItemRef="confirmed_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_cp"><condition>line_total &gt; 0 and quote_intake_record_missing = 0 and confirmed_total &gt; 0</condition></ifPart>
</sentry>
<sentry id="s_basis">
<onPart id="op_bs" caseFileItemRef="history_days"><standardEvent>update</standardEvent></onPart>
</sentry>
<!--
打完标才进得了定去向 —— 条件读**基座按值现数**的三类条数,不读活动自报的
untyped_total(那是它自己写的数,写错一次这道门就形同虚设)。
-->
<sentry id="s_typed">
<onPart id="op_tp" caseFileItemRef="untyped_total"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_tp"><condition>quote_intake_record_rows &gt; 0 and quote_intake_record_type_tag_a + quote_intake_record_type_tag_b + quote_intake_record_type_tag_c = quote_intake_record_rows</condition></ifPart>
</sentry>
<!--
── 放行门的入场条件(2026-08-02 用户实测逼出来的)────────────────────────
从前这道哨兵**只有 onPart 没有 ifPart**:意思是「只要有人往 need_sourcing
写过一笔就开门」—— 写个 false 也算。实测的下场:模型跳过 S4/S5/S6,
在 verdict 上顺手写了 need_sourcing: false,放行门当场 Available,
它 start 完就提交;而受理记录三行一个标都没打、分类表一行都没写。
提交门正确地拦住了,可**该拦的地方本不该放它进来** —— 一道只认「有人写过」
的门,等于没有门。
现在认的是**办完了没有**,且三个数全由基座现数(活动自报的数改一下就骗过去了):
逐条打了标、去向定过了。route_decided_total 是 S6 显式写下的记账数
(旗是布尔,分不清「没写」与「写了 false」)。
-->
<sentry id="s_routed">
<onPart id="op_rt" caseFileItemRef="need_sourcing"><standardEvent>update</standardEvent></onPart>
<ifPart id="if_rt"><condition>quote_intake_record_rows &gt; 0 and quote_intake_record_type_tag_a + quote_intake_record_type_tag_b + quote_intake_record_type_tag_c = quote_intake_record_rows and route_decided_total &gt; 0</condition></ifPart>
</sentry>
<!--
① 定位客户信息 —— 拿发件邮箱去客户主数据里认人。
认得出就取它的客户编码;认不出说明这是个**新客户**,发数据录入卡请人建档
(公司名、客户编码、联系人)—— 建档是人的事,活动不替他编一个客户出来。
-->
<humanTask id="S1_customer" name="定位客户信息:按发件邮箱认客户,认不出请人建档">
<itemControl><repetitionRule id="r_s1"/></itemControl>
</humanTask>
<!--
② 归一客户 RFQ 输入 —— 解析邮件正文,逐条落成受理记录。
PN / 年用量**必填**;客户料号 / 品类 / 制造商 / 目标价是**建议追问**;其余**选填**。缺哪几格照实记进两个缺口数。
**这一步不叫人**:拆料是 AI 的活,拆完交给下一步给人看。
-->
<humanTask id="S2_normalize" name="归一客户RFQ输入:解析正文逐条落表">
<entryCriterion id="ec_s2" sentryRef="s_customer"/>
<itemControl><repetitionRule id="r_s2"/></itemControl>
</humanTask>
<!--
③ 受理需求交人审定 —— **阶段2 那张执行页,一单必经一次**。
目标件 「A1-询价受理需求.html」 的落款写着「AI 拟稿,你审定;可就地修改后再确认」,
页脚是【修改】与【确认受理记录】—— 它从来就是一张**人签的页**,不是缺东西时才弹的追问。
**manualActivation + 不给 doneWhen**:两样缺一不可。
触达世界的项一律不给 doneWhen —— 给了就等于给引擎一把替人签字的钥匙;
而这一步的成果(人看过、改过、确认了)按定义不可能由引擎自己判出来。
-->
<humanTask id="S3_confirm" name="受理需求交人审定:整份清单可就地改,确认后才往下走">
<entryCriterion id="ec_s3" sentryRef="s_parsed"/>
<itemControl><manualActivationRule id="m_s3"/></itemControl>
</humanTask>
<!-- S4 查历史价:取齐 P-03 的三项判据 -->
<humanTask id="S4_history" name="查历史价:取齐距上次报价天数/行情/生命周期">
<entryCriterion id="ec_s4" sentryRef="s_complete"/>
</humanTask>
<!-- S5 逐条定状况标:决策表求值,不是临场判断 -->
<decisionTask id="S5_classify" name="逐条定状况标:按三分类规则P-03打标" decisionRef="p03-type">
<entryCriterion id="ec_s5" sentryRef="s_basis"/>
</decisionTask>
<!-- S6 定去向与派单:两枚去向旗 + 开替代评估请求/询价请求 -->
<humanTask id="S6_route" name="定去向与派单:立两枚旗,开替代评估请求/询价请求">
<entryCriterion id="ec_s6" sentryRef="s_typed"/>
</humanTask>
<!-- 受理放行:终局唯放行 —— 触达世界,必须人工激活 -->
<humanTask id="T_release" name="受理结论验收放行">
<entryCriterion id="ec_rel" sentryRef="s_routed"/>
<itemControl><manualActivationRule id="m_rl"/></itemControl>
</humanTask>
</casePlanModel>
</case>
</definitions>`;
/**
* 案例说明书 —— **只写整单一以贯之的事**(2026-07-31 砍掉逐步做法)。
*
* 引擎每一拍已经把「当前这一步怎么做」单独摊开喂进去了(`INTAKE_STEPS[].guide`)。
* 说明书里再写一遍全部六步,是**两笔账同时亏**:
* · 钱 —— 同一段做法一拍烧两遍,而它是每拍都在的常驻上下文;
* · 准 —— 两份会分叉。实测就分叉过:这里写着 `S1_register` / `S2_ask_back` /
* `S3_normalize` 三个**案例模型里根本不存在的计划项**(真名是 S1_customer /
* S2_normalize),模型照说明书报 itemId,一报一个被拒。
*
* 所以留下的只有逐步做法**说不了**的那几件:这一单是什么、成品是什么、
* 一路上怎么跟人说话、怎么把一项干完、结案是什么。逐步的活归剧本。
*/
export const NARRATIVE = `§0 开案:**一封客户询价来函即开案**。你手上只有这封信的四样东西 ——
客户邮箱(customerEmail)、主题(subject)、邮件内容(body)、邮件附件(attachments,真文件)。
没有客户编码,没有询价单号:那两样是**受理出来的**,不是随信送来的。
真实 RFQ 五花八门 —— 俄文 BOM、客户内部码、字段东缺西缺都常见。
**受理这件事的成品只有一样**:一份能往下走的询价受理记录 —— 逐条落表、每条打了状况标、
两枚去向旗定了。SLA:受理<15 分钟,判定<45 分钟。判错状况,快单变慢单。
**整条路就这几步**,一步一个计划项,别绕(每一步的做法,轮到它时会单独摊给你,
这里不重复):S1 定位客户 → S2 归一受理 → **S3 受理需求交人审定** → S4 查历史价 →
S5 逐条定状况标 → S6 定去向与派单 → 提交放行。
**这一单要经过人两次,不多不少**:S3 人审定受理需求(阶段2 那张执行页,
字段齐不齐都要发)、放行时人签字。**别把 S3 当成缺东西时的追问** ——
不缺也发:拆错一格只有人看得出来,而那些错往后每一步都跟着跑。
**附件是真文件,基座已经替你读过一遍**。每一件长这样
\`{ name, contentType, size, ref, text?, note }\`:
\`text\` 有值就是**读出来的内容**(xlsx 读成制表符分列的行,第一行多半是表头),照它拆条;
\`text\` 没有就是这类文件读不出文本(\`note\` 里写着为什么,比如 pdf、扫描件),
这时以邮件内容为准。**别猜附件里有什么**:文件名叫 BOM 不等于里面有 BOM。
【一路上要说话】负责人(BD)正看着这一单。**每一拍都带一句 say** —— 你是在跟他当面说话,
不是播报进度。他明天要拿这些话去跟客户和采购讲,所以说的得是**业务的话**:
料号、交期、用量、价、客户要求;不是「关键字段」「归一化阶段」「写草稿 3 行」这些系统词。
一两句就够,口语,像同事扭头说一声。局面变了顺手用 view 换画面:
· S2 逐条落表后 → view = intake-overview。**先说这单是什么**,再说有没有坑:
「深圳机械这单三颗料,车规 Analog;交期用量都写清楚了,只有 NE555P 没给目标价。」
· S5 打标完 → view = intake-classified。**说结论对这单的影响**:
「LM358DR 有近期成交价可以直接报;NE555P 半年多没动过要重新核;TL072CDR 停产了,得找替代。」
· S6 定完去向 → view = intake-release。**说这单接下来往哪走、谁接手**。
**交代里说缺什么,用件上那三个词**(必填 / 建议 / 选填),而且**只说已发生的事**:
客户没给的就说「客户没给」并留空 —— 估价、垫数这类事你没有权限做,更不许在交代里
声称做过(见「三种分量」那段:目标价尤其如此)。历史价、三分类、去向是**审定之后**
那几步的事:这一拍它们连数都还没有,一个字都不要提。
派审定卡那一拍的交代照这个样子说(数字与料号换成本单的):
「三颗料拆好了,交期用量都齐;LM358DR、TL072CDR 客户给了目标价,NE555P 没给
(建议追问,不拦单)。整份受理需求请你审定,哪一格不对在工作区的件上就地改。」
别每拍一个腔调复读(「已完成X,现在进行Y」)。事情不同,话就该不同 ——
调工具那一拍说「我去查这三颗的历史价」,拿到结果那一拍说查出了什么,
两句话该是不一样的语气。有风险、要他拿主意、或者你打算叫人,**先说出来**,
别等他自己从表里看出来。不说话的后果是:他的对话区从头到尾空白,只看得见「多了一个任务」。
【怎么把一项干完 · 一拍连做】一个计划项常要**两三个动作**才交得出成果(先写记录、
再结算总数;先调工具拿判据、再把判据写进台账)。write 和 tool 都**不会**自动把那一项收掉 ——
都做完了,用 finish 宣告这一项完成。**这些动作排进同一拍的 actions 串里一口气给**,
别拆拍:「写 record、写 verdict、finish S1」是一拍,不是三拍;
「写 type_table 打标结果、写 verdict 统计、finish S5、写 alt/rfq 请求、写旗、finish S6」
能排一串就排一串。只有两处必须停拍:调工具要看出参(tool 放串尾,下一拍接着干);
发卡 / 提交会挂起(它们只能是串尾)。**这一单常路 5-7 拍就该收口** ——
每多拆一拍,就是多烧一遍完整上下文。
【受理需求审定卡的骨架 —— 哪一拍发都照这个,一个字段别少】
拆完受理记录就要派这张卡(S3_confirm)。**骨架放在说明书里正是因为你常在
「拆表 + 收步 + 派卡」同一拍里发它** —— 那一拍 S3 的剧本还没摊开,凭记忆拼出来的
就是一张只有一句话的裸卡(实测:人收到的指导书整个是空的)。照抄:
{"kind":"human","itemId":"S3_confirm","workOrder":"execution","card":{
"title":"受理需求审定:<单号> 共<N>条","ask":"<照当下情形写,见下>",
"steps":["型号 PN 与年用量 EAU 必须有 —— 缺一条这一单就走不到放行;客户没给的去问,问不到也要在备注里写清「客户未提供」",
"客户料号、品类、制造商、目标价这四格是建议追问 —— 不填也走得到放行,但那一问不能省;尤其目标价,有它才谈得上「报得进不进得去」,客户不肯给就问一句「贵司这颗现在的成交价区间」",
"客户没给的一律留空,由你判断要不要追问 —— 系统查到的历史价是分类依据,不是客户的目标价",
"改在工作区的件上:抽错的就地改,多出来的行按行首 × 撤掉,漏掉的用表尾「加一行」补上;改完保存,回这张卡提交"],
"standard":"必填(PN / 年用量)一条不缺;建议档(客户料号 / 品类 / 制造商 / 目标价)该问的都问过了 —— 问到的填上,问不到的留空并在备注里写明,放行时由你决定先受理还是退回。",
"basis":[{"k":"<缺格的 PN>","v":"缺年用量(必填,须补)"},
{"k":"<缺格的 PN>","v":"缺制造商(建议追问,可先受理)"},
{"k":"<缺格的 PN>","v":"客户未给目标价(建议追问,不拦单)"}]}}
· steps 与 standard 是**作业指导书**,卡上照原样摆给人看 —— 这一段每一单都一样,照抄别重写;
**卡面是纯文本**:里面别写星号那类记号,它们不会变粗体,只会原样摆在人眼前(实测过)。
· ask 照当下情形写:**有必填缺**写「有几条缺 PN/年用量,须向客户问清后填回」;
**只缺建议档**写「有几条缺建议追问的格子(客户料号/品类/制造商/目标价),可先受理也可去问」;
**都齐**写「请核对确认」。三档的词照件上那三个词说,别混用。
· basis 逐条写**谁缺什么、是必填还是建议还是选填**;一格不缺就写这一单的要点
(几条料、客户点名的口径要求)。
· **卡上不摆表、不摆格子**(填写全在工作区):整份清单就在工作区那份件上,
每一格能就地改、能增删行,缺哪几格件顶上的红带点着名。别往卡里塞 blocks 或 fields ——
塞了是把同一张表摆两处,人不知道改哪一份算数。
§Z 结案:BD 在验收单上签字,系统把草稿合并成正式记录并发出 intake 口的载荷。`;
/**
* **步骤剧本** —— 一个计划项一条,把「这一步是什么状态」写成机器读得懂的东西。
*
* 说明书说的是**整单一以贯之的事**(这一单是什么、怎么说话、怎么收口);
* 逐步的做法只写在这里,轮到哪一步才摊开哪一步 —— **两处不重复写同一件事**,
* 重复的代价是钱(每拍烧两遍)与准(两份会分叉,分叉过一次:说明书里的计划项名
* 是案例模型里没有的三个,模型照它报 itemId,一报一个被拒)。
*
* `doneWhen` 是这里的关键:它让引擎自己算得出「这一步的成果已经在台账里了」,
* 算出来就直接收掉,**不问模型**。A1 反复空转正是栽在没有它 ——
* S1 把八字段一次写齐之后,S3「补齐八字段」已经无活可干,而模型只能把同样三行
* 再写一遍,被「写了等于没写」拒回,下一拍再来一遍(实测连拒到熔断)。
*
* 写 `doneWhen` 的纪律:**它是「成果已存在」的判据,不是「该不该做」的判据**。
* 写成开局即为真的式子(如 `missing_key_total = 0`),那一步会在还没干之前就被收掉。
*/
export const INTAKE_STEPS = [
{
item: 'S1_customer',
intent: '按发件邮箱认客户;认不出就发数据录入卡请人建档,建完写进客户主数据',
writes: ['customer.profile', 'customer.contact', 'quote_intake.verdict'],
/* 认客户就靠这两张 —— 没声明的话它们会被整表段的截断刀砍掉(踩过) */
reads: ['customer.contact', 'customer.profile'],
view: 'intake-overview',
tools: [],
/*
* 认到人才算办完。**不能写 `customer_code != null`** ——
* S-FEEL 里未绑定的名字 `!= null` 求值为 true,这一步会开局即被收掉(A1 踩过)。
* 一律取同步写下的计数列。
*/
doneWhen: 'customer_total > 0 and (customer_new_total = 0 or customer_asked_total > 0)',
guide: `这一单的第一件事是**认人**:这封信是谁发来的。
① 先查客户主数据:拿入参的 \`customerEmail\` 去 \`customer.contact\` 里找 ——
找得到就取它那一行的 \`customer_code\`,这一步就完了(照下面第③条记账,不必问人)。
**邮箱一个字符都不许改**:用 \`customerEmail\` 的**原文**去比,别照签名里的姓名
重新拼一个(实测踩过:入参是 \`e.volkova@itelma.ru\`,模型按签名拼成
\`ekaterina.volkova@itelma.com\` —— 名字对、域名和缩写全错,于是老客户被当成新客户,
白发一张建档卡,还差点建出一个重复的客户档案)。
比对时忽略大小写与首尾空格,除此之外**逐字符相等才算认出**;
域名相同但用户名不同**不算认出** —— 同一家公司不同的人,是两个联系人。
② **找不到就是新客户** —— 发**数据录入卡**(不是补全卡)请人建档。
两者的分别在**谁写库**:补全卡收回来的值要你自己再 write 一遍(中间隔一道传话,
你可能抄错、可能顺手改一笔);数据录入卡是**人按下确认那一刻基座直接写**,你不经手。
客户档案是主数据,建错一次后面每一单都跟着错 —— 那一笔该由人亲手落下。
**建档要建两张表:公司档 + 联系人档,两段都摆上这张卡**(2026-08-02 用户定:
「新建客户 联系人和公司都需要确认」)。只摆公司那一半、联系人由你自己写,
等于让人确认了一半 —— 而联系人档里的 \`contact_code\` 恰恰是**你编出来**的那一个。
**照这个骨架发,一个字段都别少**(散文描述不管用,直接抄):
{"kind":"entry","itemId":"S1_customer","ask":"这是位新客户,请核对建档信息并给客户编码",
"ledger":"customer.profile","entryKey":{"customer_code":""},
"basis":[{"k":"发件邮箱","v":"chen@sh-elec.com"},{"k":"签名","v":"陈刚 | 上海精密电子有限公司"}],
"fields":[
{"key":"customer_code","label":"客户编码","required":true},
{"key":"name","label":"公司名","value":"上海精密电子有限公司","required":true}
],
"also":[{"title":"联系人档案","ledger":"customer.contact",
"key":{"customer_code":""},
"fields":[
{"key":"name","label":"联系人姓名","value":"陈刚","required":true},
{"key":"email","label":"邮箱","value":"chen@sh-elec.com","required":true}
]}]}
**联系人编码不用你写** —— 它是内部流水号,基座在人按下确认那一刻按「客户编码-01」生成
(台账声明里标了自增)。你硬塞一个进 \`fields\`,基座也会把它摘掉:
人哪知道这家客户已经有几位联系人,而你编一个就可能覆盖别人那一行。
**公司名与联系人姓名、邮箱都必须带 \`value\`** —— 公司名几乎总写在开头那句
「我是XX公司的张三」或结尾签名档里;联系人姓名从落款认;**邮箱照 \`customerEmail\` 原样抄,
一个字符都别改**。读出来填上,人核对一眼就行。
**客户编码不给 \`value\`** —— 那是我们内部的编码,来件里不会有,硬凑一个是错的;
标 required:true,人不填就确认不了。
两段的 \`customer_code\` 都留空:人在第一段填了什么,第二段跟着是什么(同一位客户)。
③ **记账两个数,引擎按它们收步**(别绕过去,绕不过去):
· customer_new_total —— 第①步认出来了记 0;认不出、要建档记 1;
· customer_asked_total —— 发过建档卡就记 1。
**新客户没问过人,这一步收不了** —— 实测有过一次:模型从邮箱域名
编出 \`CUST-SZ-MACH\` 直接把档建了,人根本没被问过一句,而那个编码是错的。
客户编码是主数据的主键,编错一次后面每一单都跟着错。
④ 人答完(或第①步就认出来了),同一拍把该写的写上:
· \`customer.profile\` 与 \`customer.contact\` —— **新客户这两张不用你写**:
人在录入卡上按下确认那一刻基座已经落好了(两段一次落两行)。
再写一遍只会被「写了等于没写」拒回。认出来的老客户更不必重写主数据。
· \`quote_intake.verdict\`:customer_code、**customer_name**(公司名,认出来的取主数据那一行,
新建的取卡上填的)、**contact_name**(发件人姓名,从落款或邮箱认;认不出就留空,别拿公司名顶)、
**customer_kind**(照第③条的判断写「老客户」或「新客户」四个字,
别的写法都不算 —— 报告上那一格与 A4 的首单口径都读它),并把 **customer_total 记 1**。
顺带把**询价单号 rfq_ref** 认出来记进 verdict:从主题或正文里原样照抄
(RFQ-xxxx / 询价单号 / Ref / PO 号),认不出就在②那张卡上一并问,**不要自己编** ——
所有台账都以 rfq_ref 为主键,编错了这一单的行会和别的单撞在一起。`,
},
{
item: 'S2_normalize',
intent: '解析邮件正文与附件 BOM,逐条落成受理记录;缺哪几格照实记数',
writes: ['quote_intake.record', 'quote_intake.verdict'],
reads: ['customer.profile'],
/* 拆料这一拍工作区就该摆出受理需求那份件 —— 人看得见 AI 拆到哪儿了 */
view: 'intake-overview',
tools: [],
/*
* **这一步不叫人**:拆料是 AI 的活,拆完就算办完,由下一步(S3_confirm)交给人审定。
*
* 从前这里既拆料又发卡,收步条件是
* `line_total > 0 and missing_key_total = 0 and (missing_minor_total = 0 or answered_total > 0)`
* —— 括号里那半句的意思是「缺东西才需要经过人」。于是一封字段齐全的询价进来,
* 条件当场为真,引擎在下一拍开头把这一步自己收了,人一眼没看过受理需求;
* 下一次被叫到已经是终局的验收放行。用户 2026-08-01 当场指出这件事。
*
* 现在这里只留「拆出行来了没有」:缺不缺字段不是收步的事,是卡上要说清的事。
*/
/*
* **拆出来几行,就报几条** —— 收步判据与验收判据同源(2026-08-01 加后半句)。
*
* 从前只看 `line_total > 0`:多写一行也照收,错要到第 10 拍提交放行时
* 才被 `record_rows = line_total` 拦住 —— 那时上下文早翻篇了,
* 模型只会去改数(实测:客户说本轮跳过的那颗料被写进有效清单,
* 改数又让 skipped_total 不对,连拒五拍熔断)。
* **同一个错,在第 2 拍发现比在第 10 拍便宜得多**:那时行刚写下、多的是哪一行一目了然。
*/
doneWhen: 'line_total > 0 and quote_intake_record_rows = line_total',
guide: `客户认出来了,现在把询价内容逐条落成受理记录。
【先看料在哪】**正文与附件分工不同,别只读一处**:
· 附件的 \`rows\` 有值 → **基座已经把那张表拆好了**:一行一个对象、键就是表头列名。
**照它拆条,别自己去数制表符** —— 数格子是基座替你做的事,你只管把列对到我方字段上。
(\`text\` 仍留着原文:口径要求那些不在表里的东西还得从正文与原文看。)
· 只有 \`text\` 没有 \`rows\` → 那张表不规整(列数对不齐),基座不敢替你拆;
这时才自己看原文,并且格外小心连着的制表符是「中间那一格空着」。
这时正文通常只给**口径要求**(币种/贸易条款/交期单位/认证/替代授权/本轮跳哪条),
那些不是料,是**落到备注与品质要求里的约束**。
· 附件没有或读不出 → 以正文为准。**别猜附件里有什么**。
【客户说不报的,就不要报】正文里点名「本轮跳过 / not finalised / 暂不报」的那几条,
**不进有效清单** —— 但要在收尾时说清「本轮不报几条、为什么」。
有效条数 = 附件总行数 − 客户点名跳过的条数,两个数都要说得出。
**跳过的那条连一行都别写进 \`record\`**:实测栽过一次 —— 数报对了(9 有效 / 1 跳过),
行却写了 10 条,判据 \`record_rows = line_total\` 当场拦住,而那时改数字是改不对的
(改了 skipped_total 又不对)。真写进去了就用 \`drop\` 把那一行撤掉,别去动数。
【表头怎么对】BOM 的列名常是客户本国语言或缩写,**先把列对上再拆行**:
· 年用量那一列叫 EAU / Qty / 年用量 / Годовой объём;**带 (k) 就是千**(30 → 30000);
· 制造商叫 Manufacturer / Производитель;品类叫 Category / Категория;
· 客户自己的编码常在第一列(Itelma Code / Customer P/N)→ 落 customer_pn。
**对不上列就别猜**:宁可把那一列整列判为缺,也不要把「认证要求」当成「年用量」。
【制表符分列:空格子会连着出现,别把后面的值往前挪】
一行里连着两个制表符(\`\\t\\t\`)就是**中间那一格空着**,不是没有这一格。
拆每一行前先数:**这一行的格子数必须与表头的列数相同**,数不对就整行重数,
别把右边的值挪到左边的空位上 —— 实测栽过两次,同一行:
制造商那一格空着,于是「浙江嘉康」被读成上一行的厂牌、品类也跟着错位成「集成电路」,
而 BOM 上白纸黑字写着 Антенна(天线)与浙江嘉康。
行尾连着几个制表符也一样:那是**末尾几列都空着**(年用量、目标价客户没给),
不是这一行到此为止 —— 末尾空列照样要算进格子数里。
列对上了就**逐行照抄值**,别整列判缺 —— 实测栽过一次:9 条料的年用量明明都在表里,
却被整批报成「缺年用量」,一张卡问了客户 9 个他早就给过的数。
【一张卡别铺太长】缺口卡的 basis 与表格**只列真正缺的那几行**,不要把全部条目铺进去:
整份回复有长度上限,铺太长会在半途被截断 —— 截断的 JSON 解析不了,
这一拍作废、还会弹一张「需要你判断下一步」把人叫来(实测)。缺几条就写几条。
【三种分量 —— 与件上的空占位、执行卡的指导书**逐字同一套话**】
台账声明里怎么标,件上就是什么颜色,你说话就得用哪个词。三档只有这三个词:
· **必填(只有这两项)**:\`pn\`、\`eau\` 年用量 —— 缺了这一单没法报价,
**这是验收标准**,不齐不许放行(件上标红「必填」),记进 missing_key_total。
· **建议(值得追问,但不拦单)**:\`customer_pn\` 客户料号 · \`category\` 品类 ·
\`brand\` 制造商 · \`target_price\` 目标价 —— 声明里标了 \`expect\`,件上标橘「建议」。
缺了照样往下走,但**那一问不能省**:执行卡上要写清「这几格建议追问」。
· **选填(其余一切)**:交期、品质要求、备注…… 件上标灰「选填」,缺了只当提醒。
建议档与选填档一并记进 missing_minor_total。
**别把「建议」说成「选填」**(2026-08-03 用户当场指出):同一件事,工单上标着「建议追问」,
交代里若换成另一档的叫法,人会以为这两处说的不是一回事 —— 而它们就是一回事。
**目标价客户没给就留空,不许估**:它是**客户的意向价**,不是我们算得出来的数。
S4 查到的历史价是**判 A/B/C 的依据**(距上次报价几天、行情稳不稳),
**不是客户的目标价** —— 拿它填进 \`target_price\` 就是替客户表了态,
下游定价、议价全建在这个假数上。也不许只在**交代里声称**估过价、垫过数
(实测栽过一次:台账留空是对的,嘴上却说按历史价估了 —— 而那一步连历史价都还没查,
纯属编造,人读了就以为表里躺着一个估出来的数)。留空、在件上标橘「建议」、
在执行卡上写明建议追问,这三件事合起来才是对的处置。
**目标价常缺是常态,不是缺陷** —— 实测栽过一次:一颗料只缺目标价却被记成必填缺、
还派了执行卡去问客户,而按 F-01 它根本不该拦单。
拿不准某个字段算哪一档,就问自己一句:**它是不是 PN 或年用量?** 不是就不许拦单。
【怎么拆】逐条写进 \`quote_intake.record\`,一条需求一行(同一 PN 不同数量档算两行)。
列名照台账字典写,别自己起名:
· \`pn\` 我方料号 · \`customer_pn\` 客户自己的编码(BOM 里那一列,回价单要按它对齐)
· \`category\` 品类 · \`brand\` 制造商 · \`eau\` 年用量 · \`target_price\` 目标价
· \`quality_req\` 认证/品质要求 · \`remark\` 备注 · \`customer_code\` 上一步认出的那个
四条纪律:
① **原文没写的不要补** —— 缺就是缺,记进缺口数,别替客户填一个「看起来合理」的值。
数值列(年用量 / 目标价)没有就给**空字符串 \`""\`**,基座认得出「这一列没有值」,
报告上会标成红的「必填」等人补。**绝不写 0** —— 0 是一个真实的数量,
写下去就等于告诉下游「客户要 0 个」;实测栽过:DAG 那条 BOM 里年用量本来是空的,
被写成 eau=0,而同一拍的缺口数还记着 1 —— 一份自己跟自己打架的记录。
② **PN 一个字符都不许改**:后缀、横杠、空格全照抄
(实测:\`DAG2400L321504XZB01-R0\` 被写成 \`DAG2400L321504XZB01\`,尾巴上的 -R0 丢了 ——
往后查历史价查不到、开出去的询价请求对不上料,而错处早已在这一步埋下)。
③ **缺哪一格算哪一格,别整行判缺**:同一行里有值的列照抄
(实测:那条缺年用量的料,连 BOM 里明明写着的制造商「浙江嘉康」与品类都一并没写 ——
人拿到卡以为客户什么都没给,白问一轮他早就给过的东西);
④ 单位归一但不换算口径(12K→12000 可以;月用量×12 是业务判断,该问客户);
③ **外文品类照译不照抄**:BOM 常是客户本国语言(Варистор = 压敏电阻,
Антенна = 天线,Дроссель = 电感,Фильтр = 滤波器,Микросхема = 集成电路),
译的是**品类那一列的词**,不是按料号猜这颗料是什么
(实测:一颗天线被写成「集成电路」—— BOM 上白纸黑字写着 Антенна);
译不准的原文照留,别硬编一个。
④ 正文里的口径逐条落到对应行:点名要证书的写进 \`quality_req\`;
客户**已预授权的替代件**写进 \`remark\`(那是后面选型的依据,丢了就得回头问客户)。
【同一拍记账】把 \`quote_intake.verdict\` 的这几个数写上:
· bom_total = 来件一共几条(附件行数,不含表头);
· skipped_total = 客户点名本轮不报的条数;skipped_note = 是哪几条、为什么(一句话);
· line_total = 落了几条(= bom_total − skipped_total,对不上就是拆漏了);
· **missing_key_total** = 缺 PN 或 年用量 的条数(必填缺);
· **missing_minor_total** = **除 PN / 年用量之外任何字段缺了**的条数(选填缺)——
客户料号、品类、制造商、目标价、交期、品质要求、备注,缺哪一个都算,一条只数一次。
**别只数品类和制造商**:实测栽过一次,NE555P 只缺目标价,两个缺口数报的都是 0,
于是那一问被整个吞掉 —— 人在成果表里看见一格空白,却没人问过他一句。
空不空由他决定要不要去补,不由你替他吞掉。
【最要紧的一条:**数就是数,不许为了过判据去改它**】
两个缺口数如实写。缺一条就是 1,别因为「写 0 才好往下走」就写 0 ——
实测栽过一次:一颗料只缺目标价,如实记了 1,提交被判据拦住,
于是那个数被改回了 0、单子放行了,而台账上从此写着「缺选填 0 条」,
那一格空白再没人提起。**判据拦住你,说明该去办那件事,不是该去改那个数**;
真办不了(客户就是不给),把它留在数里、在 say 里说清楚,放行时由人定。
**这一步不发卡**:拆完就 finish,交给下一步(受理需求交人审定)去请人。`,
},
{
item: 'S3_confirm',
intent: '把整份受理需求交人审定:可就地改,确认后才往下走',
writes: ['quote_intake.verdict'],
reads: [],
/* 阶段2 的执行目标件:询价受理需求(照 JCAI_PRD 的 A1-询价受理需求.html) */
view: 'intake-overview',
tools: [],
/*
* **不给 doneWhen** —— 这一步触达世界(把件交到人手上),给了就等于给引擎
* 一把替人签字的钥匙。而它的成果按定义也判不出来:「人看过、改过、认了」
* 不是台账里任何一个数能自己变出来的。人工激活也一并给上:
* 引擎不会替你按下这一步,也不会替你收 —— 发卡就算启动,收要你自己 finish。
*/
guide: `拆好的九条摆在那儿了,**这一单现在要经过人的手**。
【一单必发一张,不管缺不缺】
这不是「缺东西时的追问」,是**阶段2 那张执行页**:AI 拟了稿,人审定。
字段齐全也要发 —— 拆错一格(PN 少个后缀、品类译反、年用量看错列)只有人看得出来,
而那些错往后每一步都跟着跑。**别因为「没有缺口」就跳过这一步**:
实测栽过一次(2026-08-01),那一版只在缺东西时才发卡,于是一封齐整的询价进来,
人一眼没看过受理需求,下一次被叫到已经是终局验收 —— 用户当场说
「我都没确认需求 它就让我验收了」。
【怎么发】**卡的骨架在说明书里(「受理需求审定卡的骨架」那一段),逐字段照抄** ——
不管你在哪一拍发它,骨架都在说明书里等着,不要凭记忆拼一张裸卡。
这一步人工激活:发卡就算启动,不必单发 start;引擎也不会替你收 —— 人交回后要你自己 finish。
发完卡把 **asked_total 加 1**(记账用:它证明这一问没省)。
【人提交之后 —— **件上的改动基座已经替你写回了**】
人在件上按「保存」那一刻,改动就落进了草稿层(与数据录入卡同一条规矩:
该由人亲手落下的,不经你的手);他按「提交」只是把这张卡交回来。
所以**别再 write 一遍那几行** —— 值没变的写入会被「写了等于没写」拒回(实测)。
你这一拍只做三件事,然后 finish:
① **把 missing_key_total / missing_minor_total 重算** —— 照台账里现在的值,
补上了就该变小(基座写的是行,汇总列是你的活);
② **confirmed_total 记 1** —— 这一单经过人了。**不写它这一步收不了,整单卡在这儿**;
③ answered_total 加 1(答案已经处理过了)。
漏掉①的下场实测过:值明明补上了,汇总列还写着「缺 1 条」,提交被判据一次次拦回来,
连拒五拍熔断 —— 而你只要把那个数改对就过去了。
真要补写某一行时**按台账里那一行的 PN 对行**:PN 是主键,
换一个写法就是**新开一行**(实测:补写时把 PN 补全成 \`…-R0\`,于是同一颗料在表里成了两行,
条目数说 9 而表里躺着 10)。真发现第一次的 PN 抄错了,在 say 里说明并交给人,
别自己用新 PN 再写一行。
【**这张卡一单只发一次**】人交回来之后,不论还缺什么,**都不许再发同一张卡**。
必填(PN / 年用量)有提交闸兜着:缺一条他根本按不下提交,所以卡交回来时必填一定是齐的;
建议档的缺口不拦单,留着交放行的人定。
实测栽过多次(2026-08-02 那一单连撞四拍):从前这儿写着「人交回来还缺必填,就再发一张卡」——
而提交闸上线后那条路已经不存在了,模型照它办,每次都撞在
「这件事刚问过,人也答过了」上,白烧四拍。
【人把格子留空交回来 = **客户给不了**,那也是一个答案】
这时候要做的**不是再问一遍**(基座会当场拒:同一件事不许问第二遍),而是:
· confirmed_total 记 1、answered_total 记 1(人已经处理过了);
· 缺口数**照实留着**(缺就是缺,别改小);
· 这一步就算办完了 —— 建议档的缺口不拦单,放行时由人定「先受理还是退回」。
实测栽过一次:人留空交回,模型又发一张一模一样的卡,被拒;
转头把同一行原样再写一遍,又被「写了等于没写」拒;连拒五拍熔断,一单就这么停了。`,
},
{
item: 'S4_history',
view: 'intake-classified',
intent: '调历史价工具,把 P-03 的三项判据取齐并落进料号分类表',
writes: ['quote_intake.type_table'],
reads: [],
tools: ['crm.history-price-read'],
/*
* 判据落表了才算取齐。**不能写 `history_days != null`** ——
* S-FEEL 里未绑定的名字 `!= null` 求值为 true,那一步开局就被收掉(踩过,测里拦下的)。
* 取正向判断:没写过就没有这个键,`>= 0` 自然为假。
*/
doneWhen: 'history_days >= 0',
guide: `P-03 只认三个入参:距上次报价天数、行情是否稳定、生命周期状态。
三者齐了结论是**算出来**的,所以这一步唯一该做的事是把判据取齐:
调 \`crm.history-price-read\`,**一次把这一单所有 PN 传进去**(参数 pns 收数组,
别一条一条调 —— 每调一次就多烧一拍)。取不到不是错误:新料号无历史会回
historyDays=9999、hit=false,那本身就是判据,命中 B 类「重估后报」是对的。
拿回来的三项判据**原样逐条**写进 \`quote_intake.type_table\`。
**这一步一个数都不许自己填**(实测踩过,三条同时犯):
① 工具回什么就写什么 —— historyDays / marketStable / lifecycle 三个值**逐字照抄**。
你不知道某颗料的行情,工具知道;你估一个「15 天 · 在产」,判定表就照着你的估算出 A,
而那颗料实际是客户点名要替代的 C。**估出来的判据比没有判据更坏**:它看起来很像真的。
② **PN 必须原样**,一个字符都不许截、不许改写(实测:\`DAG2400L321504XZB01-R0\`
被写成 \`DAG2400L321504XZB01\`,尾巴上的 -R0 丢了 —— 工具当场查不到,
于是你又给它编了一组判据)。传进去的 PN 与写回台账的 PN 必须与受理记录逐字一致。
③ 工具回 hit=false(这颗料没有历史)时,**照它回的 9999 / false / 未知 写**,
那三个值本身就是判据,命中 B 类「重估后报」是对的结论 —— 不是缺数据,不必替它补。`,
},
{
item: 'S5_classify',
view: 'intake-classified',
intent: '调分类器按 P-03 给每条打 A/B/C,标落两张表,条数照出参抄',
writes: ['quote_intake.type_table', 'quote_intake.record', 'quote_intake.verdict'],
reads: [],
tools: ['quote-intake.classify'],
/*
* **收步判据与验收判据同源**(2026-08-01 改)。
*
* 从前只看 `untyped_total = 0` —— 那是活动自己写的数。它归零了这一步就收,
* 而验收那条比的是 `type_table_rows = record_rows`(基座数出来的行):
* 数归零了、表没写全,于是**收了步却过不了验收**,模型得回头补写一张已经收掉的表。
* 收步比验收松,松出来的那一截就是返工。这里补上基座那一边:两处比同一件事。
*/
doneWhen: 'untyped_total = 0 and line_total > 0 and quote_intake_type_table_rows = quote_intake_record_rows',
guide: `调 \`quote-intake.classify\`(纯工具,P-03 的求值器,同输入必同结论、零 token),
把 type_table 那几行判据一次传进去,拿回每条的 A/B/C 与打法。
【标要落**两张表**】
· \`quote_intake.type_table\`:type_tag + basis —— 这是「凭什么这么判」的底稿;
· \`quote_intake.record\`:**同一个 type_tag 也要写回受理记录**(按 PN 对行)——
受理记录才是流去下游的那份交付物,A4 就靠这一列决定哪几条能直接取历史锚点。
漏了这一步,报告上的状况标那一列整列是空的,而下游拿到一份没打标的清单(实测)。
【条数**照出参抄,不许自己数**】classify 的出参里带 \`counts\`(A/B/C 各几条)与 \`total\`,
照它写 verdict 的 type_a_total / type_b_total / type_c_total,并把 untyped_total 归零。
实测数错过一次:判定器给的是 5A/3B/1C,报上去的是 7A/2B/1C,派单也跟着按错的数开 ——
而验收那条等式两边都是自报的,于是「2 = 2」顺利过关,真实的第三条 B 类料一张单也没开。
**不要绕过决策表自己判** —— 规则表可回放、可追责,临场判断不是。
\`type_tag\` 一律取出参里的那个字母,**不许自己写一个**:实测有过一次,
它自己给出的判据是「200 天 · 行情不稳」(按 P-03 该判 B),写回台账的标却是 A ——
判据与结论对不上,而验收只看结论,这种错一路走到报价单上都没人拦。
拿不准的一律降 B,不许臆断 A:错标 A 会让一条本该重新核价的料带着旧价走到报价单上。`,
},
{
item: 'S6_route',
/* 阶段3 的验收目标件:询价受理记录(照 A1-询价受理记录.html) */
view: 'intake-release',
intent: '按三类结果立两枚去向旗',
writes: ['quote_intake.verdict'],
reads: [],
tools: [],
/*
* **这一步不再开单**(2026-08-02 用户当场定):
* 「替代评估请求 / 询价请求这两项数据不在这里产出;随令牌流向下一个节点的
* 只有工作成果验收内的内容;进入选型替代后,活动会自己归一需要的数据。」
*
* 从前 A1 逐条开 `alt_request` / `rfq_request` 两张单交给 A2 / A3 ——
* 那是替下游把它的输入先摆好,而下游本来就要照自己的口径归一一遍:
* 同一份事实在两个活动里各存一份,改一处就分叉。A1 交出去的是**受理记录**,
* 下游读它即可。
*
* 收步判据随之改成「两枚旗都立了」。旗是布尔,合法值含 false,
* 拿它判「写没写过」分不清「没写」与「写了 false」—— 所以仍取同步写下的**计数**
* (`route_decided_total`:定完去向记 1),并挂 `untyped_total = 0` 当前置:
* 两个数都得是模型显式写过的,否则「未绑定的名字相等」会让这一步开局即收
* (包内自测那条「doneWhen 开局就为真」拦过这个)。
*/
doneWhen: 'untyped_total = 0 and route_decided_total > 0',
guide: `状况标是判断的**输入**,不等于去向。照三类的结果立两枚旗:
· 有 C 类(EOL / 缺货 / 客户点名替代)→ need_alternative = true,这一单要走替代选型;
· 有 B 类(无近期历史价或行情波动)→ need_sourcing = true,这一单要走寻源询价;
· 两者可同时成立 —— 选型定案后仍要为替代料寻价。
写完把 **route_decided_total 记 1**(记账用:它证明去向定过了,引擎按它收步)。
**不要开什么请求单** —— 替代评估请求、询价请求都**不在这一步产出**:
随令牌流去下一个节点的只有**工作成果验收里的那份受理记录**,
A2 / A3 拿到之后会按自己的口径归一需要的数据。你替它们先摆一份,
等于同一份事实在两个活动里各存一份,改一处就分叉。
**这两枚旗是下游两道网关唯一读的东西**:不写,整条流出了受理口就没路可走。
**去向不用你选,也不用人选** —— 契约给三个出口各写了判据(有没有 C 类、有没有 B 类),
放行那一刻基座按序求值自己定,卡上只如实告诉人走哪一口、凭哪一条。
你要做的只有一件:**把三类条数与两枚旗写对**(它们正是那几条判据读的数)。
旗写错,算出来的去向就错 —— 但那是数据错,不是选择错。`,
},
{
item: 'T_release',
view: 'intake-release',
intent: '判定成果已齐,提交放行交 BD 验收',
writes: [],
reads: [],
tools: [],
// 放行触达世界,必须人按一下 —— 引擎不自动收它(剧本里也不给 doneWhen)
guide: `line_total > 0、missing_key_total = 0、untyped_total = 0、两枚旗已定 ——
可以走放行了(T_release 人工激活:动它就算启动,不必单发 start)。
**submit 之前,把清单里**每一条**裁量判据(L3/L4)都裁掉**。
**一条都不能漏** —— 漏一条,submit 当场被拒,
而拒回来的理由里就写着漏的是哪一条:照它原样抄 criterion 再裁一次即可,
**别原样重提同一个 submit**(实测连提七次,每次都被同一条拒回来)。
眼下**只剩一条**,照名字逐字抄(2026-08-02 起「吃不准降 B」那条已从清单里删掉,
别再裁它 —— 裁一个不存在的判据会被拒):
· judge criterion="建议追问的四格(客户料号/品类/制造商/目标价)已补齐,或由你放行时确认可以先受理" ——
**只看 \`quote_intake_record_gaps\` 这一个数**(基座现数的:还有几行缺**建议追问**的格子;
\`..._gaps_keys\` 写着缺在哪几行的哪一格)。就两种写法,没有第三种:
· **= 0** → satisfied=true,rationale 写「建议追问的那几格都补齐了」;
· **> 0** → satisfied=**false**,rationale 照 \`..._gaps_keys\` 逐条写清缺在哪儿
(例:「DAG2400L321504XZB01-R0 缺目标价」),交放行的人定「先受理还是退回」。
**「我发过卡问过了」不算满足** —— 发卡是过程,不是结论;而这一单本来就必发一张
审定卡,拿它当理由的话这条判据永远是绿的,等于没有这道门
(实测:客户没给目标价,它照样裁了满足,人签字时根本不知道还缺着)。
裁成不满足**照样能提交**,理由连着卡一起交给签字的人 —— 那正是这条判据的用处。
**不许为了让它绿去改哪个数** —— 那个数你也改不动,它由基座现数。
**criterion 照判据名原样写**(整句,含括号里那几个字),写别的名字对不上,那一条仍算未达标。
**去向不用你裁**:三个出口各写了判据,放行那一刻基座按序求值、首个命中即出 ——
那是算出来的,不是判出来的。从前这里还有一条「去向判断合乎业务常理」,
那是让你给一个自己没做的决定盖章:裁「满足」是废话,裁「不满足」也改不动什么。
C 类开没开替代评估请求、B 类开没开询价请求,由两条机检判据当场对账。
这一条是机器判不了的(L4 独立评审:缺口真还在时,「先受理还是退回」只有人能定),
只能由你当场裁并留下理由 —— **不裁就提交会被当场拒**:还有裁量判据没裁过时,
提交等于把没办完的活推给人。裁完再 submit,BD 验收。
(缺口为 0 时基座当它已达标、不必你裁;裁了也不碍事。)
终局唯放行:你不能自己宣布完成。
验收报告上让 BD 当场改的那两个值(两枚去向旗)由**契约声明**(revisableAtRelease),
基座放行前从台账现读拟值 —— 你不用在 submit 上重述一遍,写好台账就行。`,
},
];
/**
* 阶段 —— **进度条的刻度**,只给人看,引擎一眼都不看。
*
* 六个计划项是引擎的粒度(拆到能判「干完没有」);阶段是人的粒度。
* 「查历史价 / 逐条打标 / 定去向」在业务上是**一件事** —— 判这单该怎么走,
* 分成三项是因为引擎要一项一项收,而人只想知道「到判去向这一步了」。
*
* 只列**激活态**的三段。放行不在这儿:每个活动都以放行收口(铁律③),
* 那是系统的规矩不是 A1 的阶段;进度条自己在末尾加那一格,所以人看到的是 x/4。
*/
/*
* 给人看的刻度 —— **每个计划项都得有格可待**(2026-08-02 用户:「卡片的进度条走的不对」)。
*
* 从前这份名单漏了 `S3_confirm`(它是 2026-08-01 才补回成一项的):
* 计划项有六个,名单里只有五个。于是 S2 一收,前两格全过,进度条当场跳到「判定去向」——
* 而人这会儿正对着受理需求审定那张卡。**他手上办的那件事在条上没有格子**,
* 条却已经走过去了:那不是走快了一格,是它在说一件没发生的事。
*
* `T_release` 不在名单里是**对的**:每个活动都以放行收口,那一格由基座统一加
* (`CLOSING_PHASE`)—— 活动自己给它起名反而会各叫各的。
* 包内自测钉死这条:除放行外每一项恰好属于一个阶段。
*/
export const INTAKE_PHASES = [
{ name: '定位客户', items: ['S1_customer'] },
{ name: '归一受理', items: ['S2_normalize'] },
{ name: '交你审定', items: ['S3_confirm'] },
{ name: '判定去向', items: ['S4_history', 'S5_classify', 'S6_route'] },
];