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
@@ -40,7 +40,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'E04 · 关系覆盖与关键人经营',
packageVersion: '2.0.9',
packageVersion: '2.1.9',
category: '客户关系经营',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-crm-relationship-coverage',
@@ -185,6 +185,18 @@ export const manifest: ActivityManifest = {
level: 'L2',
predicate: `${D}_plan_rows > 0 and ${D}_plan_missing = 0`,
},
{
/*
* **覆盖闭合**(2026-08-24 柴浩实测补):人核对后的候选清单是后续每一步的输入契约 ——
* 他加的那家客户,计划里必须有它。两边都是基座数:计划里有几家不同客户
* (`distinct`,声明在 plan.customer_code 上)= 候选清单几行(增删后现数)。
* S3 的收步判据与 S4 的入场哨兵同一句 —— 这里是第二道墙,防的是绕过收步的路。
*/
key: 'plan-covers-scope',
label: '每家确认纳入的候选客户都进了经营计划(含人核对时新增的,一家不落)',
level: 'L2',
predicate: `${D}_plan_distinct_customer_code = ${D}_candidate_rows`,
},
{
key: 'receipt-matches-plan',
label: '每条计划都收到执行回执,且完成状态、结果摘要、关键人变动均已填写',
@@ -197,6 +209,13 @@ export const manifest: ActivityManifest = {
level: 'L2',
predicate: `${D}_ledger_update_rows > 0 and ${D}_ledger_update_missing = 0`,
},
{
/* 2026-08-24:重做后过期的旧明细不许留 —— 同一处字段只能有一条更新 */
key: 'no-duplicate-update',
label: '同一处字段只留一条更新(重做后过期的旧记录已撤掉)',
level: 'L2',
predicate: `${D}_ledger_update_distinct_update_key = ${D}_ledger_update_rows`,
},
{
key: 'no-missed-update',
label: "每条「已完成」的执行回执至少有一条对应的台账更新(没有漏更)",
@@ -218,6 +237,8 @@ export const manifest: ActivityManifest = {
{ key: 'no-guess-contact', statement: '决策链节点对应哪位联系人不确定时不得臆断 —— 匹配不上就留空,那就是缺口' },
{ key: 'no-skip-evidence', statement: '台账更新明细每条都要有回执依据,不得凭空推算写入主数据' },
{ key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' },
/* 2026-08-24:模型给缺口节点自造过顺延引用码 + 占位人名 —— 计划里「有人」必须真的有人 */
{ key: 'no-phantom-contact', statement: '计划与回执只许引用 customer.contact 已有的联系人编码与真人姓名;不得自造编码或以占位称呼顶格 —— 查无联系人先以数据录入卡收人' },
],
authorization: {
discretion: [],
@@ -263,9 +284,16 @@ export const manifest: ActivityManifest = {
* 说明书让录入卡落什么,声明里就得有什么(物理列平台基线本就有,声明是权责面)。
* 与 A1 的 name 声明并存(add-column 合并,同名同型不冲突)。
*/
{ name: 'name', type: 'text', primaryKey: false, masked: false, label: '客户名称' },
{ name: 'industry', type: 'text', primaryKey: false, masked: false, label: '所属行业' },
{ name: 'region', type: 'text', primaryKey: false, masked: false, label: '区域' },
/*
* name 必填(2026-08-24 柴浩实测:新客户档案落库连名字都是空的 —— 候选行里
* 明明写着,模型没抄)。`customer_profile_missing` 本就在 S6 收步与放行哨兵里,
* 标上它,「无名档案」从此进不了正式表;代价是碰过的客户行要把 name 照现值带上
* (剧本写死)。industry/region 标建议填:AI 从来件与背景说明推得出就填,
* 推不出留橘胶囊给人 —— 台账更新确认屏上现在改得了。
*/
{ name: 'name', type: 'text', primaryKey: false, masked: false, required: true, label: '客户名称' },
{ name: 'industry', type: 'text', primaryKey: false, masked: false, expect: true, label: '所属行业' },
{ name: 'region', type: 'text', primaryKey: false, masked: false, expect: true, label: '区域' },
/*
* L-02 文档明列的字段,权威维护者是尚未建成的 T02(客户建档评估) —— 平时
* E04 不写它;唯一的例外是"新客户建立合作"这一刻,E04 代替不存在的 T02
@@ -296,8 +324,14 @@ export const manifest: ActivityManifest = {
* (静默退化成留空,不报错)。
*/
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, auto: true, label: '联系人编码' },
/* 建档卡的联系人段会写的三列 —— 同上,声明即授权面;email 照平台口径标脱敏 */
{ name: 'name', type: 'text', primaryKey: false, masked: false, label: '姓名' },
/*
* 建档卡的联系人段会写的三列 —— 同上,声明即授权面;email 照平台口径标脱敏。
* name 必填(2026-08-24 柴浩实测:早前一轮模型替「计划要经营的岗位」凭空建了
* 两条无名联系人档,人名职务全空、却标着关键人/亲密度3 —— 那不是联系人,是把
* 计划里的节点冒充成了人)。`customer_contact_missing` 本就在收步与放行哨兵里,
* 标上它,无名建档从此进不了正式表;代价是碰过的联系人行把 name 照现值带上。
*/
{ name: 'name', type: 'text', primaryKey: false, masked: false, required: true, label: '姓名' },
{ name: 'title', type: 'text', primaryKey: false, masked: false, label: '职务' },
{ name: 'email', type: 'text', primaryKey: false, masked: true, label: '邮箱' },
{ name: 'role', type: 'text', primaryKey: false, masked: false, expect: true, label: '角色' },
@@ -346,8 +380,21 @@ export const manifest: ActivityManifest = {
label: '关键人经营计划',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, label: '联系人编码' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, required: true, label: '客户编码' },
/*
* `refersTo` —— 计划只许指向真实存在的联系人(2026-08-24,措辞治不了的结构治):
* 模型三次给缺口节点自造顺延引用码 + 占位人名,反例描述/说明书纪律/禁令
* 全拦不住;现在基座在写入那一刻查 customer.contact(正式 + 本单草稿),
* 查无此码当场拒并指路(先发数据录入卡收人)。receipt 同款。
*/
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, refersTo: 'customer.contact.contact_code', label: '联系人编码' },
/*
* `distinct: true` —— 基座数「计划里有几家不同客户」(`crm_coverage_plan_distinct_customer_code`)。
* 它是覆盖闭合判据的落点(2026-08-24 柴浩实测:人在覆盖范围确认卡上加了一家客户,
* 行落进了台账,而计划/回执/收口把它从头无视到尾 —— 收步与放行只数「计划有没有行」,
* 「盖没盖住每一家候选」没有任何一个基座数说得出来)。计划行数 ≠ 客户数
* (一家客户几位关键人就几行),所以必须另数这一个。
*/
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, required: true, distinct: true, label: '客户编码' },
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户名称' },
{ name: 'contact_name', type: 'text', primaryKey: false, masked: false, expect: true, label: '联系人姓名' },
{ name: 'action', type: 'text', primaryKey: false, masked: false, required: true, label: '建议动作' },
@@ -385,7 +432,7 @@ export const manifest: ActivityManifest = {
label: '经营计划执行回执',
fields: [
{ name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '覆盖轮次编号' },
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, label: '联系人编码(与计划一一对应)' },
{ name: 'contact_code', type: 'text', primaryKey: true, masked: false, refersTo: 'customer.contact.contact_code', label: '联系人编码(与计划一一对应)' },
{ name: 'customer_code', type: 'text', primaryKey: false, masked: false, expect: true, label: '客户编码' },
/* 目标件的「对象」列长「AvtoKom · 产品 VP」—— 客户名与人名都要在,S3 从计划抄 */
{ name: 'customer_name', type: 'text', primaryKey: false, masked: false, label: '客户名称(承继自计划,只读展示)' },
@@ -430,7 +477,15 @@ export const manifest: ActivityManifest = {
{ name: 'field_label', type: 'text', primaryKey: false, masked: false, required: true, label: '字段(人读列名)' },
{ name: 'before_value', type: 'text', primaryKey: false, masked: false, required: true, label: '更新前(无值写"(无)")' },
{ name: 'after_value', type: 'text', primaryKey: false, masked: false, required: true, label: '更新后' },
{ name: 'evidence_ref', type: 'text', primaryKey: false, masked: false, required: true, label: '依据(引用具体回执事实)' },
{ name: 'evidence_ref', type: 'text', primaryKey: false, masked: false, required: true, label: '依据(用人话引用回执事实)' },
/*
* 更新位置键(2026-08-24 用户:「重做后老的变更记录应该删除」):主键是序号,
* 同一处字段(同一张表·同一客户·同一联系人·同一字段)重做时若新开一行,旧行就留在
* 明细里骗人 —— SQ-0031 的覆盖度一度同时挂着 50→50 与 50→100 两条。
* 这一列由模型按「台账地址|客户编码|联系人编码|字段」拼,`distinct` 让基座数
* 「有几处不同位置」;判据钉 distinct = rows —— 同一处只留一条。
*/
{ name: 'update_key', type: 'text', primaryKey: false, masked: false, required: true, distinct: true, label: '更新位置键(台账地址|客户|联系人|字段)' },
],
},
{