E04 修到零被拒线,harness 补七刀:跨进程租约与广播桥、原子写、引用墙、对话区按序号排

活动侧(E04 2.1.0 → 2.1.9,I01 1.2.2):
- 覆盖闭合成为机检:基座新增 distinct 读数,「计划盖住的客户数 = 候选行数」钉进
  S3 收步 / S4 哨兵 / 放行 L2;人加的候选客户不再被整单无视
- 「关键人待识别」占位句式全链退役:查无联系人先发数据录入卡收人;契约新增 refersTo,
  计划/回执的 contact_code 只许引用真实存在的联系人(安装期核引用目标)
- 台账更新确认摆全落库面:客户档案/联系人档案两张落库预览(touched: 数据源,
  正式现值 + 草稿叠加),可补改、可加行(自增主键由基座续号);姓名升必填
- 更新明细:update_key 位置键 + 「同一处只留一条」L2;依据写人话不带机器键;
  标题旁不摆单号;判据与文案不夹字段名;行标识去掉单标识那一段
- 回执骨架 = 计划行数钉进 S3 收步与 S4 哨兵;重做分支不许删计划凑数
- 阶段刻度对齐文档 7 步;起单样例预填满

基座侧:
- 两个后端连同一个库:task-claim 跨进程租约(迁移 0030)+ Postgres LISTEN/NOTIFY
  广播桥;对话区按任务内记忆序号排,不按墙上时钟;并卡「刚说过话」按记忆序列判紧邻
- 多行 write 整批先验后写(dryRun),被拒 = 一行都没落
- 决策卡意见与本单草稿字面比对,归属以观察进轨迹(note-anchors)
- 括号栈补「补闭括号」两形(少一个 } / 行对象没关直接跟下一行)
- 件上加行:空着的自增主键直进续号,不从别的行借号;未落下的行点名回给前端
- 人答复落库那一刻就把 runtime 翻 running(task.wake),两块同帧变脸
- 试跑单可落真库(land_master,迁移 0031,默认开);数据域按活动分组;
  JSON 列不再是 [object Object];按钮键感 + 在办进度条 + 防抖
- 报价链 A1–A7 从系统卸载(只卸不删源码),种子只装 E04/I01

