Archived
fix(w05): S1 配方次序 —— 案卷投影排到主数据/参数骨架之后
gate_reason 最深一层要读 parts_profile_missing + parts_parameter_missing,这两个数由主数据/ 参数骨架两笔撑出来。案卷投影若排在它们之前,读到的是这一拍刷新之前的旧数:candidate 的类型刚被 判定、参数骨架行即将铺出但还不存在时,parts_parameter_missing 读到 0,片刻之后变成 1。收步与 发卡的把关本身没坏(card.when 排在铺骨架之后能读到真实缺口,卡照样正确发出),但案卷里存的话、 连同卡面 basis 里「还差什么」那一格,会是一句空话。 六笔重排为 part → profile → parameter → case → card → say;新增引擎级回归测试 (w05-gate-reason-order.test.ts,手写镜像 S1 配方六笔与生产读数层的四个纯函数,因 activities/apps 两侧互相不许 import 而不能直接复用真配方或真引擎的读数逻辑),变异验证过 (旧次序下测试确实变红、且红得精确:Received 恰是评审报告里点名的那个空字符串)。 顺带补一句注释:parts.parameter 的 param_unit 有意直接取字典而不回填现值 —— 同一类型下两颗料 必须按同一个单位比,字典值才是比对基准,S2 判定时用的单位同源,不是漏写了 lookup。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -119,10 +119,18 @@ describe('S1 配方', () => {
|
||||
const acts = recipe?.do ?? [];
|
||||
const kinds = acts.map((a) => a.kind);
|
||||
|
||||
it('六笔,次序是:两颗料 → 案卷 → 主数据骨架 → 参数骨架 → 卡 → 交代', () => {
|
||||
/*
|
||||
* 次序是承重的,不是随手写的(2026-08-29 评审 Important 修正):案卷投影(`sub_eval.case`)
|
||||
* 的 `gate_reason` 要读 `parts_profile_missing`/`parts_parameter_missing` —— 这两个数由
|
||||
* 主数据/参数骨架两笔撑出来,案卷这一笔必须排在它们**之后**,否则读到的是这一拍刷新之前
|
||||
* 的旧数。病根写在 case-model.ts ④ 那一笔头上;引擎级的复现(旧次序真的会让 gate_reason
|
||||
* 变成空字符串)见 `apps/api/src/modules/loop-engine/__tests__/w05-gate-reason-order.test.ts`
|
||||
* ——这份文件只做结构断言,不跑引擎(活动包不许 import apps/*)。
|
||||
*/
|
||||
it('六笔,次序是:两颗料 → 主数据骨架 → 参数骨架 → 案卷 → 卡 → 交代', () => {
|
||||
expect(kinds).toEqual(['project', 'project', 'project', 'project', 'card', 'say']);
|
||||
expect(acts.map((a) => (a.kind === 'project' ? a.into : undefined)).filter(Boolean)).toEqual([
|
||||
'sub_eval.part', 'sub_eval.case', 'parts.profile', 'parts.parameter',
|
||||
'sub_eval.part', 'parts.profile', 'parts.parameter', 'sub_eval.case',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -140,7 +148,7 @@ describe('S1 配方', () => {
|
||||
});
|
||||
|
||||
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
|
||||
const params = acts[3];
|
||||
const params = acts[2];
|
||||
expect(params?.kind === 'project' && params.prune).toBe(true);
|
||||
const from = params?.kind === 'project' ? params.from : undefined;
|
||||
/* 外层是本单两颗料,里层是该类型的字段清单 —— 人没挑类型时 where 一行不命中,什么也不铺 */
|
||||
|
||||
@@ -156,7 +156,83 @@ export const ASSESSMENT_STEPS = [
|
||||
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'pn' } }, take: 'chip_type_id', else: '' },
|
||||
},
|
||||
},
|
||||
/* ② 案卷一行:`gate_reason` 是**摆在件上给人看**的那句话(条件判断另用基座读数,见 ⑤) */
|
||||
/*
|
||||
* ② 主数据骨架:**两颗料都铺、值回填现值**。
|
||||
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
|
||||
* **「在库的老料还没归类型」于是也被拦住** —— 那一格回填出来是空的,而 `chip_type_id` 必填。
|
||||
* 人不必在件上「加一行」:要建哪颗料是启动参数就定死的事,行由系统摆好,他只填格子。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'parts.profile',
|
||||
prune: true,
|
||||
from: { each: 'mine:sub_eval.part' },
|
||||
fields: {
|
||||
internal_pn: { col: 'internal_pn' },
|
||||
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'chip_type_id', else: '' },
|
||||
brand_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'brand_name', else: '' },
|
||||
lifecycle_status: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'lifecycle_status', else: '' },
|
||||
specification_description: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'specification_description', else: '' },
|
||||
manufacturer_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'manufacturer_name', else: '' },
|
||||
datasheet_ref: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_ref', else: '' },
|
||||
datasheet_version: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_version', else: '' },
|
||||
},
|
||||
},
|
||||
/*
|
||||
* ③ 参数骨架 —— 三件事一笔办完:
|
||||
* · 人还没挑类型 → `{outer:'chip_type_id'}` 是空串,`where` 一行不命中,**什么也不铺**;
|
||||
* 他挑了、按了保存,行就出来 —— **「保存即铺格」不是特判,是这一笔的自然结果**;
|
||||
* · 在库那颗料**也**铺,值回填现值 → 屏上是它此刻的样子,`_missing` 不会误报;
|
||||
* · **「在库但当年参数没录全」于是自动被拦住** —— 那一格回填出来是空的,红带点名、闸按不下。
|
||||
*
|
||||
* **`param_unit` 是有意不回填现值、直接取字典**(2026-08-29 评审 Minor 记录):
|
||||
* 同一类型下两颗料必须按**同一个单位**比,`parts.type_param.param_unit` 才是这次比对的基准 ——
|
||||
* 若某颗料自己记的单位与字典不同,以字典覆盖是**故意的**,不是漏写了 `lookup`;
|
||||
* S2 喂给 AI 判定时用的 `unit` 同样取自这张字典,两处必须同源,换成回填现值反而会让
|
||||
* 「展示的单位」与「判定时用的单位」分叉。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'parts.parameter',
|
||||
prune: true,
|
||||
from: {
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
},
|
||||
fields: {
|
||||
internal_pn: { outer: 'internal_pn' },
|
||||
param_name: { col: 'param_name' },
|
||||
param_unit: { col: 'param_unit' },
|
||||
param_value: {
|
||||
lookup: 'parts.parameter',
|
||||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||||
take: 'param_value',
|
||||
else: '',
|
||||
},
|
||||
param_note: {
|
||||
lookup: 'parts.parameter',
|
||||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||||
take: 'param_note',
|
||||
else: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
/*
|
||||
* ④ 案卷一行:`gate_reason` 是**摆在件上给人看**的那句话(条件判断另用基座读数,见 ⑤)。
|
||||
*
|
||||
* **排在 ②③ 之后 —— 这是承重的次序,不是随手写的**(2026-08-29 评审 Important 修正):
|
||||
* `gate_reason` 的 when 链最深一层要读 `parts_profile_missing + parts_parameter_missing`,
|
||||
* 而这两个数是 ②③ 刚铺出来的骨架撑出来的基座读数。这一笔若排在 ②③ **之前**
|
||||
* (原来的次序),它读到的是这一拍**刷新之前**的旧数:当某颗料的 `chip_type_id`
|
||||
* 刚在这一拍被 ① 设上、②③ 即将把它的主数据与参数骨架铺出来时,那一刻 ③ 即将新建的
|
||||
* 参数骨架行还不存在,`parts_parameter_missing` 读到 0(不存在的行不算「缺」),
|
||||
* 片刻之后 ③ 一跑就变成 1。收步与发卡的把关本身没坏(⑤ 排在 ②③④ 之后,`card.when`
|
||||
* 用的是刷新之后的真实读数,卡照样正确发出),但案卷里存的这句话、连同卡面
|
||||
* `basis` 里「还差什么」那一格,会是一句空话 —— 人最需要一句解释的时候,读到的是空的。
|
||||
* **每一笔之前刷读数**(auto-runner 的硬规矩)保证的是「下一笔看得见上一笔的结果」,
|
||||
* 保证不了「这一笔算的话用了后面几笔才有的数」—— 读别人数据的那一笔,必须排在被读的
|
||||
* 那几笔之后。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'sub_eval.case',
|
||||
@@ -187,61 +263,6 @@ export const ASSESSMENT_STEPS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
/*
|
||||
* ③ 主数据骨架:**两颗料都铺、值回填现值**。
|
||||
* 在库那颗回填出它此刻的样子(人不必重填);要建那颗全空等人填。
|
||||
* **「在库的老料还没归类型」于是也被拦住** —— 那一格回填出来是空的,而 `chip_type_id` 必填。
|
||||
* 人不必在件上「加一行」:要建哪颗料是启动参数就定死的事,行由系统摆好,他只填格子。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'parts.profile',
|
||||
prune: true,
|
||||
from: { each: 'mine:sub_eval.part' },
|
||||
fields: {
|
||||
internal_pn: { col: 'internal_pn' },
|
||||
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'chip_type_id', else: '' },
|
||||
brand_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'brand_name', else: '' },
|
||||
lifecycle_status: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'lifecycle_status', else: '' },
|
||||
specification_description: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'specification_description', else: '' },
|
||||
manufacturer_name: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'manufacturer_name', else: '' },
|
||||
datasheet_ref: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_ref', else: '' },
|
||||
datasheet_version: { lookup: 'parts.profile', by: { internal_pn: { col: 'internal_pn' } }, take: 'datasheet_version', else: '' },
|
||||
},
|
||||
},
|
||||
/*
|
||||
* ④ 参数骨架 —— 三件事一笔办完:
|
||||
* · 人还没挑类型 → `{outer:'chip_type_id'}` 是空串,`where` 一行不命中,**什么也不铺**;
|
||||
* 他挑了、按了保存,行就出来 —— **「保存即铺格」不是特判,是这一笔的自然结果**;
|
||||
* · 在库那颗料**也**铺,值回填现值 → 屏上是它此刻的样子,`_missing` 不会误报;
|
||||
* · **「在库但当年参数没录全」于是自动被拦住** —— 那一格回填出来是空的,红带点名、闸按不下。
|
||||
*/
|
||||
{
|
||||
kind: 'project' as const,
|
||||
into: 'parts.parameter',
|
||||
prune: true,
|
||||
from: {
|
||||
each: 'mine:sub_eval.part',
|
||||
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
|
||||
},
|
||||
fields: {
|
||||
internal_pn: { outer: 'internal_pn' },
|
||||
param_name: { col: 'param_name' },
|
||||
param_unit: { col: 'param_unit' },
|
||||
param_value: {
|
||||
lookup: 'parts.parameter',
|
||||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||||
take: 'param_value',
|
||||
else: '',
|
||||
},
|
||||
param_note: {
|
||||
lookup: 'parts.parameter',
|
||||
by: { internal_pn: { outer: 'internal_pn' }, param_name: { col: 'param_name' } },
|
||||
take: 'param_note',
|
||||
else: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
/*
|
||||
* ⑤ 缺什么就派一张数据工单。
|
||||
* **条件只用基座读数**:`gate_reason` 为空串时那一列压根不写、于是未绑定,
|
||||
|
||||
Reference in New Issue
Block a user