This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/app/__tests__/seed-mvp-data.test.ts
T
xcsandClaude Opus 5 c6b02dfd39 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
2026-08-27 02:19:39 +08:00

101 lines
5.4 KiB
TypeScript

/**
* 主数据四域 —— 客户 / 项目 / 供应商 / 市场。
*
* 这份测试从前断言的是「四域十九对象」,那个四域是 project / customer / intel / supplier,
* 装的是**退役占位槽**的声明,里面混着一单一份的流转单据(受理记录、比价表、报价草案)。
* 2026-07-31 重分:单据归各活动自己的域,这个文件只留**主数据**,按常规 CRM / ERP 分四域。
*
* 守的是三条,每条都能机检:
* · 八个域,多一个就说明有人把单据塞进主数据了;
* · 每张表都是 `master` 且**不带单标识** —— 带了就是单据归错类(安装静态审也会拦);
* · 主键是**业务实体**,不是单。
*/
import { describe, expect, it } from 'vitest';
import { MASTER_DOMAINS, MASTER_OBJECTS, MASTER_SAMPLE_ROWS } from '../seed-mvp-data.ts';
describe('主数据四域', () => {
it('八个域:客户 / 项目 / 供应商 / 市场 + 料件 / 需求 / 替代 / 验证(SS02 差异评估 2026-08-26 加后四域)', () => {
expect([...MASTER_DOMAINS]).toEqual(['customer', 'project', 'supplier', 'market', 'parts', 'demand', 'substitution', 'validation']);
expect([...new Set(MASTER_OBJECTS.map((o) => o.domain))].sort()).toEqual([...MASTER_DOMAINS].sort());
});
it('SS02 差异评估要读写的七张主数据表都在基线里,主键照《统一方案》', () => {
const pk = (domain: string, object: string) =>
MASTER_OBJECTS.find((o) => o.domain === domain && o.object === object)?.fields.filter((f) => f.primaryKey).map((f) => f.name);
expect(pk('parts', 'profile')).toEqual(['internal_pn']);
expect(pk('parts', 'parameter')).toEqual(['internal_pn', 'param_name']);
expect(pk('parts', 'document')).toEqual(['internal_pn', 'doc_seq']);
expect(pk('demand', 'request')).toEqual(['request_id']);
expect(pk('substitution', 'relation')).toEqual(['substitution_relation_id']);
expect(pk('substitution', 'difference_item')).toEqual(['substitution_relation_id', 'difference_item_id']);
expect(pk('validation', 'record')).toEqual(['validation_id']);
});
it('样例料号:原 PN 与同族可调版在库,候选 XL2596S-5.0 刻意不播(数据工单那条路要试得出来)', () => {
const parts = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile').map((r) => r.key['internal_pn']);
expect(parts).toEqual(['LM2596S-5.0', 'LM2596S-ADJ']);
expect(parts).not.toContain('XL2596S-5.0');
});
it('样例料号的标准化参数集是独立子表 parts.parameter:两颗料各 7 行,主键是 (internal_pn, param_name)', () => {
const params = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'parameter');
const byPn = (pn: string) => params.filter((r) => r.key['internal_pn'] === pn);
expect(byPn('LM2596S-5.0')).toHaveLength(7);
expect(byPn('LM2596S-ADJ')).toHaveLength(7);
for (const row of params) {
expect(Object.keys(row.key).sort()).toEqual(['internal_pn', 'param_name']);
expect(row.values['param_value'], JSON.stringify(row.key)).toBeTruthy();
expect(row.values['constraint_type']).toMatch(/^(hard|soft)$/);
}
// parts.profile 不再声明这一列的值 —— 两行样例主记录里它应为空(物理列留在基线,不再写)
const profiles = MASTER_SAMPLE_ROWS.filter((r) => r.address.domain === 'parts' && r.address.object === 'profile');
for (const row of profiles) expect(row.values['normalized_parameter_set']).toBeUndefined();
});
it('四个域各自都有表 —— 空占位也得立得住', () => {
for (const d of MASTER_DOMAINS) {
expect(MASTER_OBJECTS.filter((o) => o.domain === d).length, `${d} 域一张表都没有`).toBeGreaterThan(0);
}
});
/*
* 这一条是分类的要害:主数据按**业务实体**分行,不按单。
* 带上单标识就说明它其实是一单一份的单据 —— 归错类,治理方式全跟着错。
*/
it('每张表都是 master 且不带单标识', () => {
for (const o of MASTER_OBJECTS) {
expect(o.kind, `${o.domain}.${o.object}`).toBe('master');
expect(o.caseKey, `${o.domain}.${o.object} 不该有单标识`).toBeUndefined();
}
});
it('每张表都有主键 —— 没主键的台账合并与去重无从谈起', () => {
for (const o of MASTER_OBJECTS) {
const pk = o.fields.filter((f) => f.primaryKey).map((f) => f.name);
expect(pk.length, `${o.domain}.${o.object} 没有主键`).toBeGreaterThan(0);
}
});
/* 主键得是业务实体键 —— 单标识那几个名字出现在主键里,就是归错了类 */
it('主键里不出现单标识那几个名字', () => {
for (const o of MASTER_OBJECTS) {
const pk = o.fields.filter((f) => f.primaryKey).map((f) => f.name);
for (const caseish of ['rfq_ref', 'quote_no', 'task_id']) {
expect(pk, `${o.domain}.${o.object} 的主键里有单标识 ${caseish}`).not.toContain(caseish);
}
}
});
/* 个人信息必须标脱敏 —— 主数据里存联系人,治理面照 `masked` 裁剪 */
it('联系人的邮箱与电话标了脱敏', () => {
const contacts = MASTER_OBJECTS.filter((x) => x.object === 'contact');
expect(contacts.length).toBeGreaterThan(0);
for (const o of contacts) {
for (const name of ['email', 'phone']) {
const field = o.fields.find((x) => x.name === name);
expect(field?.masked, `${o.domain}.${o.object}.${name} 未标脱敏`).toBe(true);
}
}
});
});