W05 在途修订:差异评估结论审核件一个候选一张表 · 关键台账数据看板件(ledger-board)· S1 清单不再把人刚填的参数摆成缺口

活动包 `part-substitution-assessment`(case-model / manifest / report / target-style / badge / ledger-board 与两份包内测试)、
目标件「验收工单-01-差异评估结论审核」、`docs/ss02-act-05` 关键台账数据两份、端到端测试记录、e2e 脚本。
逐条口径见同批 CLAUDE.md 快照。2026-08-27 另一会话做的,一直留在工作树没入库;与同批另两条提交互相依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
This commit is contained in:
xcs
2026-08-27 23:20:15 +08:00
co-authored by Claude Fable 5
parent 0923fbfe4d
commit 909b1fe4b7
13 changed files with 1943 additions and 208 deletions
@@ -174,6 +174,45 @@ describe('说明书与剧本的骨架', () => {
expect(secondPass).toMatch(/收不了步|收不掉/);
});
/*
* 数据工单确认即入库(2026-08-27 用户定:「数据工单里填的数据,确认后就应该入库」)。
* 基座在确认那一刻把卡上 gateLedgers 那几张表并进正式表;剧本这一头不能再教模型「已经落草稿层」——
* 那句话会让它以为放行前还能改、还能 drop,而在库的键写下来会被当场拒。
*/
it('两张数据工单的剧本都写明:确认后已入正式表,别再写、别 drop 那几行;P-10 的卡骨架也说「确认后即入库」', () => {
for (const item of ['S0d_p08_master', 'S0d_p10_document']) {
const guide = ASSESSMENT_STEPS.find((s) => s.item === item)?.guide ?? '';
expect(guide, item).toContain('入了正式表');
expect(guide, item).not.toContain('已经落草稿层');
expect(guide, item).toContain('别去 drop');
}
expect(NARRATIVE).toContain('确认后这批主数据即入库');
expect(NARRATIVE).toContain('确认后这批资料记录即入库');
});
/*
* S1 的「待确认」行只许来自真写着「未披露」的参数(2026-08-27 用户:「我在第一步已经补充了
* XL2596S-5.0 使能阈值,当前这一步还要填写一次?」)—— 剧本从前给了一句原话示例
* 「input_item 如『XL2596S-5.0 使能阈值』」,模型照抄示例、不读现值,把人刚填的 2.5 摆成缺口。
* 提示词里唯一该出现的原话,是你想听到的那句;示例行一律不引具体料号 + 参数名。
*/
/*
* 第三次同一病(2026-08-27):措辞改了两遍照样复现 —— 台账里每个参数都有值,模型还是写了一行
* 「使能阈值 · 原厂未披露」。参数值有没有是台账里的事实,不该由模型在清单里宣布:
* S1 从此**一行参数都不写**,件上摆一张基座现读的「候选参数现值」表;说明书全文不再出现「使能阈值」。
*/
it('S1 不写参数行(item_key 不许以 unknown_ 开头);说明书全文没有「使能阈值」这个可照抄的名字', () => {
const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? '';
expect(s1).toContain('候选参数一行都不写');
expect(s1).toContain('不许以 unknown_ 开头');
expect(s1).not.toContain('待确认');
expect(NARRATIVE).not.toContain('使能阈值');
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toContain('使能阈值');
const skeleton = NARRATIVE.slice(NARRATIVE.indexOf('⑤ 执行工单 01'), NARRATIVE.indexOf('"standard":"每一条「待补」行'));
expect(skeleton).toContain('候选参数现值');
expect(skeleton).not.toContain('待确认');
});
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明 found 的一颗都不建', () => {
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
expect(s0).toContain('当下可为');
@@ -206,23 +245,47 @@ describe('说明书与剧本的骨架', () => {
for (const step of ASSESSMENT_STEPS) expect(step.guide, step.item).not.toMatch(disableCallPattern);
});
it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来),两项都没有 doneWhen', () => {
it('S0d 拆成两项各自绑自己的 view(P-10 那份件不会永远推不出来)', () => {
const p08 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master');
const p10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document');
expect(p08?.view).toBe('p08-master-create');
expect(p10?.view).toBe('p10-document-create');
expect(p08?.doneWhen).toBeUndefined();
expect(p10?.doneWhen).toBeUndefined();
});
it('P-08 交回之后先 finish 再发 P-10 卡,顺序写死进两段 guide(2026-08-27 实测:漏收步会让工作区画面停在 P-08 那两张表,P-10 卡发出后人却看不见资料表、提交闸拦死)', () => {
/*
* **两张数据工单的 doneWhen 只读入库事实**(2026-08-27 晚):引擎只在人答过卡之后求它 —— 值是人在件上填的、
* 引擎数得出来,不再靠模型记得 finish(剧本写了两处它照样忘,件停在 P-08)。
*/
it('P-08:要建的每颗料主数据与参数都落了、必填不缺 → 收得掉;少建一颗 / 还缺必填 / 参数行对不上账 → 收不掉', () => {
const ok = { parts_profile_rows: 2, [`${D}_pn_check_exists_status_absent`]: 2, [`${D}_pn_check_exists_status_created`]: 0, parts_profile_missing: 0, parts_parameter_missing: 0, parts_parameter_rows: 14, parts_parameter_distinct_param_name: 7 };
expect(safeEval(doneOf('S0d_p08_master'), ok)).toBe(true);
// 模型把其中一颗改成 created 也一样算(absent + created 才是要建的料数)
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, [`${D}_pn_check_exists_status_absent`]: 1, [`${D}_pn_check_exists_status_created`]: 1 })).toBe(true);
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_rows: 1, parts_parameter_rows: 7 })).toBe(false);
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_profile_missing: 1 })).toBe(false);
expect(safeEval(doneOf('S0d_p08_master'), { ...ok, parts_parameter_rows: 13 })).toBe(false);
});
it('P-10:建过的每颗料都有一行资料记录且必填不缺 → 收得掉;资料少一行 / 骨架还空着 → 收不掉', () => {
const ok = { parts_document_rows: 2, parts_profile_rows: 2, parts_document_missing: 0 };
expect(safeEval(doneOf('S0d_p10_document'), ok)).toBe(true);
expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_rows: 1 })).toBe(false);
expect(safeEval(doneOf('S0d_p10_document'), { ...ok, parts_document_missing: 3 })).toBe(false);
});
it('S1 的入场哨兵不再等模型写 created 旗:入库事实齐了门就开', () => {
const cond = /<sentry id="s_pns_resolved">[\s\S]*?<condition>([\s\S]*?)<\/condition>/.exec(CMMN_XML)?.[1] ?? '';
expect(cond).not.toContain('sub_eval_pn_check_exists_status_absent = 0');
expect(cond).toContain('sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created');
});
it('P-08 交回之后的收步不再靠模型记得 finish:剧本写明引擎按入库事实自己收,finish 只是省一拍(2026-08-27 晚:「顺序写死」两处模型照样忘)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('finish S0d_p08_master');
expect(s0d).toMatch(/顺序不能反/);
expect(s0d).toContain('p08-master-create');
expect(s0d).toMatch(/引擎也会自己收/);
expect(s0d).not.toMatch(/顺序不能反/);
const s0d10 = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p10_document')?.guide ?? '';
expect(s0d10).toContain('finish S0d_p08_master');
expect(s0d10).toMatch(/若它还开着|先.*finish/);
expect(s0d10).toMatch(/引擎按入库事实自己收/);
});
it('「发卡前清单必须先有行」写在说明书里,不只在剧本里(2026-08-27 实测:模型把 P-10 收步与 S1 发卡压成一拍,那一拍 S1 剧本没摊开,卡发出去时 input_gap 一行都没有,人对着空表无从下手)', () => {
@@ -270,7 +333,7 @@ describe('说明书与剧本的骨架', () => {
expect(NARRATIVE).toContain('parts.document` 必须先有骨架行');
expect(NARRATIVE).toMatch(/压拍/);
// 标了几颗建好,主数据就得有几行 —— 逐颗对账(第一刀写成「≥1 行就算数」太松,三候选那单从缝里过去了)
expect(CMMN_XML).toContain('parts_profile_rows &gt;= sub_eval_pn_check_exists_status_created');
expect(CMMN_XML).toContain('parts_profile_rows &gt;= sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created');
});
it('评估输入那张卡的**前提**写在说明书的卡骨架上(2026-08-27 e2e:两颗缺库,模型只问只建了第一颗,CN 还 absent 就把 S1 的卡发了出去 —— 发卡的授权面是调人预算,绕得过哨兵)', () => {
@@ -295,7 +358,7 @@ describe('说明书与剧本的骨架', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/参数名一律用中文/);
// 措辞写过两遍都没拦住 → 上结构:把「同一套名字」变成基座算得出的数
expect(CMMN_XML).toContain('parts_parameter_rows = parts_parameter_distinct_param_name * sub_eval_pn_check_exists_status_created');
expect(CMMN_XML).toContain('parts_parameter_rows = parts_parameter_distinct_param_name * (sub_eval_pn_check_exists_status_absent + sub_eval_pn_check_exists_status_created)');
expect(g).toContain('input_voltage_range');
expect(g).toMatch(/不是同一行/);
});
@@ -316,6 +379,7 @@ describe('说明书与剧本的骨架', () => {
it('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => {
const html = manifest.assets.viewTemplates.find((v) => v.name === 'p08-master-create')?.html ?? '';
// 两种包装都要能横滑:验收件用目标件的 .tbl-wrap(它的验收版写着 overflow:hidden),2026-08-27 用户在结论审核页撞上
// 运营侧的看板也在这条纪律里(它六张表都宽)
for (const v of manifest.assets.viewTemplates) expect(v.html, v.name).toContain('.jc-tbl-wrap, .tbl-wrap { overflow-x: auto;');
expect(html).toContain('#tbl1.on td > .jc-vcell:not(.ro) { display: none; }');
expect(html).toContain('input.jc-cell, select.jc-cell { height: 32px;');
@@ -347,3 +411,47 @@ describe('说明书与剧本的骨架', () => {
expect([...all].sort()).toEqual([...ITEM_IDS.filter((i) => i !== 'T_release')].sort());
});
});
/*
* 人工验收清单(2026-08-27 用户定):验收卡上的条目是人核这份工单要走的步骤,不是系统判的判据;
* 每份验收工单固定 —— 差异评估结论审核(S4_review 审核卡)与评审放行与落账确认(T_release 终局验收卡)各一份。
*/
describe('两张验收工单的人工验收清单', () => {
it('S4_review 与 T_release 各声明一份固定清单:至少 4 条,每条是一句可核对的动作,互不重复', () => {
for (const item of ['S4_review', 'T_release']) {
const list = ASSESSMENT_STEPS.find((s) => s.item === item)?.acceptanceChecklist ?? [];
expect(list.length, item).toBeGreaterThanOrEqual(4);
expect(new Set(list).size, item).toBe(list.length);
for (const line of list) expect(line.length, `${item}: ${line}`).toBeGreaterThan(12);
}
// 别的步骤不发验收卡,不该有清单
for (const step of ASSESSMENT_STEPS) {
if (step.item === 'S4_review' || step.item === 'T_release') continue;
expect(step.acceptanceChecklist, step.item).toBeUndefined();
}
});
});
/*
* 评估输入清单每行六列都要写(2026-08-27 实测:模型 11 行全漏了必填列 input_item,件上红带点名「缺输入项」,
* 人把别的格填满也过不了闸;而剧本从前只在括号里提过一次 input_item,逐行的做法里没再说)。
*/
describe('S1 清单:每行六列都要写', () => {
it('剧本逐行写明 input_item 的人读名与 source_ledger,并让模型发卡前核 _missing = 0', () => {
const s1 = ASSESSMENT_STEPS.find((s) => s.item === 'S1_input_confirm')?.guide ?? '';
expect(s1).toContain('每一行六列都要写');
for (const name of ['customer「客户」', 'request「客户需求」', 'original_pn「原 PN」', 'application_scenario「应用场景」', 'bom_position「BOM 位」', 'original_doc_version「原 PN 资料版本」']) {
expect(s1, name).toContain(name);
}
expect(s1).toContain(`${D}_input_gap_missing`);
});
});
/* 放行那一步只 submit,不发工单(2026-08-27 实测:先一张执行单「评审放行与落账确认」,人答完才 submit 又一张验收单) */
describe('T_release 只 submit', () => {
it('剧本写明不发执行工单 / 决策卡', () => {
const t = ASSESSMENT_STEPS.find((s) => s.item === 'T_release')?.guide ?? '';
expect(t).toContain('不发执行工单');
expect(t).toContain('submit');
});
});