ACT-WORK-05:标准化参数集改子表 parts.parameter,与目标件页面结构对齐

用户核对实跑页面后指出:P-08 数据工单做出来的页面结构必须与目标件
《数据工单-01-芯片主数据创建》一致——目标件本就是两张表(表 1 主记录 /
表 2 标准化参数子表),而实现把参数集塞成了表 1 的一个文本列,违反
《SS02-SS03-SS04_Demo台账字段细化与统一方案》§六「重复明细用子表承载,
不在单一文本字段堆叠」。

五处联动改齐:
- 平台基线新增 parts.parameter 主数据表(内部料号+参数名复合主键),
  两颗样例料的参数集从 normalized_parameter_set 文本列拆成 7 行子表数据
  (数值原样搬,一个字未改);parts.profile 物理列保留、不再赋值。
- 活动清单:parts.profile 撤掉该字段声明,新增 parts.parameter 声明
  (internal_pn 引用墙、param_value/constraint_type 必填);版本号
  1.1.1 → 1.2.0。
- 案例模型:S0d_p08_master/S1_input_confirm/S2_normalize 的读写面与
  guide 全部改指向新子表;顺带修正候选计数措辞歧义(实测样例0一张该发
  的决策卡被跳过——"候选absent且候选数≥2"被模型读成"不在库的候选≥2颗",
  改成先分别数"候选总数"与"不在库的候选数"两个数、显式说清楚判据认哪个)。
- caseFileModel 补 parts_parameter_missing 读数,两条哨兵(s_p08_created /
  s_pns_resolved)一并要求这一列不缺必填。
- 报告模版补齐表 2(逐块搬自目标件),主键列挂 data-col,约束类型走
  data-class-of 徽标。
- e2e 套件加性支持"一张卡填两张表"(stage.extraTables),buildEdits
  的 rowKey 泛化支持复合主键数组形态(不破坏既有八套件)。

验证:vitest 413 passed(活动包+平台种子测试全绿)、typecheck/lint/
depcruise 三绿、activities:install 零静态审发现、SQL 核对
parts.parameter 主键与行数(两颗料各7行)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-27 02:19:39 +08:00
co-authored by Claude Opus 5
parent 02ef572470
commit c6b02dfd39
9 changed files with 324 additions and 141 deletions
@@ -76,11 +76,17 @@ describe('清单本身合规', () => {
});
describe('台账声明:授权面与《统一方案》口径', () => {
it('P-08 一张表:主键 internal_pn,参数集是它的一个必填字段,没有 parts.parameter 子表', () => {
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')?.required).toBe(true);
expect(byAddress('parts', 'parameter')).toBeUndefined();
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');
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);
});
it('P-11 主记录的原 PN / 候选 PN 都是引用墙(查无此料当场拒 —— 存在性检查的硬门)', () => {
@@ -186,12 +192,16 @@ describe('案例模型与清单的对齐', () => {
});
describe('报告模版:编辑面与只读面各就各位', () => {
it('两张数据工单件可加行可编辑:data-edit / data-edit-add 与主键 data-col 都在', () => {
it('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"');
expect(p08).toContain('data-col="internal_pn"');
expect(p08).toContain('data-col="normalized_parameter_set"');
expect(p08).toContain('data-edit="parts.parameter"');
expect(p08).toContain('data-edit-add="parts.parameter"');
expect(p08).toContain('data-col="param_name"');
expect(p08).toContain('data-col="param_value"');
expect(p08).toContain('data-col="constraint_type"');
const p10 = viewHtml('p10-document-create');
expect(p10).toContain('data-edit="parts.document"');
expect(p10).toContain('data-col="doc_seq"');
@@ -211,15 +221,15 @@ describe('报告模版:编辑面与只读面各就各位', () => {
}
});
it('每个可编辑格子的 data-col 都在对应台账对象的声明里', () => {
const cases: Array<[string, string, string]> = [
['p08-master-create', 'parts', 'profile'],
['p10-document-create', 'parts', 'document'],
['eval-input-list', D, 'input_gap'],
it('每个可编辑格子的 data-col 都在对应台账对象的声明里(p08-master-create 一屏两张表,两张台账对象的声明并起来看)', () => {
const cases: Array<[string, Array<[string, string]>]> = [
['p08-master-create', [['parts', 'profile'], ['parts', 'parameter']]],
['p10-document-create', [['parts', 'document']]],
['eval-input-list', [[D, 'input_gap']]],
];
for (const [view, domain, object] of cases) {
for (const [view, objects] of cases) {
const cols = new Set([...viewHtml(view).matchAll(/data-col="([^"]+)"/g)].map((m) => m[1] ?? ''));
const declared = new Set(byAddress(domain, object)?.fields.map((f) => f.name));
const declared = new Set(objects.flatMap(([domain, object]) => byAddress(domain, object)?.fields.map((f) => f.name) ?? []));
for (const c of cols) expect(declared.has(c), `${view}: ${c}`).toBe(true);
}
});
@@ -268,6 +278,7 @@ describe('徽标别名与值域相关的坑(2026-08-27 实测账,回归网)', ()
/** 每张模版真的摆了徽标的那几列(带下划线的 tally 值最容易漏,漏了就是「待定」那次事故) */
const BADGED: ReadonlyArray<{ view: string; domain: string; object: string; column: string }> = [
{ view: 'p08-master-create', domain: 'parts', object: 'profile', column: 'lifecycle_status' },
{ view: 'p08-master-create', domain: 'parts', object: 'parameter', column: 'constraint_type' },
{ view: 'p10-document-create', domain: 'parts', object: 'document', column: 'document_type' },
{ view: 'p10-document-create', domain: 'parts', object: 'document', column: 'document_status' },
{ view: 'eval-input-list', domain: D, object: 'pn_check', column: 'exists_status' },
@@ -319,6 +330,7 @@ describe('可编辑件的主键与必填列(2026-08-27 复审:主键丢钥匙 /
/** 三张真能编辑的件与它们各自绑定的台账对象 */
const EDITABLE: ReadonlyArray<{ view: string; domain: string; object: string }> = [
{ view: 'p08-master-create', domain: 'parts', object: 'profile' },
{ view: 'p08-master-create', domain: 'parts', object: 'parameter' },
{ view: 'p10-document-create', domain: 'parts', object: 'document' },
{ view: 'eval-input-list', domain: D, object: 'input_gap' },
];
@@ -154,13 +154,22 @@ describe('说明书与剧本的骨架', () => {
expect(s3).toContain('工具调用后同拍其余动作作废');
});
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明候选 ≥2 才问、=1 必建、原 PN 硬阻塞', () => {
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本按候选总数判断该不该问人(=1 必建、≥2 才问、原 PN 硬阻塞)', () => {
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
expect(s0).toContain('当下可为');
expect(s0).toContain('不要对计划项用');
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('硬阻塞');
expect(s0d).toContain('候选只有 1 颗');
expect(s0d).toContain('候选总数 = 1');
});
it('S0d_p08_master 剧本按候选总数(不是「不在库的颗数」)判断要不要问人 —— 候选总数 ≥2 时逐颗不在库的都要问(2026-08-27 实测逼出的措辞修正)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('候选总数');
expect(s0d).toContain('不在库');
expect(s0d).toMatch(/候选总数\s*≥\s*2/);
// 反面提示:「不在库的只有一颗」不该被读成跳过决策卡的理由
expect(s0d).toContain('不是跳过决策卡的理由');
});
it('s_bound 哨兵真的检查了「有料号不在库」这件事(都在库时门不该开;退回旧写法会红)', () => {