feat(activity-ops): 启动表单两格按芯片类型联动 —— 候选带分组键,先填的那格是锚
CI / ci (push) Failing after 10s

契约:launchOption 加 group/groupLabel(x-options.group 从字典表现读人读名)、
launchParam 加 linkGroup(x-link 结组);api-report 已随 --update 入库。
前端 launch-link.ts 纯函数管三条口径:锚摆全量、其余按锚的分组键收窄
(未标类型殿后一组,新增照走);锚粘着 —— 清空只解除收窄不换锚,人改得了主意。
服务端不据此立墙:combo 本就收候选之外的值,类型一致归 S1 收步判据
sub_eval_part_distinct_chip_type_id = 1(数得出来的门)。
W05 清单两格挂上 x-link: chip-type;文档同步。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-31 18:57:29 +08:00
co-authored by Claude Fable 5
parent 04ec718a66
commit 8c5f3a7736
13 changed files with 630 additions and 20 deletions
@@ -287,6 +287,28 @@ describe('契约', () => {
}
});
/*
* 按芯片类型联动(2026-08-31 用户定):候选带上分组键与人读名,两格结成一组。
* 分组键指的那一列必须是候选表**真有**的列(`parts.profile.chip_type_id`),
* 字典指的那一张表也得是本活动声明过的 —— 写错地址不会炸,只会静默不联动(服务端 warn 一句),
* 而「静默不联动」正是没人会发现的那种坏。
*/
it('两格按芯片类型联动:候选带分组键 + 字典换人读名,x-link 同名结成一组', () => {
const props = manifest.contract.input.data['properties'] as Record<string, Record<string, unknown>>;
const ledgerOf = (address: string) => manifest.declarations.ledger.find((l) => `${l.domain}.${l.object}` === address);
for (const key of ['originalInternalPn', 'candidateInternalPn']) {
expect(props[key]?.['x-link']).toBe('chip-type');
const options = props[key]?.['x-options'] as { group?: { column?: string; ledger?: string; value?: string; label?: string } };
expect(options.group).toEqual({ column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' });
// 分组键那一列在候选表上真的存在
expect(ledgerOf('parts.profile')?.fields.some((f) => f.name === options.group?.column)).toBe(true);
// 换名字典的两列也真的存在
const dict = ledgerOf('parts.chip_type');
expect(dict?.fields.some((f) => f.name === options.group?.value)).toBe(true);
expect(dict?.fields.some((f) => f.name === options.group?.label)).toBe(true);
}
});
it('零工具 —— 这是这次重写的立场,不是疏漏', () => {
expect(manifest.contract.resources.tools).toEqual([]);
expect(manifest.declarations.tools).toEqual([]);
@@ -89,7 +89,14 @@ export const manifest: ActivityManifest = {
*
* 运营端启动表单渲染这两格为「选择 + 新增」下拉(`x-control: combo`),候选从
* `parts.profile.internal_pn` 现读(`x-options`);`x-order` 定表单次序(清单存在
* jsonb 里,书写顺序到不了运营端)。requestDescription / candidateInternalPns(数组)/
* jsonb 里,书写顺序到不了运营端)。
*
* **两格按芯片类型联动**(2026-08-31 用户定):`x-options.group` 让候选带上各自的
* `chip_type_id` 与它在 `parts.chip_type` 里的人读名,`x-link: 'chip-type'` 把两格结成一组 ——
* 组内**先填的那格是锚**(摆全量),另一格只列同类型的料 + 库里未标类型的(殿后一组)+「新增」。
* 锚没有类型、或锚填的是库里没有的新料号 → 不收窄(两颗都还没建档时不该拦着人)。
* **这只是表单的引导,不是墙**:服务端照收两格不同类型的组合(`launch-params.ts` 头注),
* 类型一致由 S1 的收步判据 `sub_eval_part_distinct_chip_type_id = 1` 管 —— 那才是数得出来的门。requestDescription / candidateInternalPns(数组)/
* datasheets 三键已随重写撤下 —— 这一版没有「需求主记录」「N 候选并行」「预填
* datasheet」这几件事。
*/
@@ -103,8 +110,9 @@ export const manifest: ActivityManifest = {
description: '差异评估基准料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。',
examples: ['LM2596S-5.0'],
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn', group: { column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' } },
'x-order': 1,
'x-link': 'chip-type',
},
candidateInternalPn: {
type: 'string',
@@ -112,8 +120,9 @@ export const manifest: ActivityManifest = {
description: '被评估的候选料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。',
examples: ['XL2596S-5.0'],
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn', group: { column: 'chip_type_id', ledger: 'parts.chip_type', value: 'type_id', label: 'type_name' } },
'x-order': 2,
'x-link': 'chip-type',
},
},
},