契约变更(api-report 已刷新):FieldDeclaration.distinct / refersTo,
objectDeclaration.file,landMaster,unkeyed,assetFiles,ledgerByActivityRoute。
1700 测 / typecheck / lint / depcruise / api:check 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
This commit is contained in:
xcs
2026-08-25 01:23:30 +08:00
co-authored by Claude Fable 5
parent 92dd729c36
commit f3397a60ef
91 changed files with 3919 additions and 350 deletions
@@ -150,23 +150,104 @@ describe('doneWhen 是「成果已存在」的判据 —— 开局不许真,干
).toBe(false);
});
it('计划有行且必填齐 → S3 收得掉;缺一头都收不掉', () => {
expect(safeEval(doneOf('S3_plan'), { crm_coverage_plan_rows: 3, crm_coverage_plan_missing: 0 })).toBe(true);
expect(safeEval(doneOf('S3_plan'), { crm_coverage_plan_rows: 0, crm_coverage_plan_missing: 0 })).toBe(false);
expect(safeEval(doneOf('S3_plan'), { crm_coverage_plan_rows: 3, crm_coverage_plan_missing: 2 })).toBe(false);
it('计划有行、必填齐、且盖住每家候选 → S3 收得掉;缺一头都收不掉', () => {
const full = {
crm_coverage_plan_rows: 3,
crm_coverage_plan_missing: 0,
crm_coverage_plan_distinct_customer_code: 2,
crm_coverage_candidate_rows: 2,
crm_coverage_receipt_rows: 3,
};
expect(safeEval(doneOf('S3_plan'), full)).toBe(true);
expect(safeEval(doneOf('S3_plan'), { ...full, crm_coverage_plan_rows: 0 })).toBe(false);
expect(safeEval(doneOf('S3_plan'), { ...full, crm_coverage_plan_missing: 2 })).toBe(false);
});
it('台账更新明细齐 + 主数据必填齐 → S6 收得掉', () => {
it('**回执骨架比计划少一行 → S3 收不掉、S4 哨兵不开**(2026-08-24 柴浩实测:计划 7 回执 6,人填完就能交卷)', () => {
expect(
safeEval(doneOf('S3_plan'), {
crm_coverage_plan_rows: 7,
crm_coverage_plan_missing: 0,
crm_coverage_plan_distinct_customer_code: 6,
crm_coverage_candidate_rows: 6,
crm_coverage_receipt_rows: 6,
}),
).toBe(false);
const sentry = /<ifPart id="if_pa"><condition>([^<]+)<\/condition>/.exec(CMMN_XML)?.[1] ?? '';
expect(sentry).toContain('crm_coverage_receipt_rows = crm_coverage_plan_rows');
const s4 = COVERAGE_STEPS.find((x) => x.item === 'S4_plan_approval')?.guide ?? '';
expect(s4).toContain('加了一条计划就同拍给它加一行');
const rel = COVERAGE_STEPS.find((x) => x.item === 'T_release')?.guide ?? '';
expect(rel).toContain('不许删计划来凑数');
});
it('**计划漏了一家候选客户 → S3 收不掉**(2026-08-24 柴浩实测:人加的那家被从头无视到尾)', () => {
// 人在覆盖范围确认卡上加了一家(candidate 2 行),计划只盖住 AI 识别的那一家
expect(
safeEval(doneOf('S3_plan'), {
crm_coverage_plan_rows: 3,
crm_coverage_plan_missing: 0,
crm_coverage_plan_distinct_customer_code: 1,
crm_coverage_candidate_rows: 2,
}),
).toBe(false);
// S4 的入场哨兵是同一句闭合 —— 计划没盖全,审批卡就不该发出来
const sentry = /<ifPart id="if_pd"><condition>([^<]+)<\/condition>/.exec(CMMN_XML)?.[1] ?? '';
expect(sentry).toContain('crm_coverage_plan_distinct_customer_code = crm_coverage_candidate_rows');
});
it('覆盖闭合的两边都是基座数:distinct 声明在 plan.customer_code 上,放行 L2 也钉同一句', () => {
const plan = manifest.declarations.ledger.find((l) => l.object === 'plan');
expect(plan?.fields.find((f) => f.name === 'customer_code')?.distinct).toBe(true);
const criterion = manifest.contract.acceptanceCriteria.find((c) => c.key === 'plan-covers-scope');
expect(criterion?.level).toBe('L2');
expect(criterion?.predicate).toBe('crm_coverage_plan_distinct_customer_code = crm_coverage_candidate_rows');
});
it('台账更新明细齐 + 主数据必填齐 + 无重复位置 → S6 收得掉', () => {
expect(
safeEval(doneOf('S6_ledger_update'), {
crm_coverage_ledger_update_rows: 4,
crm_coverage_ledger_update_missing: 0,
crm_coverage_ledger_update_distinct_update_key: 4,
customer_profile_missing: 0,
customer_contact_missing: 0,
}),
).toBe(true);
});
it('**重做后旧明细没撤(同一处位置两条)→ S6 收不掉**(2026-08-24 用户:「重做后老的变更记录应该删除」)', () => {
expect(
safeEval(doneOf('S6_ledger_update'), {
crm_coverage_ledger_update_rows: 5,
crm_coverage_ledger_update_missing: 0,
crm_coverage_ledger_update_distinct_update_key: 4,
customer_profile_missing: 0,
customer_contact_missing: 0,
}),
).toBe(false);
const decl = manifest.declarations.ledger.find((l) => l.object === 'ledger_update');
const col = decl?.fields.find((f) => f.name === 'update_key');
expect(col?.distinct).toBe(true);
expect(col?.required).toBe(true);
expect(manifest.contract.acceptanceCriteria.some((c) => c.key === 'no-duplicate-update' && c.level === 'L2')).toBe(true);
// 依据一列写人话:不带联系人编码这类机器键;重做分支写明过期行要撤
const s6 = COVERAGE_STEPS.find((x) => x.item === 'S6_ledger_update')?.guide ?? '';
expect(s6).toContain('不写联系人编码');
expect(s6).toContain('update_key');
const rel = COVERAGE_STEPS.find((x) => x.item === 'T_release')?.guide ?? '';
expect(rel).toContain('过期的旧记录要撤掉');
});
it('四份件标题旁不再摆单号(2026-08-24 用户定);验收判据与「AI 已完成」文案不夹字段名', () => {
for (const t of manifest.assets.viewTemplates) {
expect(t.html).not.toMatch(/<h3>[^<]*<span data-slot="round_ref">/);
}
const html = manifest.assets.viewTemplates.find((t) => t.name === 'coverage-ledger-update')?.html ?? '';
const prose = html.replace(/<style>[\s\S]*?<\/style>/, '').replace(/data-[a-z-]+="[^"]*"/g, '');
expect(prose).not.toMatch(/coverage_score|decision_chain/);
});
it('**收口写了明细却漏了联系人必填(如 influence)→ S6 收不掉** —— 新客户路的死锁口', () => {
expect(
safeEval(doneOf('S6_ledger_update'), {
@@ -197,6 +278,45 @@ describe('实测账本对应的剧本口径 —— 改口要连测试一起改',
* T_release 的剧本「裁判据 → submit」原样又递回来 —— 剧本里压根没有「被打回了怎么办」
* 这一支)。基座那道闸只拦得住原样重提,拦不出「该改哪一张表」——那是活动自己的知识。
*/
it('驳回意见先认归属:S4 剧本写死「照基座比对的观察认、不许改到没点名的行、认不出唯一就重发卡问」', () => {
// 2026-08-24 柴浩实测:意见点名「测试111」,模型把改动落到了 Itelma 那一行
const guide = COVERAGE_STEPS.find((x) => x.item === 'S4_plan_approval')?.guide ?? '';
expect(guide).toContain('先认归属');
expect(guide).toContain('意见按字面点名对上了');
expect(guide).toContain('不是把改动挪到别家');
expect(guide).toContain('不许猜');
});
it('姓名是必填:客户档案与联系人档案都不许无名落库(2026-08-24 两条空联系人 + 无名新客户档案)', () => {
const profile = manifest.declarations.ledger.find((l) => l.domain === 'customer' && l.object === 'profile');
const contact = manifest.declarations.ledger.find((l) => l.domain === 'customer' && l.object === 'contact');
expect(profile?.fields.find((f) => f.name === 'name')?.required).toBe(true);
expect(contact?.fields.find((f) => f.name === 'name')?.required).toBe(true);
// 行业/区域是建议档:AI 推得出就填,推不出橘胶囊留给人(确认屏上现在改得了)
expect(profile?.fields.find((f) => f.name === 'industry')?.expect).toBe(true);
expect(profile?.fields.find((f) => f.name === 'region')?.expect).toBe(true);
// 剧本写死:没有真实姓名的人不建联系人行;碰过的行 name 照现值带上
const guide = COVERAGE_STEPS.find((x) => x.item === 'S6_writeback')?.guide ?? '';
expect(guide).toContain('没有真实姓名的人不许建联系人行');
expect(guide).toContain('name');
expect(guide).toContain('照候选行抄');
expect(guide).toContain('不许编造一个查无出处的行业或地区');
});
it('计划只指向真人:占位句式已退役,查无联系人走数据录入卡(2026-08-24 用户裁定)', () => {
// 实测:模型把占位句式泛化到缺口节点,给 Itelma 编了两个顺延引用码 + 占位人名
const guide = COVERAGE_STEPS.find((x) => x.item === 'S3_plan')?.guide ?? '';
expect(guide).toContain('真实存在的编码');
expect(guide).toContain('数据录入卡');
expect(guide).toContain('"kind":"entry"');
// 占位引用码的句式一个不留 —— 给了可照抄的样板,模型就会把它用到不该用的地方
expect(guide).not.toContain('-00');
expect(guide).not.toContain('待识别');
const s6 = COVERAGE_STEPS.find((x) => x.item === 'S6_writeback')?.guide ?? '';
expect(s6).not.toContain('-00');
expect(manifest.contract.prohibitions.some((x) => x.key === 'no-phantom-contact')).toBe(true);
});
it('放行剧本带「被打回重做」那一支,且写面覆盖要改的那几张表', () => {
const release = COVERAGE_STEPS.find((s) => s.item === 'T_release');
expect(release?.guide).toContain('验收被打回重做');
@@ -315,3 +435,27 @@ function safeEval(expression: string, scope: Record<string, unknown>): boolean {
return false;
}
}
/**
* **进度条的刻度要与业务流程的步数对得上**(2026-08-24 用户:「E04 应该是有 7 步、
* 给人确认是 4 步,这里要是 x/7 才对」)。从前粗分三段 → 屏上 x/4,
* 而文档开篇写的是「整体流程(7 步)」:同一件事两个数,人只能挑一个信。
*/
describe('阶段刻度 = 文档那 7 步', () => {
it('声明 6 段 —— 加上基座那一格「放行收口」,人看到的是 x/7', () => {
expect(COVERAGE_PHASES).toHaveLength(6);
});
it('一段一步:除「更新关系台账」外每段恰好一个计划项', () => {
const multi = COVERAGE_PHASES.filter((p) => p.items.length > 1).map((p) => p.name);
expect(multi).toEqual(['更新关系台账']); // 写回主数据 + 明细留痕,业务上是同一步⑥
});
it('四张卡对应的那四步各占一格 —— 「给人确认是 4 步」说的就是它们', () => {
const names = COVERAGE_PHASES.map((p) => p.name);
// 人确认的三格在声明里,第四格(放行收口)由基座加
expect(names).toContain('覆盖范围确认');
expect(names).toContain('经营计划审批');
expect(names).toContain('执行经营动作与回执');
});
});