人手改的那一手真正落得下去;界面不撒谎那几处补齐

手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
2026-08-02 13:41:44 +08:00
co-authored by Claude Opus 5
parent 94f1c7a4ef
commit 259192f736
228 changed files with 27850 additions and 2847 deletions
+182 -173
View File
@@ -1,9 +1,22 @@
/**
* 四域十九对象(docs/MVP示例工作流.html · 动态数据 + 静态底座;定价审批表落审批机制,不入台账)。
* 主数据四域 —— **客户 / 项目 / 供应商 / 市场**(2026-07-31 用户定,照常规 CRM & ERP 的分法)。
*
* 划分原则(产品决策,2026-07-27):**流转单据(过程)归 project;沉淀资产按内容归
* customer / intel / supplier**。声明只写活动**要用**的字段;物化走安装通道的声明合并
* (最大化合并、只增不破坏),这里不建表 —— 建表是 ledger 的事。
* 为什么主数据要单独立域、由**平台**播种,而不是跟着某个活动走:
*
* 台账原来只按「谁写的」分域(`quote_intake.*` / `quote_alternative.*` …)。那是**实现轴**:
* 六个活动各有一张 verdict,一单的故事散在六处;而真正需要治理的那几张
* (客户档案、历史价)被挤进一个叫 `shared` 的抽屉 —— 那个名字描述的是机制(跨活动共享),
* 不是性质(主数据)。用机制命名,抽屉必然变杂物间。
*
* 更要紧的是**所有权**:主数据不是任何一个活动的产出。客户档案先于活动存在、
* 活动退役了它还在;活动只是**读它做判断、提议更新它**。
* 所以它由平台播种物化,不挂在谁的清单下 —— 活动装了卸了,主数据都在。
*
* 与三类性质的关系(见 `ledgerObjectKindSchema`):这里全是 `master`。
* 单据(`record`)归各活动自己的域;派生指标(`derived`)另说 —— 它本不该手写。
*
* **现在是空占位**:结构先立起来,还没有活动往里写。
* 表建出来但一行数据都没有 —— 治理台「主数据」那一区能看见它们等着被接上。
*/
import { objectDeclarationSchema } from '@contracts';
import type { LedgerFieldType, ObjectDeclaration } from '@contracts';
@@ -16,184 +29,180 @@ const f = (name: string, type: LedgerFieldType, label: string, primaryKey = fals
masked: false,
});
const obj = (domain: string, object: string, label: string, fields: ReturnType<typeof f>[]) =>
objectDeclarationSchema.parse({ domain, object, label, fields });
/** 主数据一律 `kind: 'master'`:按业务实体分行,没有单标识 */
const master = (domain: string, object: string, label: string, fields: ReturnType<typeof f>[]) =>
objectDeclarationSchema.parse({ domain, object, label, kind: 'master', fields });
export const MVP_OBJECTS: readonly ObjectDeclaration[] = [
/* ── project:每一单的流转单据(位置签 输入/过程/输出) ─────────────── */
obj('project', 'rfq_file', 'RFQ/BOM文件', [
f('rfq_ref', 'text', '询价单号', true),
f('pn', 'text', 'PN', true),
f('qty_eau', 'text', '数量/EAU'),
f('brand', 'text', '厂牌'),
f('analog_flag', 'boolean', 'Analog'),
f('aec_cert', 'text', 'AEC认证'),
f('target_price', 'number', '目标价'),
f('lead_time', 'text', '交期'),
]),
obj('project', 'intake_record', '统一受理记录', [
f('customer_code', 'text', '客户码', true),
f('pn', 'text', 'PN', true),
f('brand', 'text', '厂牌'),
f('eau', 'text', 'EAU'),
f('type_tag', 'text', 'Type标'),
f('target_price', 'number', '目标价'),
f('status', 'text', '状态'),
]),
obj('project', 'pn_class', '料号分类表', [
f('pn', 'text', 'PN', true),
f('type_tag', 'text', 'Type标'),
f('basis', 'text', '分级依据'),
f('history_hit', 'boolean', '历史价命中'),
f('review_note', 'text', '复核结论'),
]),
/*
* 受理结论 —— **一单一行**(主键只有询价单号),不是一料一行。
*
* 它承载 A1 出口的两枚去向旗。为什么必须单独一个对象、不能挂在按料号分行的
* 「统一受理记录」上:出口载荷的标量字段取自 `landed.flat`,而 `flat` 是把这一单
* **所有**落地行 reduce 展开成一个平对象(`wiring/engines.ts`)。旗挂在按料号分的行上,
* 六个料号就有六行同名列,**最后合并的那行赢** —— 而归并规则是「任一条命中 C 即需选型」,
* last-write-wins 会静默给出错的去向,网关照着错值分叉,没人看得出来。
*
* 一单一行就没有这个歧义:G1 / G2 读的是同一行的同一个值。
* 逐料号的判定仍写「料号分类表」,这里只放归并后的结论。
*/
obj('project', 'intake_verdict', '受理结论', [
f('rfq_ref', 'text', '询价单号', true),
f('need_alternative', 'boolean', '需选型'),
f('need_sourcing', 'boolean', '需寻源'),
f('c_count', 'number', 'C类条数'),
f('status', 'text', '状态'),
]),
obj('project', 'alt_request', '替代评估请求', [
f('material', 'text', '初选物料', true),
f('chip_type', 'text', '芯片类型'),
f('spec_params', 'text', '参数规格'),
f('scenario', 'text', '应用场景'),
f('requester', 'text', '需求人'),
]),
obj('project', 'inquiry_request', '询价请求', [
f('pn', 'text', 'PN', true),
f('brand', 'text', 'Brand'),
f('era', 'text', 'ERA'),
f('project_stage', 'text', '项目阶段'),
f('accept_alt', 'boolean', '接受替代'),
f('unit_usage', 'text', '单机用量'),
f('tp', 'number', 'TP'),
f('scenario', 'text', '场景'),
]),
obj('project', 'supplier_floor', '供应商底价表', [
f('pn', 'text', 'PN', true),
f('supplier', 'text', '供应商', true),
f('floor_price', 'number', '底价'),
f('moq', 'text', 'MOQ'),
f('lead_time', 'text', '交期'),
]),
obj('project', 'alt_benchmark', '替代对标表', [
f('original_pn', 'text', '原型号', true),
f('candidate_pn', 'text', '候选型号', true),
f('alt_grade', 'text', '替代等级'),
f('param_diff', 'text', '参数差异项'),
f('diff_risk', 'text', '差异风险'),
f('mod_eval', 'text', '改动评估'),
f('verify_advice', 'text', '验证建议'),
]),
obj('project', 'sourcing_compare', '寻源比价表', [
f('pn', 'text', 'PN', true),
f('supplier', 'text', '供应商', true),
f('quote_price', 'number', '报价'),
f('moq', 'text', 'MOQ'),
f('lead_time', 'text', '交期'),
f('channel_grade', 'text', '渠道等级'),
f('condition', 'text', '成色'),
f('verdict', 'text', '结论'),
]),
obj('project', 'quote_draft', '报价草案', [
f('pn', 'text', 'PN', true),
f('proposed_price', 'number', '拟报单价'),
f('margin', 'number', '毛利率'),
f('deviation', 'number', '偏离度'),
]),
obj('project', 'formal_quote', '正式报价单', [
f('pn', 'text', 'PN', true),
f('unit_price_cny', 'number', '单价CNY'),
f('lead_time', 'text', '交期'),
f('spq', 'text', 'SPQ'),
f('moq', 'text', 'MOQ'),
f('trade_terms', 'text', '贸易条款'),
f('valid_until', 'date', '有效期'),
]),
obj('project', 'followup', '跟进提醒', [
f('quote_no', 'text', '报价单号', true),
f('customer_code', 'text', '客户'),
f('d3', 'date', 'D+3'),
f('d7', 'date', 'D+7'),
f('reply_status', 'text', '回复状态'),
]),
obj('project', 'landing_stats', '报价落地率统计', [
f('period', 'text', '期', true),
f('landing_rate', 'number', '落地率'),
f('type_a_hit', 'number', 'TypeA命中'),
f('d3_reply_rate', 'number', 'D+3回复率'),
f('return_rate', 'number', '退回率'),
]),
/* ── customer:客户信息 ────────────────────────────────────────────── */
obj('customer', 'profile', '客户资料库', [
f('customer_code', 'text', '客户码', true),
export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [
/* ── 客户:谁在买 ──────────────────────────────────────────────────── */
master('customer', 'profile', '客户档案', [
f('customer_code', 'text', '客户编码', true),
f('name', 'text', '客户名称'),
f('grade', 'text', '客户等级'),
f('tags', 'text', '画像标签'),
f('payment_terms', 'text', '付款条款'),
f('industry', 'text', '行业属性'),
f('risk_level', 'text', '风险等级'),
f('annual_purchase', 'number', '年采购额'),
f('industry', 'text', '所属行业'),
f('region', 'text', '区域'),
f('owner', 'text', '负责 BD'),
f('payment_terms', 'text', '付款条件'),
f('credit_limit', 'number', '信用额度'),
/* 画像标签:价格敏感度、决策周期、常用条款 —— A7 归档时回写的就是这一列 */
f('profile_tags', 'text', '画像标签'),
f('status', 'text', '状态'),
]),
master('customer', 'contact', '客户联系人', [
f('customer_code', 'text', '客户编码', true),
f('contact_code', 'text', '联系人编码', true),
f('name', 'text', '姓名'),
f('title', 'text', '职务'),
/* 邮箱与电话是个人信息 —— 主数据里必须标脱敏,治理面照此裁剪 */
{ ...f('email', 'text', '邮箱'), masked: true },
{ ...f('phone', 'text', '电话'), masked: true },
]),
master('customer', 'price_terms', '客户价格条款', [
f('customer_code', 'text', '客户编码', true),
f('category', 'text', '品类', true),
f('target_margin', 'number', '目标毛利'),
f('discount', 'number', '折扣'),
f('valid_until', 'date', '有效期至'),
]),
/* ── intel:市场与销售情报 ─────────────────────────────────────────── */
obj('intel', 'history_price', '历史价格情报', [
f('pn', 'text', 'PN', true),
f('quoted_at', 'timestamp', '报价日期', true),
f('brand', 'text', '厂牌'),
f('unit_price_cny', 'number', '单价CNY'),
f('lead_time', 'text', '交期'),
f('moq', 'text', 'MOQ'),
f('valid_until', 'date', '有效期'),
/* ── 项目:为什么买 —— 客户的产品项目,反复询价的载体 ───────────────── */
master('project', 'profile', '项目档案', [
f('project_code', 'text', '项目编码', true),
f('customer_code', 'text', '所属客户'),
f('name', 'text', '项目名称'),
/* BOM 早期 / BOM 后期 / 量产 —— A3 的询价打法就按这个分 */
f('stage', 'text', '项目阶段'),
f('annual_volume', 'integer', '年用量预估'),
f('start_at', 'date', '立项日期'),
f('owner', 'text', '负责人'),
f('status', 'text', '状态'),
]),
obj('intel', 'chip_spec', '芯片规格书', [
f('pn', 'text', 'PN', true),
f('key_params', 'text', '关键参数'),
master('project', 'bom', '项目 BOM', [
f('project_code', 'text', '项目编码', true),
f('pn', 'text', '料号', true),
f('qty_per_unit', 'number', '单机用量'),
f('eau', 'integer', '年需求量'),
f('designed_in', 'boolean', '已导入设计'),
f('status', 'text', '状态'),
]),
/* ── 供应商:从哪买 ───────────────────────────────────────────────── */
master('supplier', 'profile', '供应商档案', [
f('supplier_code', 'text', '供应商编码', true),
f('name', 'text', '供应商名称'),
/* 原厂 / 授权代理 / 贸易商 —— 「现货场景不得选无库存转单商」判据读的就是它 */
f('type', 'text', '供应商类型'),
f('region', 'text', '区域'),
f('rating', 'text', '评级'),
f('payment_terms', 'text', '账期'),
f('status', 'text', '状态'),
]),
master('supplier', 'line_card', '代理线卡', [
f('supplier_code', 'text', '供应商编码', true),
f('brand', 'text', '品牌', true),
f('authorized', 'boolean', '是否授权'),
f('lead_time_typ', 'text', '常规交期'),
]),
master('supplier', 'contact', '供应商联系人', [
f('supplier_code', 'text', '供应商编码', true),
f('contact_code', 'text', '联系人编码', true),
f('name', 'text', '姓名'),
{ ...f('email', 'text', '邮箱'), masked: true },
{ ...f('phone', 'text', '电话'), masked: true },
]),
/* ── 市场:买什么、行情如何 ──────────────────────────────────────────
料号主数据放这一域:ERP 里它常自成一域(物料主数据),
但这门生意关心的是它**在市场上什么状态**(生命周期、有没有货、什么价),
跟行情摆在一起更顺手。要独立成域也说得通,记在这儿备考。 */
master('market', 'part', '料号主数据', [
f('pn', 'text', '料号', true),
f('brand', 'text', '品牌'),
f('category', 'text', '品类'),
f('package', 'text', '封装'),
f('version_date', 'date', '版本日期'),
]),
obj('intel', 'public_select', '公网选型库', [
f('alt_relation', 'text', '替代关系', true),
f('param_compare', 'text', '参数对比'),
f('spec_ref', 'text', '规格书'),
/* Active / NRND / EOL —— A1 三分类与 A2 选型都读它 */
f('lifecycle', 'text', '生命周期'),
f('price_ref', 'text', '价格参考'),
f('rohs', 'boolean', 'RoHS'),
f('datasheet_url', 'text', '规格书链接'),
]),
/* ── supplier:供应商信息 ─────────────────────────────────────────── */
obj('supplier', 'channel_dir', '供应商渠道目录', [
f('supplier', 'text', '供应商', true),
f('channel_type', 'text', '渠道类型'),
f('brand_lines', 'text', '品牌线'),
f('contact', 'text', '联系人'),
master('market', 'price_history', '历史价格情报', [
f('pn', 'text', '料号', true),
f('quoted_at', 'date', '报价日期', true),
f('customer_code', 'text', '客户编码'),
f('unit_price', 'number', '成交/报出单价'),
f('lead_time', 'text', '交期'),
f('lost_reason', 'text', '未成交原因'),
f('competitor_price', 'number', '竞对价位'),
]),
obj('supplier', 'dead_stock', '呆滞库存表', [
f('pn', 'text', 'PN', true),
f('stock_qty', 'number', '库存量'),
f('stock_age', 'text', '库龄'),
f('cost_price', 'number', '成本价'),
master('market', 'quote_index', '市场行情', [
f('pn', 'text', '料号', true),
f('observed_at', 'date', '观测日期', true),
f('source', 'text', '来源'),
f('price', 'number', '市场价'),
f('stock', 'integer', '可见库存'),
f('trend', 'text', '走势'),
]),
];
export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[] {
return objects.map((key) => {
const [domain, object] = key.split('.');
const hit = MVP_OBJECTS.find((o) => o.domain === domain && o.object === object);
if (hit === undefined) throw new Error(`未知对象:${key} —— 先在 MVP_OBJECTS 登记`);
return hit;
});
}
/** 四域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 */
export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market'] as const;
/**
* **样例客户** —— 让「老客户」那条路试得出来。
*
* 主数据四域建出来是空的,而 A1 的第一个状态有两条路:
* 认得出客户就直接过、认不出才请人建档。库里一个客户都没有的话,
* 两条样例跑起来一模一样 —— **第二条路根本试不到**。
*
* 所以随主数据一起播一位客户:样例 2 的来信就用他的邮箱发。
* 只播这一位,且是**幂等的按主键写**:它是让样例跑得起来的最小夹具,
* 不是往主数据里塞演示数据 —— 真实客户由 A1 建档时一位一位落进来。
*/
export const MASTER_SAMPLE_ROWS = [
/*
* **Itelma** —— 老客户测试案例的那一位(照 `JCAI_PRD/docs/活动执行页/` 两份目标件)。
* 俄罗斯车规电子、项目制;客户码 CU-0142 就是需求件上写的「已命中客户资料库」那一条。
*/
{
address: { domain: 'customer', object: 'profile' },
key: { customer_code: 'CU-0142' },
values: {
customer_code: 'CU-0142',
name: 'Itelma',
grade: 'A',
industry: '车规电子',
region: '俄罗斯',
payment_terms: 'EXW · CNY 含税',
profile_tags: '项目制 · 按项目批量询价 · 认证要求严(AEC-Q200)',
status: 'active',
},
},
{
address: { domain: 'customer', object: 'contact' },
key: { customer_code: 'CU-0142', contact_code: 'CU-0142-01' },
values: {
customer_code: 'CU-0142',
contact_code: 'CU-0142-01',
name: 'Ekaterina Volkova',
title: '采购部',
// **认人的钥匙**:老客户样例的来信就从这个邮箱发,A1 第一步照它认得出
email: 'e.volkova@itelma.ru',
},
},
/*
* 项目档案 —— 这一单询的是 MD14.1 这个项目的料。
* 播它是因为需求件上「项目制」那句话得有出处:客户属性说得出「按项目询价」,
* 库里就该有那个项目,否则那句话是装出来的。
*/
{
address: { domain: 'project', object: 'profile' },
key: { project_code: 'MD14.1' },
values: {
project_code: 'MD14.1',
customer_code: 'CU-0142',
name: 'MD14.1 车身控制器',
stage: 'BOM 后期',
status: 'active',
},
},
] as const;