Archived
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:
@@ -13,8 +13,14 @@ import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
|
||||
import activity from '../src/index.ts';
|
||||
|
||||
const ITEMS = ['S0_bind', 'S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S2_normalize', 'S3_assess', 'S4_review', 'S5_prepare_landing', 'T_release'];
|
||||
const NO_DONE_WHEN = new Set(['S0d_p08_master', 'S0d_p10_document', 'S1_input_confirm', 'S4_review', 'T_release']);
|
||||
/*
|
||||
* 2026-08-27 晚:两张数据工单改成**给 doneWhen**(只读入库事实)—— 引擎只在人答过卡之后求它,
|
||||
* 不再靠模型记得 finish。仍不给的三处:输入确认(交人补缺口)/ 审核循环 / 放行。
|
||||
*/
|
||||
const NO_DONE_WHEN = new Set(['S1_input_confirm', 'S4_review', 'T_release']);
|
||||
const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'difference-review', 'landing-confirm'];
|
||||
/** 运营中心 L2 沙箱里那张关键台账数据看板 —— 不是工单件,只在「都声明了」这一条里出现;自己那组断言在文件尾 */
|
||||
const BOARD_VIEW = 'activity-ledger';
|
||||
const byAddress = (domain: string, object: string) => manifest.declarations.ledger.find((l) => l.domain === domain && l.object === object);
|
||||
const viewHtml = (name: string) => manifest.assets.viewTemplates.find((t) => t.name === name)?.html ?? '';
|
||||
|
||||
@@ -67,8 +73,8 @@ describe('清单本身合规', () => {
|
||||
expect(newCustomer).toBeDefined();
|
||||
});
|
||||
|
||||
it('五张视图模版都声明了,剧本每一步引用的 view 都在其中', () => {
|
||||
expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set(VIEWS));
|
||||
it('五张工单视图 + 一张运营侧看板都声明了,剧本每一步引用的 view 都在其中', () => {
|
||||
expect(new Set(manifest.assets.viewTemplates.map((v) => v.name))).toEqual(new Set([...VIEWS, BOARD_VIEW]));
|
||||
for (const step of manifest.assets.businessModel.steps as ReadonlyArray<{ view?: string }>) {
|
||||
if (step.view !== undefined) expect(VIEWS, step.view).toContain(step.view);
|
||||
}
|
||||
@@ -158,7 +164,7 @@ describe('案例模型与清单的对齐', () => {
|
||||
expect((manifest.assets.businessModel.steps as ReadonlyArray<{ item: string }>).map((s) => s.item)).toEqual(ITEMS);
|
||||
});
|
||||
|
||||
it('五处不给 doneWhen(两张数据工单 / 输入确认 / 审核 / 放行),其余四处都给', () => {
|
||||
it('三处不给 doneWhen(输入确认 / 审核 / 放行),其余六处都给 —— 两张数据工单的只读入库事实', () => {
|
||||
for (const s of manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; doneWhen?: string }>) {
|
||||
if (NO_DONE_WHEN.has(s.item)) expect(s.doneWhen, s.item).toBeUndefined();
|
||||
else expect(s.doneWhen, s.item).toBeDefined();
|
||||
@@ -313,6 +319,23 @@ describe('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', ()
|
||||
{ view: 'landing-confirm', domain: 'substitution', object: 'relation', column: 'analysis_status' },
|
||||
{ view: 'landing-confirm', domain: 'substitution', object: 'relation', column: 'risk_level' },
|
||||
{ view: 'landing-confirm', domain: 'demand', object: 'request', column: 'request_status' },
|
||||
/* 运营侧看板把六张表的枚举列全摆成徽标 —— 每一列的每个值都得有文案,漏一个就是一枚「—」 */
|
||||
{ view: BOARD_VIEW, domain: 'parts', object: 'profile', column: 'lifecycle_status' },
|
||||
{ view: BOARD_VIEW, domain: 'parts', object: 'document', column: 'document_type' },
|
||||
{ view: BOARD_VIEW, domain: 'parts', object: 'document', column: 'document_status' },
|
||||
{ view: BOARD_VIEW, domain: 'demand', object: 'request', column: 'request_status' },
|
||||
{ view: BOARD_VIEW, domain: 'demand', object: 'request', column: 'risk_level' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'relation', column: 'substitution_type' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'relation', column: 'recommendation_level' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'relation', column: 'analysis_status' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'relation', column: 'risk_level' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'difference_item', column: 'constraint_type' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'difference_item', column: 'difference_status' },
|
||||
{ view: BOARD_VIEW, domain: 'substitution', object: 'difference_item', column: 'risk_level' },
|
||||
{ view: BOARD_VIEW, domain: 'validation', object: 'record', column: 'validation_type' },
|
||||
{ view: BOARD_VIEW, domain: 'validation', object: 'record', column: 'validation_status' },
|
||||
{ view: BOARD_VIEW, domain: D, object: 'pn_check', column: 'role' },
|
||||
{ view: BOARD_VIEW, domain: D, object: 'pn_check', column: 'exists_status' },
|
||||
];
|
||||
|
||||
it('每张模版摆了徽标的每个 tally 列,声明里的每个值都有对应的 v-<token> 规则(选择器是从值算出来的,不是手写的)', () => {
|
||||
@@ -400,3 +423,134 @@ const CONTRACT = {
|
||||
prohibitions: [],
|
||||
discretion: [],
|
||||
} as const;
|
||||
|
||||
/* 验收件题头是验收绿(2026-08-27 用户定:与对话区验收卡同色,不再是目标件的红) */
|
||||
describe('两张验收件的主题色', () => {
|
||||
it('ACC1 / ACC2 的 --ledger 是验收绿 #0aa294,题头渐变不再带红;CONDITIONAL 徽标保留语义红', async () => {
|
||||
const { ACC1_CSS, ACC2_CSS } = await import('../src/views/target-style.ts');
|
||||
for (const css of [ACC1_CSS, ACC2_CSS]) {
|
||||
expect(css).toContain('--ledger:#0aa294');
|
||||
expect(css).not.toContain('--ledger:#b23a2e');
|
||||
expect(css).not.toContain('#fdf3f1');
|
||||
expect(css).toContain('rgba(10,162,148,.25)');
|
||||
}
|
||||
// ACC2 的 CONDITIONAL 徽标保留目标件的语义红;ACC1 目标件 2026-08-27 改版把它改成了品牌蓝,照抄
|
||||
expect(ACC2_CSS).toContain('.st.cond{color:#b23a2e');
|
||||
expect(ACC1_CSS).toContain('.st.cond{color:var(--brand-deep)');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 差异评估结论审核 · 目标件 2026-08-27 改版:「一个候选一张表」(用户:存在多个替代 PN 要对比差异时看不清楚)。
|
||||
* 外层按 relation 逐个复制 <section class="cand">,里层明细只列该候选名下的行(data-by 父键过滤);
|
||||
* 汇总表一个候选一条;品牌从 parts.profile 整表按料号挑(取值,不是算)。
|
||||
*/
|
||||
describe('difference-review:一个候选一张表', () => {
|
||||
const html = viewHtml('difference-review');
|
||||
it('候选块是嵌套行模版:外层 relation、里层明细按 substitution_relation_id 过滤;容器标签不在行模版里再出现', () => {
|
||||
expect(html).toContain('<article class="cands" data-rows="mine:substitution.relation"><section class="cand">');
|
||||
expect(html).toContain('data-rows="mine:substitution.difference_item" data-by="substitution_relation_id"');
|
||||
const inner = html.slice(html.indexOf('<section class="cand">'), html.indexOf('</section></article>'));
|
||||
expect(inner).not.toContain('<article');
|
||||
expect(inner).toContain('class="cand-idx"');
|
||||
expect(inner).toContain('data-slot="original_internal_pn"');
|
||||
});
|
||||
it('汇总表一个候选一条:推荐程度 / 风险级别按值上色,差异统计绑 difference_summary,品牌按料号从 parts.profile 挑', () => {
|
||||
expect(html).toContain('<tbody data-rows="mine:substitution.relation"><tr>');
|
||||
expect(html).toContain('<span class="rec" data-class-of="recommendation_level"></span>');
|
||||
expect(html).toContain('class="stat" data-slot="difference_summary"');
|
||||
expect(html).toContain('data-rows="parts.profile" data-by="internal_pn=candidate_internal_pn"');
|
||||
expect(html).toContain('data-slot="substitution_relation_rows"');
|
||||
expect(html).not.toContain('【AI 产出的差异明细】');
|
||||
});
|
||||
});
|
||||
|
||||
/* 评估输入清单的件上摆「候选参数现值」:一颗料一张、里层按料号过滤、只读(没有 data-edit) */
|
||||
describe('eval-input-list:候选参数现值只读参考', () => {
|
||||
it('外层按核对表逐颗复制,里层 parts.parameter 按 internal_pn 过滤,不是可编辑表', () => {
|
||||
const html = viewHtml('eval-input-list');
|
||||
expect(html).toContain(`<article class="prefs" data-rows="mine:${D}.pn_check"><section class="pref">`);
|
||||
expect(html).toContain('<tbody data-rows="parts.parameter" data-by="internal_pn"><tr>');
|
||||
const block = html.slice(html.indexOf('<article class="prefs"'), html.indexOf('</section></article>'));
|
||||
expect(block).not.toContain('data-edit=');
|
||||
expect(block).toContain('data-slot="param_value"');
|
||||
expect(html).toContain('【候选参数现值】');
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* 运营中心沙箱看板(2026-08-27 用户定:替换全流程分支图,只摆最近一次已放行实例的关键台账落账快照)。
|
||||
* 模版归活动,「哪几行是这一单的」也归活动 —— `data` 声明:案号怎么铸、哪张表按哪一列圈;基座只照它查正式表。
|
||||
*/
|
||||
describe('activity-ledger:关键台账数据看板', () => {
|
||||
const board = manifest.assets.viewTemplates.find((v) => v.name === BOARD_VIEW);
|
||||
const html = board?.html ?? '';
|
||||
const data = board?.data;
|
||||
const declaredAddresses = new Set(manifest.declarations.ledger.map((l) => `${l.domain}.${l.object}`));
|
||||
|
||||
it('取数声明:案号台账是 record 且 key 是它的 caseKey、前缀 W05-;每条 rows 的表都已声明;from 只指 case 或前面的 as.列', () => {
|
||||
expect(data).toBeDefined();
|
||||
if (data === undefined) return;
|
||||
const caseLedger = manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === data.case.ledger);
|
||||
expect(caseLedger?.kind).toBe('record');
|
||||
expect(caseLedger?.caseKey).toBe(data.case.key);
|
||||
expect(data.case.prefix).toBe('W05-');
|
||||
const seen = new Set<string>();
|
||||
for (const entry of data.rows) {
|
||||
expect(declaredAddresses.has(entry.ledger), entry.ledger).toBe(true);
|
||||
const columns = new Set(manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === entry.ledger)?.fields.map((f) => f.name));
|
||||
expect(columns.has(entry.by.column), `${entry.as} by ${entry.by.column}`).toBe(true);
|
||||
for (const ref of [entry.by.from, entry.not?.from]) {
|
||||
if (ref === undefined || ref === 'case') continue;
|
||||
const at = ref.lastIndexOf('.');
|
||||
const as = ref.slice(0, at);
|
||||
const column = ref.slice(at + 1);
|
||||
expect(seen.has(as), `${entry.as} 引用了后面 / 不存在的集合 ${as}`).toBe(true);
|
||||
const source = data.rows.find((r) => r.as === as);
|
||||
const sourceColumns = new Set(manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === source?.ledger)?.fields.map((f) => f.name));
|
||||
expect(sourceColumns.has(column), `${ref} 不是 ${source?.ledger} 的列`).toBe(true);
|
||||
}
|
||||
seen.add(entry.as);
|
||||
}
|
||||
// 六张主表 + 案号两张 + 同料号历史都圈了
|
||||
expect([...seen]).toEqual(
|
||||
expect.arrayContaining([`${D}.verdict`, `${D}.pn_check`, 'demand.request', 'customer.profile', 'parts.profile', 'parts.parameter', 'parts.document', 'substitution.relation', 'substitution.difference_item', 'validation.record', 'validation.history']),
|
||||
);
|
||||
});
|
||||
|
||||
it('html 里每个 data-rows 键都是声明里的一个 as(行由基座圈过,不是整表);嵌套只用 data-by 父键过滤', () => {
|
||||
const as = new Set(data?.rows.map((r) => r.as) ?? []);
|
||||
const keys = [...html.matchAll(/data-rows="([^"]+)"/g)].map((m) => m[1] ?? '');
|
||||
expect(keys.length).toBeGreaterThan(8);
|
||||
for (const key of keys) expect(as.has(key), key).toBe(true);
|
||||
expect(html).toContain('<span class="pnrow" data-rows="sub_eval.pn_check" data-by="internal_pn">');
|
||||
expect(html).toContain('<div class="pchips" data-rows="parts.parameter" data-by="internal_pn">');
|
||||
expect(html).toContain('<tbody class="hist" data-rows="validation.history">');
|
||||
expect(html).not.toContain('mine:');
|
||||
});
|
||||
|
||||
it('无脚本、无 on*、无外链(过安装静态审);有 </body> 给出口注入报高脚本;三态由 board:state 投影;只读', () => {
|
||||
expect(html.length).toBeGreaterThan(5000);
|
||||
expect(html).not.toMatch(/<\s*script\b/i);
|
||||
expect(html).not.toMatch(/\son[a-z]+\s*=/i);
|
||||
expect(html).not.toMatch(/(src|href)\s*=\s*["']?\s*(https?:)?\/\//i);
|
||||
expect(html.toLowerCase()).toContain('</body>');
|
||||
expect(html).toContain('<div class="page" data-class-of="board:state">');
|
||||
expect(html).toContain('.page.v-none .notice.none{display:flex}');
|
||||
expect(html).toContain('.page.v-missing .notice.missing{display:flex}');
|
||||
expect(html).not.toContain('data-edit=');
|
||||
expect(html).not.toContain('data-col=');
|
||||
});
|
||||
|
||||
it('目标件六张表的表头与页头要素都在;MATCH 行淡显、放弃的候选只在有 abandoned 时露面 —— 都是 class 交样式表,模版不判', () => {
|
||||
for (const header of ['<th>内部料号</th>', '<th>替代关系 ID</th>', '<th>参数项</th>', '<th>关联料号</th>', '<th>验证 ID</th>', '<th>标准化参数集</th>']) expect(html).toContain(header);
|
||||
expect(html).toContain('data-slot="case:instanceNo"');
|
||||
expect(html).toContain('data-slot="case:ref"');
|
||||
expect(html).toContain(`data-slot="${D}.verdict.original_internal_pn"`);
|
||||
expect(html).toContain('data-slot="demand.request.request_id"');
|
||||
expect(html).toContain('<tr data-class-of="difference_status">');
|
||||
expect(html).toContain('tr.v-match td{opacity:.55}');
|
||||
expect(html).toContain('.aband:has(i.v-abandoned){display:block}');
|
||||
expect(html).toContain('data-slot="substitution_difference_item_difference_status_unknown"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 >= sub_eval_pn_check_exists_status_created');
|
||||
expect(CMMN_XML).toContain('parts_profile_rows >= 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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user