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

手改是**兜 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
@@ -1,61 +1,67 @@
/**
* 四域十九对象 —— 声明即物化的输入。守三件事:数量与域别齐、主键齐、按名可取。
* 主数据四域 —— 客户 / 项目 / 供应商 / 市场。
*
* 这份测试从前断言的是「四域十九对象」,那个四域是 project / customer / intel / supplier,
* 装的是**退役占位槽**的声明,里面混着一单一份的流转单据(受理记录、比价表、报价草案)。
* 2026-07-31 重分:单据归各活动自己的域,这个文件只留**主数据**,按常规 CRM / ERP 分四域。
*
* 守的是三条,每条都能机检:
* · 只有这四个域,不多不少 —— 多一个就说明有人把单据塞进主数据了;
* · 每张表都是 `master` 且**不带单标识** —— 带了就是单据归错类(安装静态审也会拦);
* · 主键是**业务实体**,不是单。
*/
import { describe, expect, it } from 'vitest';
import { MVP_OBJECTS, declarationsFor } from '../seed-mvp-data.ts';
import { MASTER_DOMAINS, MASTER_OBJECTS } from '../seed-mvp-data.ts';
describe('MVP_OBJECTS', () => {
it('四域十九对象,不多不少 —— 定价审批表载体是审批流,不入台账', () => {
expect(MVP_OBJECTS).toHaveLength(19);
const count = (domain: string) => MVP_OBJECTS.filter((o) => o.domain === domain).length;
expect(count('project')).toBe(13);
expect(count('customer')).toBe(1);
expect(count('intel')).toBe(3);
expect(count('supplier')).toBe(2);
describe('主数据四域', () => {
it('只有客户 / 项目 / 供应商 / 市场四个域', () => {
expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market']);
expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort());
});
it('每个对象至少一个主键字段 —— 只增不破坏靠主键立身', () => {
for (const o of MVP_OBJECTS) {
expect(o.fields.some((f) => f.primaryKey), `${o.domain}.${o.object}`).toBe(true);
}
});
it('每个字段都有中文名 —— 界面上不许只见键不见名', () => {
for (const o of MVP_OBJECTS) {
expect(o.label, `${o.domain}.${o.object} 缺对象名`).toBeTruthy();
for (const f of o.fields) {
expect(f.label, `${o.domain}.${o.object}.${f.name} 缺字段名`).toBeTruthy();
}
it('四个域各自都有表 —— 空占位也得立得住', () => {
for (const d of MASTER_DOMAINS) {
expect(MASTER_OBJECTS.filter((o) => o.domain === d).length, `${d} 域一张表都没有`).toBeGreaterThan(0);
}
});
/*
* 受理结论是**一单一行**,主键只有询价单号。
*
* 这条断言守的是一个会静默出错的设计:出口的标量字段取自 `landed.flat`,
* 而 `flat` 是把这一单所有落地行 reduce 展开成一个平对象。两枚去向旗若挂在
* 按料号分行的对象上,六个料号就有六行同名列、最后合并的那行赢 ——
* 而归并规则是「任一条命中 C 即需选型」,网关会照着错值分叉且无人察觉。
* 这一条是分类的要害:主数据按**业务实体**分行,不按单。
* 带上单标识就说明它其实是一单一份的单据 —— 归错类,治理方式全跟着错。
*/
it('受理结论一单一行 —— 去向旗不能挂在按料号分行的对象上', () => {
const verdict = MVP_OBJECTS.find((o) => o.object === 'intake_verdict');
expect(verdict, '缺 project.intake_verdict').toBeDefined();
expect(verdict?.domain).toBe('project');
expect(verdict?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['rfq_ref']);
for (const flag of ['need_alternative', 'need_sourcing']) {
expect(verdict?.fields.find((f) => f.name === flag)?.type, flag).toBe('boolean');
}
// 反过来也要守:按料号分行的对象上不许出现去向旗
for (const o of MVP_OBJECTS) {
if (o.object === 'intake_verdict') continue;
const names = o.fields.map((f) => f.name);
expect(names, `${o.domain}.${o.object} 不该带去向旗`).not.toContain('need_alternative');
expect(names, `${o.domain}.${o.object} 不该带去向旗`).not.toContain('need_sourcing');
it('每张表都是 master 且不带单标识', () => {
for (const o of MASTER_OBJECTS) {
expect(o.kind, `${o.domain}.${o.object}`).toBe('master');
expect(o.caseKey, `${o.domain}.${o.object} 不该有单标识`).toBeUndefined();
}
});
it('declarationsFor 按名取子集,名不存在即抛', () => {
expect(declarationsFor(['project.intake_record', 'intel.history_price'])).toHaveLength(2);
expect(() => declarationsFor(['project.nope'])).toThrow('未知对象');
it('每张表都有主键 —— 没主键的台账合并与去重无从谈起', () => {
for (const o of MASTER_OBJECTS) {
const pk = o.fields.filter((f) => f.primaryKey).map((f) => f.name);
expect(pk.length, `${o.domain}.${o.object} 没有主键`).toBeGreaterThan(0);
}
});
/* 主键得是业务实体键 —— 单标识那几个名字出现在主键里,就是归错了类 */
it('主键里不出现单标识那几个名字', () => {
for (const o of MASTER_OBJECTS) {
const pk = o.fields.filter((f) => f.primaryKey).map((f) => f.name);
for (const caseish of ['rfq_ref', 'quote_no', 'task_id']) {
expect(pk, `${o.domain}.${o.object} 的主键里有单标识 ${caseish}`).not.toContain(caseish);
}
}
});
/* 个人信息必须标脱敏 —— 主数据里存联系人,治理面照 `masked` 裁剪 */
it('联系人的邮箱与电话标了脱敏', () => {
const contacts = MASTER_OBJECTS.filter((x) => x.object === 'contact');
expect(contacts.length).toBeGreaterThan(0);
for (const o of contacts) {
for (const name of ['email', 'phone']) {
const field = o.fields.find((x) => x.name === name);
expect(field?.masked, `${o.domain}.${o.object}.${name} 未标脱敏`).toBe(true);
}
}
});
});