Archived
feat(w05): 清单重写 —— 九张台账、两个启动参数、零工具
台账:两张类型字典 + P-08 主/参数(撤 createOnly、封装工艺归字典)+ 案卷两张 + P-11 主/明细 + P-06。 契约:启动只剩原 PN 与候选 PN 两格且都选填(缺了由第一步派工单补);tools 清空 —— 这是立场不是疏漏。 判据全读基座数;明细加合成列 judgement_class,因为「硬约束不符有几条」不是单列 tally 数得出来的。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { activityManifestSchema } from '@contracts';
|
||||
import { createMockHost } from '@sdk';
|
||||
import { manifest, D, RULE_VERSION } from '../src/manifest.ts';
|
||||
import { manifest, D } 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'];
|
||||
@@ -23,6 +23,9 @@ const VIEWS = ['p08-master-create', 'p10-document-create', 'eval-input-list', 'd
|
||||
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 ?? '';
|
||||
/* 「台账声明」组用的短写(2026-08-29 重写引入):地址是 `域.对象` 一整串,`byAddress` 拆两段 */
|
||||
const ledgerOf = (address: string) => manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === address);
|
||||
const colOf = (address: string, name: string) => ledgerOf(address)?.fields.find((f) => f.name === name);
|
||||
|
||||
describe('清单本身合规', () => {
|
||||
it('过 @contracts 的 schema', () => {
|
||||
@@ -33,12 +36,13 @@ describe('清单本身合规', () => {
|
||||
expect(manifest.declarations.listeners).toEqual([]);
|
||||
});
|
||||
|
||||
it('L1/L2 判据都给了谓词;L4 三条不给(裁量项);L5 恰一条', () => {
|
||||
/* 2026-08-29 重写:spec §7 的判据表从九条改成八条(五条机检 L1/L2 + 两条 L4 裁量 + 一条 L5),L4 从三条收到两条 —— 证据链与硬约束两条留 L4,「推荐等级与明细一致」已升级成 L2 机检(level-by-rule) */
|
||||
it('L1/L2 判据都给了谓词;L4 两条不给(裁量项);L5 恰一条', () => {
|
||||
for (const c of manifest.contract.acceptanceCriteria) {
|
||||
if (c.level === 'L1' || c.level === 'L2') expect(c.predicate, c.key).toBeDefined();
|
||||
if (c.level === 'L4') expect(c.predicate, c.key).toBeUndefined();
|
||||
}
|
||||
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(3);
|
||||
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L4')).toHaveLength(2);
|
||||
expect(manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L5')).toHaveLength(1);
|
||||
});
|
||||
|
||||
@@ -49,21 +53,31 @@ describe('清单本身合规', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('三件必交付物都有台账地址;人环预算严格大于「基线 8 + 两轮审核循环 4」', () => {
|
||||
expect(manifest.contract.requiredDeliverables).toHaveLength(3);
|
||||
/*
|
||||
* 2026-08-29 重写:必交付物从三件(评估输入清单 / 差异评估结论 / 拟落账内容)收到两件 ——
|
||||
* 「评估输入清单」那件连同它绑的三张旧单据(pn_check/input_gap/verdict)一起退休。
|
||||
* 人环预算从 16(候选放弃/创建决策等岔路)收到 8(补数据 / 审核 / 放行三次调人,零工具、
|
||||
* AI 只出场一次)—— 见「契约 > 预算」那条把 8 钉死;这里只需两个数上下一致。
|
||||
*/
|
||||
it('两件必交付物都有台账地址;人环预算恰为 8', () => {
|
||||
expect(manifest.contract.requiredDeliverables).toHaveLength(2);
|
||||
for (const d of manifest.contract.requiredDeliverables) expect(d.ledger, d.key).toBeDefined();
|
||||
expect(manifest.contract.authorization.humanCallBudget).toBeGreaterThan(12);
|
||||
expect(manifest.contract.resources.budget.humanCalls).toBeGreaterThan(12);
|
||||
expect(manifest.contract.authorization.humanCallBudget).toBe(8);
|
||||
expect(manifest.contract.resources.budget.humanCalls).toBe(8);
|
||||
});
|
||||
|
||||
it('起单一格必填都不设(2026-08-28 用户定);客户两格同日从启动表单撤下;datasheet 是选填文件', () => {
|
||||
/*
|
||||
* 2026-08-29 重写:启动表单从「需求 + 原PN + 候选数组 + datasheet」四件收到「原PN + 候选」两件
|
||||
* (单数,一次评估恰一对一候选)—— `requestDescription` / `candidateInternalPns`(数组)/
|
||||
* `datasheets` 三键随 P-01 需求主记录一起撤下(本活动不再碰它)。逐条断言见「契约 > 启动两参」。
|
||||
*/
|
||||
it('起单一格必填都不设(2026-08-28 用户定);客户与需求两类字段已随重写从启动表单撤下', () => {
|
||||
expect(manifest.contract.input.data['required']).toEqual([]);
|
||||
const launchProps = Object.keys(manifest.contract.input.data['properties'] as Record<string, unknown>);
|
||||
expect(launchProps).not.toContain('customerCode');
|
||||
expect(launchProps).not.toContain('customerName');
|
||||
const properties = manifest.contract.input.data['properties'] as Record<string, { type?: string; format?: string }>;
|
||||
expect(properties['candidateInternalPns']?.type).toBe('array');
|
||||
expect(properties['datasheets']?.format).toBe('file');
|
||||
expect(launchProps).not.toContain('requestDescription');
|
||||
expect(launchProps).not.toContain('datasheets');
|
||||
});
|
||||
|
||||
it('五条样例:第一条满载走数据工单那条路,名字 ≤14 字,形状各不相同', () => {
|
||||
@@ -76,7 +90,13 @@ describe('清单本身合规', () => {
|
||||
expect(newCustomer).toBeDefined();
|
||||
});
|
||||
|
||||
it('五张工单视图 + 一张运营侧看板都声明了,剧本每一步引用的 view 都在其中', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2):`assets.viewTemplates` 已按 spec §8 暂时只留 `activity-ledger`
|
||||
* 一张(五张旧局面全部作废,它们绑的是已退休的九步与旧台账地址)—— Task 7–9 会各自加回
|
||||
* `input-supplement` / `difference-review` / `landing-confirm` 三张新件,那时再补一条按新
|
||||
* 视图集合钉死的断言。这条转性检查在过渡期间的意义不大,先跳过、不删。
|
||||
*/
|
||||
it.skip('五张工单视图 + 一张运营侧看板都声明了,剧本每一步引用的 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);
|
||||
@@ -84,79 +104,120 @@ describe('清单本身合规', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('台账声明:授权面与《统一方案》口径', () => {
|
||||
it('P-08 主键 internal_pn,标准化参数集拆成独立子表 parts.parameter(2026-08-27 结构改:与目标件一致)', () => {
|
||||
const p08 = byAddress('parts', 'profile');
|
||||
expect(p08?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn']);
|
||||
expect(p08?.fields.find((f) => f.name === 'normalized_parameter_set')).toBeUndefined();
|
||||
const param = byAddress('parts', 'parameter');
|
||||
expect(param?.kind).toBe('master');
|
||||
expect(param?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn', 'param_name']);
|
||||
expect(param?.fields.find((f) => f.name === 'internal_pn')?.refersTo).toBe('parts.profile.internal_pn');
|
||||
/*
|
||||
* 参数名自指引用:只能用台账里已经在用的那几个 —— 2026-08-27 实测三次,
|
||||
* 模型给新料写英文 key,剧本写过两遍 + 一条乘法对账都拦不住「只建一颗」那一路。
|
||||
*/
|
||||
const pn = param?.fields.find((f) => f.name === 'param_name');
|
||||
expect(pn?.refersTo).toBe('parts.parameter.param_name');
|
||||
expect(pn?.distinct).toBe(true);
|
||||
expect(param?.fields.find((f) => f.name === 'param_value')?.required).toBe(true);
|
||||
expect(param?.fields.find((f) => f.name === 'constraint_type')?.tally).toEqual(['hard', 'soft']);
|
||||
expect(param?.fields.find((f) => f.name === 'constraint_type')?.required).toBe(true);
|
||||
describe('台账声明', () => {
|
||||
it('恰好九条声明(七个业务对象,P-08 与 P-11 各是主表 + 子表)', () => {
|
||||
expect(manifest.declarations.ledger.map((l) => `${l.domain}.${l.object}`).sort()).toEqual([
|
||||
'parts.chip_type', 'parts.parameter', 'parts.profile', 'parts.type_param',
|
||||
'sub_eval.case', 'sub_eval.part',
|
||||
'substitution.difference_item', 'substitution.relation', 'validation.record',
|
||||
].sort());
|
||||
});
|
||||
|
||||
it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => {
|
||||
const rel = byAddress('substitution', 'relation');
|
||||
for (const name of ['original_internal_pn', 'candidate_internal_pn']) {
|
||||
expect(rel?.fields.find((f) => f.name === name)?.refersTo, name).toBe('parts.profile.internal_pn');
|
||||
it('类型字典:type_id 是自增主键 —— 人在件上新建类型时留空,由基座续号', () => {
|
||||
expect(colOf('parts.chip_type', 'type_id')).toMatchObject({ primaryKey: true, type: 'integer', auto: true });
|
||||
expect(colOf('parts.chip_type', 'type_name')?.required).toBe(true);
|
||||
});
|
||||
|
||||
it('字段字典:复合主键 + 约束类型长在这里(不在每颗料上)', () => {
|
||||
expect(ledgerOf('parts.type_param')?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['type_id', 'param_name']);
|
||||
expect(colOf('parts.type_param', 'type_id')?.refersTo).toBe('parts.chip_type.type_id');
|
||||
expect(colOf('parts.type_param', 'param_desc')?.required).toBe(true);
|
||||
expect(colOf('parts.type_param', 'constraint_type')).toMatchObject({ required: true, tally: ['hard', 'soft'] });
|
||||
});
|
||||
|
||||
it('P-08:类型必填并引用字典;createOnly 撤掉 —— 在库老料要能补上类型这一格', () => {
|
||||
expect(colOf('parts.profile', 'chip_type_id')).toMatchObject({ required: true, type: 'integer', refersTo: 'parts.chip_type.type_id' });
|
||||
expect(ledgerOf('parts.profile')?.createOnly).toBeUndefined();
|
||||
/* 封装与工艺本来就是「核心参数」—— 它们该以类型字段的身份出现在字典里,不在主记录上另开一列 */
|
||||
for (const gone of ['package', 'process_technology', 'eol_pcn_status', 'application_scenarios']) {
|
||||
expect(colOf('parts.profile', gone)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('客户批准状态**不在声明里**:本活动没有它的写权(只读边界)', () => {
|
||||
const rel = byAddress('substitution', 'relation');
|
||||
expect(rel?.fields.map((f) => f.name)).not.toContain('customer_approval_status');
|
||||
it('参数子表:约束类型与自指引用墙都退休了', () => {
|
||||
expect(colOf('parts.parameter', 'constraint_type')).toBeUndefined();
|
||||
expect(colOf('parts.parameter', 'param_name')?.refersTo).toBeUndefined();
|
||||
expect(colOf('parts.parameter', 'param_name')?.distinct).toBeUndefined();
|
||||
expect(colOf('parts.parameter', 'param_value')?.required).toBe(true);
|
||||
expect(ledgerOf('parts.parameter')?.createOnly).toBeUndefined();
|
||||
});
|
||||
|
||||
it('UNKNOWN 必有去向:unknown_disposition 钉在 difference_status = unknown 的行上;四态是 ASCII tally', () => {
|
||||
const item = byAddress('substitution', 'difference_item');
|
||||
expect(item?.fields.find((f) => f.name === 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' });
|
||||
expect(item?.fields.find((f) => f.name === 'difference_status')?.tally).toEqual(['match', 'mismatch', 'unknown', 'conditional']);
|
||||
expect(item?.fields.find((f) => f.name === 'substitution_relation_id')?.distinct).toBe(true);
|
||||
it('案卷两张:part 一单两行(配方遍历的唯一来源)、case 一单一行', () => {
|
||||
expect(ledgerOf('sub_eval.part')?.caseKey).toBe('eval_ref');
|
||||
expect(ledgerOf('sub_eval.part')?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['eval_ref', 'internal_pn']);
|
||||
/* 「两颗料是不是同一个类型」于是成了基座数得出来的数:不同取值恰好 1 种 */
|
||||
expect(colOf('sub_eval.part', 'chip_type_id')).toMatchObject({ required: true, distinct: true });
|
||||
expect(colOf('sub_eval.part', 'exists_status')?.tally).toEqual(['found', 'absent']);
|
||||
expect(ledgerOf('sub_eval.case')?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['eval_ref']);
|
||||
});
|
||||
|
||||
it('P-01 回写五项钉在 current_stage = gate03 那一刻(放行前不拦,放行门 L2 数得出来)', () => {
|
||||
const p01 = byAddress('demand', 'request');
|
||||
for (const name of ['risk_level', 'verification_requirement', 'output_ref', 'next_action', 'current_activity']) {
|
||||
expect(p01?.fields.find((f) => f.name === name)?.requiredWhen, name).toEqual({ column: 'current_stage', value: 'gate03' });
|
||||
it('P-11 明细带合成列 judgement_class —— 推荐等级要数的是「硬约束不符有几条」', () => {
|
||||
expect(colOf('substitution.difference_item', 'judgement_class')).toMatchObject({
|
||||
required: true,
|
||||
tally: ['match', 'soft_mismatch', 'hard_mismatch', 'unknown', 'conditional'],
|
||||
});
|
||||
/* 「判断依据」不是文件引用 —— 不写 P-10 了,名字得说实话 */
|
||||
expect(colOf('substitution.difference_item', 'evidence_note')?.required).toBe(true);
|
||||
expect(colOf('substitution.difference_item', 'evidence_ref')).toBeUndefined();
|
||||
expect(colOf('substitution.difference_item', 'unknown_disposition')?.requiredWhen).toEqual({ column: 'difference_status', value: 'unknown' });
|
||||
});
|
||||
|
||||
it('P-11 主记录:退休三列,等级只剩三档', () => {
|
||||
for (const gone of ['substitution_type', 'confidence_score', 'analysis_report_ref']) {
|
||||
expect(colOf('substitution.relation', gone)).toBeUndefined();
|
||||
}
|
||||
expect(colOf('substitution.relation', 'recommendation_level')?.tally).toEqual(['recommended', 'conditional', 'not_recommended']);
|
||||
});
|
||||
|
||||
it('P-06:关联的是本单案号(P-01 不在本活动里),类型加了 parametric 一档', () => {
|
||||
expect(colOf('validation.record', 'eval_ref')?.required).toBe(true);
|
||||
expect(colOf('validation.record', 'request_id')).toBeUndefined();
|
||||
expect(colOf('validation.record', 'validation_type')?.tally).toContain('parametric');
|
||||
});
|
||||
|
||||
it('这个活动不碰 P-01 / P-10 / 客户档案', () => {
|
||||
for (const gone of ['demand.request', 'parts.document', 'customer.profile']) {
|
||||
expect(ledgerOf(gone)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('契约', () => {
|
||||
it('启动两参,都选填 —— 缺了由第一步派工单补', () => {
|
||||
const props = manifest.contract.input.data['properties'] as Record<string, Record<string, unknown>>;
|
||||
expect(Object.keys(props)).toEqual(['originalInternalPn', 'candidateInternalPn']);
|
||||
expect(manifest.contract.input.data['required']).toEqual([]);
|
||||
/* 运营端启动表单:两格都是「可选可新写」的下拉 */
|
||||
for (const key of ['originalInternalPn', 'candidateInternalPn']) {
|
||||
expect(props[key]?.['x-control']).toBe('combo');
|
||||
expect(props[key]?.['x-options']).toMatchObject({ ledger: 'parts.profile', value: 'internal_pn' });
|
||||
}
|
||||
});
|
||||
|
||||
it('自持单据都带 caseKey eval_ref 且在主键里;放弃候选必须写原因', () => {
|
||||
for (const object of ['pn_check', 'input_gap', 'verdict']) {
|
||||
const t = byAddress(D, object);
|
||||
expect(t?.kind, object).toBe('record');
|
||||
expect(t?.caseKey, object).toBe('eval_ref');
|
||||
expect(t?.fields.find((f) => f.name === 'eval_ref')?.primaryKey, object).toBe(true);
|
||||
it('零工具 —— 这是这次重写的立场,不是疏漏', () => {
|
||||
expect(manifest.contract.resources.tools).toEqual([]);
|
||||
expect(manifest.declarations.tools).toEqual([]);
|
||||
});
|
||||
|
||||
it('单一出口,载荷是已落地的事实', () => {
|
||||
expect(manifest.contract.output.mandatory.map((p) => p.name)).toEqual(['assessment-released']);
|
||||
expect(manifest.contract.output.selective).toEqual([]);
|
||||
});
|
||||
|
||||
it('判据全读基座数 —— 任何一边都不许是活动自己写的汇总列', () => {
|
||||
const machine = manifest.contract.acceptanceCriteria.filter((c) => c.level === 'L1' || c.level === 'L2');
|
||||
expect(machine.length).toBeGreaterThanOrEqual(5);
|
||||
for (const c of machine) {
|
||||
expect(c.predicate).toBeDefined();
|
||||
/* 基座读数的形状:域_对象_rows / _missing / _distinct_列 / tally 计数 */
|
||||
expect(c.predicate).toMatch(/_(rows|missing|distinct_[a-z_]+|[a-z_]+)\b/);
|
||||
}
|
||||
expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' });
|
||||
});
|
||||
|
||||
it('三张主数据表标 createOnly:数据工单只建库里没有的料,在库的写下来当场拒(2026-08-27 实测:模型在数据工单里多铺了一行不该建的料号,件上多出一行要人填)', () => {
|
||||
for (const obj of ['profile', 'parameter', 'document'] as const) expect(byAddress('parts', obj)?.createOnly, obj).toBe(true);
|
||||
// 凭空拼出来的料号(实测 XL2596S-ADJ)由这一道管:只许建核对表里核过的
|
||||
expect(byAddress('parts', 'profile')?.fields.find((f) => f.name === 'internal_pn')?.refersTo).toBe(`${D}.pn_check.internal_pn`);
|
||||
});
|
||||
|
||||
it('判「在库」必须填得出引用依据,基座拿它去主数据里查(2026-08-27 实测:原 PN 不在库而模型把两颗都写成 found,硬阻塞那条路整个被跳过)', () => {
|
||||
const ref = byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'found_pn_ref');
|
||||
expect(ref?.refersTo).toBe('parts.profile.internal_pn');
|
||||
expect(ref?.requiredWhen).toEqual({ column: 'exists_status', value: 'found' });
|
||||
expect(ref?.primaryKey ?? false).toBe(false);
|
||||
});
|
||||
|
||||
it('规则版本常量随包冻结', () => {
|
||||
expect(RULE_VERSION).toBe('R-2026.07');
|
||||
expect(manifest.assets.businessModel.dmn.tables.map((t) => t.id)).toEqual(['d01-difference-status', 'd02-recommendation']);
|
||||
it('预算:调人三次(补数据 / 审核 / 放行),模型只出场一次', () => {
|
||||
expect(manifest.contract.resources.budget.humanCalls).toBe(8);
|
||||
expect(manifest.contract.authorization.humanCallBudget).toBe(8);
|
||||
expect(manifest.contract.resources.models).toEqual(['deepseek-v4-flash']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,7 +270,13 @@ describe('案例模型与清单的对齐', () => {
|
||||
expect(n).toContain('custom:');
|
||||
});
|
||||
|
||||
it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 →Task 3 补回):`contract.acceptanceCriteria` 已按 spec §7 整体重写,
|
||||
* 旧的九步 T_release 剧本(`case-model.ts`,本任务不动)引的还是旧的三条 L4 label
|
||||
* (「本单数据工单的创建记录」等旧措辞)。Task 3 整体重写四步案例模型时,新 T_release 会照新的
|
||||
* 两条 L4 判据重新引用,那时再放开这条断言。
|
||||
*/
|
||||
it.skip('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => {
|
||||
const tRelease = (manifest.assets.businessModel.steps as ReadonlyArray<{ item: string; guide?: string }>).find((s) => s.item === 'T_release');
|
||||
const quoted = [...(tRelease?.guide ?? '').matchAll(/judge criterion="([^"]+)"/g)].map((m) => m[1] ?? '');
|
||||
expect(quoted).toHaveLength(3);
|
||||
@@ -219,7 +286,13 @@ describe('案例模型与清单的对齐', () => {
|
||||
});
|
||||
|
||||
describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
it('P-08 数据工单件的表 1 + 表 2(标准化参数子表)都可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 7 补回):`p08-master-create` / `p10-document-create` /
|
||||
* `eval-input-list` 三张局面已随重写从 `assets.viewTemplates` 撤下(spec §8:五张旧件全部作废,
|
||||
* 新的 `input-supplement` 由 Task 7 重做)。`viewHtml()` 现在对这三个名字一律返回空串,
|
||||
* 下面几条对 HTML 结构的断言无从谈起 —— 跳过、不删,等 Task 7–9 补回视图再验。
|
||||
*/
|
||||
it.skip('P-08 数据工单件的表 1 + 表 2(标准化参数子表)都可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
|
||||
const p08 = viewHtml('p08-master-create');
|
||||
expect(p08).toContain('data-edit="parts.profile"');
|
||||
expect(p08).toContain('data-edit-add="parts.profile"');
|
||||
@@ -234,7 +307,8 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
expect(p10).toContain('data-col="doc_seq"');
|
||||
});
|
||||
|
||||
it('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => {
|
||||
/* SKIP(2026-08-29,Task 2 → Task 7):`eval-input-list` 局面已撤下,`sub_eval.input_gap` / `sub_eval.pn_check` 两张单据也已随重写退休(顶掉它们的是 `sub_eval.part` / `sub_eval.case`) */
|
||||
it.skip('评估输入清单:缺口表可编辑(supplement),绑定区只读', () => {
|
||||
const html = viewHtml('eval-input-list');
|
||||
expect(html).toContain(`data-edit="${D}.input_gap"`);
|
||||
expect(html).toContain('data-col="supplement"');
|
||||
@@ -261,7 +335,8 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
|
||||
/* SKIP(2026-08-29,Task 2 → Task 8/9):`difference-review` / `landing-confirm` 两张局面已撤下(新的两张由 Task 8/9 重做);它们绑的字段(recommendation_level 已只剩三档、analysis_status 已不带 tally)到时也会一并对齐 */
|
||||
it.skip('枚举列只用 data-class-of 投影,不摆 ASCII 原值', () => {
|
||||
expect(viewHtml('difference-review')).toContain('data-class-of="difference_status"');
|
||||
expect(viewHtml('difference-review')).toContain('data-class-of="recommendation_level"');
|
||||
expect(viewHtml('difference-review')).not.toContain('data-slot="difference_status"');
|
||||
@@ -277,7 +352,8 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
await expect(host.base.interaction.pushHtml('<div><script>x()</script></div>')).rejects.toThrow(/无脚本执行权/);
|
||||
});
|
||||
|
||||
it('五张模版的外壳都是搬自目标件的 page/card,不再含 E04/I01 那套视觉底(.proto-root)的痕迹', () => {
|
||||
/* SKIP(2026-08-29,Task 2 → Task 7-9):VIEWS 列的五张局面已全部撤下,`assets.viewTemplates` 现在只有运营侧看板一张 */
|
||||
it.skip('五张模版的外壳都是搬自目标件的 page/card,不再含 E04/I01 那套视觉底(.proto-root)的痕迹', () => {
|
||||
for (const view of VIEWS) {
|
||||
const html = viewHtml(view);
|
||||
expect(html, view).toContain('class="card-head"');
|
||||
@@ -298,7 +374,13 @@ describe('报告模版:编辑面与只读面各就各位', () => {
|
||||
* ② **缺口表补充内容每一行都摆红「必填」,而闸只认 3 行**——`requiredWhen` 是按行不按列,
|
||||
* 静态 `class="req"` 把整列都标了必填。
|
||||
*/
|
||||
describe('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 7-9 补回):`BADGED` 列表里的每一项都指着已撤下的五张局面
|
||||
* (p08-master-create 等)或已改动的字段(recommendation_level 掉了 alternate 档、
|
||||
* analysis_status 不再带 tally、`sub_eval.pn_check` 整个改名 `sub_eval.part`)。
|
||||
* 这整组「回归网」要等 Task 7–9 把新三张件补回来、字段清单也重新对齐后才谈得上继续钉。
|
||||
*/
|
||||
describe.skip('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', () => {
|
||||
/** 必须与 `apps/api/src/modules/interaction/internal/domain/view-render.ts` 的 `classToken` 一字不差 */
|
||||
const vtok = (v: string): string => v.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24);
|
||||
|
||||
@@ -370,7 +452,12 @@ describe('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', ()
|
||||
* 提交闸永远拦着,这是「墙不配路」的死路形态。
|
||||
* 两条各配一张回归网,防的是同一类事故再出现在别的字段上。
|
||||
*/
|
||||
describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归)', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 7 补回):`EDITABLE` 列表指着的三张局面(p08-master-create /
|
||||
* p10-document-create / eval-input-list)与 `sub_eval.input_gap` 都已随重写撤下 —— 这两条
|
||||
* Critical 回归网等 Task 7 重做数据工单件时再钉在新件上。
|
||||
*/
|
||||
describe.skip('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 / 必填填不了,两条 Critical 回归)', () => {
|
||||
/** 三张真能编辑的件与它们各自绑定的台账对象 */
|
||||
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
|
||||
{ view: 'p08-master-create', domain: 'parts', object: 'profile' },
|
||||
@@ -448,7 +535,11 @@ describe('两张验收件的主题色', () => {
|
||||
* 外层按 relation 逐个复制 <section class="cand">,里层明细只列该候选名下的行(data-by 父键过滤);
|
||||
* 汇总表一个候选一条;品牌从 parts.profile 整表按料号挑(取值,不是算)。
|
||||
*/
|
||||
describe('difference-review:一个候选一张表', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 8 补回):`difference-review` 局面已随重写撤下,
|
||||
* 新版由 Task 8 重做(件② `difference-review` —— 差异评估结论审核)。
|
||||
*/
|
||||
describe.skip('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">');
|
||||
@@ -469,7 +560,11 @@ describe('difference-review:一个候选一张表', () => {
|
||||
});
|
||||
|
||||
/* 评估输入清单的件上摆「候选参数现值」:一颗料一张、里层按料号过滤、只读(没有 data-edit) */
|
||||
describe('eval-input-list:候选参数现值只读参考', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 7 补回):`eval-input-list` 局面已随重写撤下,
|
||||
* 「候选参数现值」这块只读参考并入 Task 7 的 `input-supplement`。
|
||||
*/
|
||||
describe.skip('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">`);
|
||||
@@ -491,7 +586,14 @@ describe('activity-ledger:关键台账数据看板', () => {
|
||||
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.列', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 9 补回):看板的 `data` 声明(`views/ledger-board.ts` 的
|
||||
* `LEDGER_BOARD_DATA`)还圈着已退休的旧台账(`sub_eval.pn_check` / `sub_eval.verdict` /
|
||||
* `demand.request` / `customer.profile` / `parts.document`)—— Task 9「运营中心看板重绑」
|
||||
* 会把它换成新九张表;这条取数声明的自洽性检查到那时再验。html 自身的结构测试(下面三条)
|
||||
* 不依赖 `declarations.ledger`,仍然有效、继续跑。
|
||||
*/
|
||||
it.skip('取数声明:案号台账是 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);
|
||||
|
||||
@@ -66,7 +66,14 @@ describe('剧本声明的读写面 ⊆ 清单声明', () => {
|
||||
const declaredAddresses = new Set(manifest.declarations.ledger.map((d) => `${d.domain}.${d.object}`));
|
||||
const declaredTools = new Set(manifest.declarations.tools.map((t) => t.name));
|
||||
|
||||
it('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 3 补回):`manifest.declarations.ledger` 已按
|
||||
* `docs/superpowers/specs/2026-08-29-act-work-05-rewrite-design.md` §3 整体重写成九张新表,
|
||||
* 而 `ASSESSMENT_STEPS`(`case-model.ts`)还是旧的九步、引用的还是已退休的旧台账地址
|
||||
* (`demand.request` / `customer.profile` / `parts.document` / `sub_eval.pn_check` 等)——
|
||||
* Task 3 整体重写案例模型(四步、零 guide)时,`writes`/`reads` 会重新对齐新的九张表。
|
||||
*/
|
||||
it.skip('steps[].writes / reads(台账那几个)全落在声明过的对象上', () => {
|
||||
for (const step of ASSESSMENT_STEPS) {
|
||||
for (const address of step.writes) expect(declaredAddresses.has(address), `${step.item} 要写 ${address}`).toBe(true);
|
||||
for (const address of step.reads.filter((r) => r.includes('.') && !r.startsWith('case:'))) {
|
||||
@@ -75,7 +82,12 @@ describe('剧本声明的读写面 ⊆ 清单声明', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 3 补回):`declarations.tools` 已清空(零工具是这次重写的
|
||||
* 立场),而旧的 S3_assess 步骤仍声明着两枚已退役工具的字面量(constants.ts 头注)。
|
||||
* Task 3 重写案例模型时四步「全程零 guide、零工具」,这条断言到时会被新版覆盖。
|
||||
*/
|
||||
it.skip('steps[].tools 全部是声明过的工具;只有 S3 调工具', () => {
|
||||
for (const step of ASSESSMENT_STEPS) {
|
||||
for (const tool of step.tools) expect(declaredTools.has(tool), `${step.item} 要调 ${tool}`).toBe(true);
|
||||
if (step.item !== 'S3_assess') expect(step.tools, step.item).toEqual([]);
|
||||
@@ -407,7 +419,13 @@ describe('说明书与剧本的骨架', () => {
|
||||
expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/);
|
||||
});
|
||||
|
||||
it('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => {
|
||||
/*
|
||||
* SKIP(2026-08-29,Task 2 → Task 7 补回):`p08-master-create` 局面已随重写撤下(它的
|
||||
* 编辑态 CSS 现在无处可查)。共享的横滑规则(`.jc-tbl-wrap, .tbl-wrap { overflow-x: auto;`)
|
||||
* 与中文表头那条(下面一条测试)仍读 `manifest.assets.viewTemplates` 整体、不点名具体局面,
|
||||
* 继续钉着看板;这条要等 Task 7 重做数据工单件才谈得上继续钉编辑态细节。
|
||||
*/
|
||||
it.skip('件上三处样式毛病钉死(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 用户在结论审核页撞上
|
||||
// 运营侧的看板也在这条纪律里(它六张表都宽)
|
||||
|
||||
Reference in New Issue
Block a user