W05 收口:五例在收紧断言下全 released · 全被拒 0(活动 1.2.9 → 1.3.6)

第 7–12 轮逼出七条(T–Z),两条是脚本自己的、五条是活动的,其中一条上结构:
- W 参数名写成英文 key,与库里中文行不是同一行 → 27 条里 14 条 UNKNOWN。措辞两遍 +
  乘法对账都拦不住单颗料那一路(5 = 5×1 恒成立),改自指 refersTo:param_name
  只能用台账里已经在用的名字,一颗料时也咬得住。
- V/Z 数据工单那一项的两个方向:建完第一颗就收掉(再给第二颗发卡撞「已办完」)、
  铺完骨架就标 created 直接 finish(撞「从没发过卡」)—— 顺序写死:骨架 → 发卡 →
  拍结束 → 人交回 → 才标 created;absent 没清零不许 finish。
- T P-10 也要「发卡前骨架先有行」;X 第二颗缺库也要先问。
- 脚本:U 一例翻车不拖垮整套;Y 按闸的指示「加一行」;断言收紧 —— 点名的行整例跑完
  仍没出现 = 失败,决策卡按「问的是哪件事」数(第 9 轮逮到一次假绿)。

流程上两条纪律(用户当场纠正):只跑受影响的那一例,改了平台共享面才全跑;先说计划再动手。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-27 11:13:53 +08:00
co-authored by Claude Fable 5
parent 182d3c39b5
commit 6f7513aa58
6 changed files with 188 additions and 14 deletions
@@ -84,6 +84,13 @@ describe('台账声明:授权面与《统一方案》口径', () => {
expect(param?.kind).toBe('master'); expect(param?.kind).toBe('master');
expect(param?.fields.filter((f) => f.primaryKey).map((f) => f.name)).toEqual(['internal_pn', 'param_name']); 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 === '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 === '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')?.tally).toEqual(['hard', 'soft']);
expect(param?.fields.find((f) => f.name === 'constraint_type')?.required).toBe(true); expect(param?.fields.find((f) => f.name === 'constraint_type')?.required).toBe(true);
@@ -263,7 +263,9 @@ describe('说明书与剧本的骨架', () => {
}); });
it('数据工单也要「先写骨架再发卡」写进说明书,且哨兵不认零行主数据(2026-08-27 e2e:模型压拍没写骨架就发卡,人打开是空表,零行同时骗过闸与哨兵,一路走到 S2 才被引用检查拦下)', () => { it('数据工单也要「先写骨架再发卡」写进说明书,且哨兵不认零行主数据(2026-08-27 e2e:模型压拍没写骨架就发卡,人打开是空表,零行同时骗过闸与哨兵,一路走到 S2 才被引用检查拦下)', () => {
expect(NARRATIVE).toContain('必须先有骨架行'); // 两张数据工单各要一条(P-08 的 profile/parameter、P-10 的 document)
expect(NARRATIVE.match(/必须先有骨架行/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
expect(NARRATIVE).toContain('parts.document` 必须先有骨架行');
expect(NARRATIVE).toMatch(/压拍/); expect(NARRATIVE).toMatch(/压拍/);
// 标了几颗建好,主数据就得有几行 —— 逐颗对账(第一刀写成「≥1 行就算数」太松,三候选那单从缝里过去了) // 标了几颗建好,主数据就得有几行 —— 逐颗对账(第一刀写成「≥1 行就算数」太松,三候选那单从缝里过去了)
expect(CMMN_XML).toContain('parts_profile_rows &gt;= sub_eval_pn_check_exists_status_created'); expect(CMMN_XML).toContain('parts_profile_rows &gt;= sub_eval_pn_check_exists_status_created');
@@ -280,6 +282,35 @@ describe('说明书与剧本的骨架', () => {
expect(g).toContain('引用检查'); expect(g).toContain('引用检查');
}); });
it('absent 没清零之前不许收掉数据工单那一项(2026-08-27 e2e:建完第一颗就 finish,再想给第二颗发卡撞上「这一步已经办完收掉了」,连拒四次)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/还有没有 absent 的料号/);
expect(g).toMatch(/这一项千万别 finish/);
expect(g).toMatch(/收掉了就回不去了/);
});
it('参数名一律中文、与库里既有行同名(2026-08-27 e2e:模型写成 input_voltage_range,而原 PN 是「输入电压范围」—— 主键的一半对不上,27 条里 14 条判成 UNKNOWN)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/参数名一律用中文/);
// 措辞写过两遍都没拦住 → 上结构:把「同一套名字」变成基座算得出的数
expect(CMMN_XML).toContain('parts_parameter_rows = parts_parameter_distinct_param_name * sub_eval_pn_check_exists_status_created');
expect(g).toContain('input_voltage_range');
expect(g).toMatch(/不是同一行/);
});
it('第二颗缺库的候选也要先问(每一颗都有被放弃的权利)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/这一颗也要先发一张决策卡问人/);
expect(g).toMatch(/第一颗问过了不代表第二颗也问过/);
});
it('created 认的是「卡交回来了」,不是「表里有行」—— 写骨架 ≠ 建好了(2026-08-27 e2e:模型铺完骨架就标 created、直接 finish,撞「从没给它发过卡」连拒五拍)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/写骨架 ≠ 建好了/);
expect(g).toMatch(/写骨架 → \*\*发卡\*\* → 这一拍结束 → 人交回/);
expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/);
});
it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => { it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => {
expect(ASSESSMENT_PHASES).toHaveLength(5); expect(ASSESSMENT_PHASES).toHaveLength(5);
for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10); for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10);
@@ -45,6 +45,9 @@ export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
<caseFileItem id="parts_profile_missing" name="本单建的芯片主数据缺必填的行数(基座现数)"/> <caseFileItem id="parts_profile_missing" name="本单建的芯片主数据缺必填的行数(基座现数)"/>
<caseFileItem id="parts_parameter_missing" name="本单建的标准化参数集缺必填的行数(基座现数)"/> <caseFileItem id="parts_parameter_missing" name="本单建的标准化参数集缺必填的行数(基座现数)"/>
<caseFileItem id="parts_document_missing" name="本单建的资料记录缺必填的行数(基座现数)"/> <caseFileItem id="parts_document_missing" name="本单建的资料记录缺必填的行数(基座现数)"/>
<caseFileItem id="parts_document_rows" name="本单建的资料记录行数(基座现数)"/>
<caseFileItem id="parts_parameter_rows" name="本单建的参数行数(基座现数)"/>
<caseFileItem id="parts_parameter_distinct_param_name" name="本单用到的不同参数名个数(基座现数不同值)"/>
<caseFileItem id="input_confirmed_total" name="评估输入已确认(人环凭据)"/> <caseFileItem id="input_confirmed_total" name="评估输入已确认(人环凭据)"/>
<caseFileItem id="sub_eval_input_gap_rows" name="评估输入清单行数(基座现数)"/> <caseFileItem id="sub_eval_input_gap_rows" name="评估输入清单行数(基座现数)"/>
<caseFileItem id="sub_eval_input_gap_missing" name="评估输入清单缺必填的行数(基座现数)"/> <caseFileItem id="sub_eval_input_gap_missing" name="评估输入清单缺必填的行数(基座现数)"/>
@@ -90,7 +93,7 @@ export const CMMN_XML = `<?xml version="1.0" encoding="UTF-8"?>
却只建了第一颗,一行足以让门开着,直到 S2 写关系时才被引用检查拦下。 却只建了第一颗,一行足以让门开着,直到 S2 写关系时才被引用检查拦下。
改成**逐颗对账**:\`parts_profile_rows >= created\` —— 标了几颗建好,就得有几行。 改成**逐颗对账**:\`parts_profile_rows >= created\` —— 标了几颗建好,就得有几行。
--> -->
<ifPart id="if_pr"><condition>sub_eval_pn_check_rows = count(candidateInternalPns) + 1 and sub_eval_pn_check_missing = 0 and sub_eval_pn_check_exists_status_absent = 0 and sub_eval_pn_check_exists_status_abandoned &lt; count(candidateInternalPns) and parts_profile_missing = 0 and parts_parameter_missing = 0 and parts_document_missing = 0 and parts_profile_rows &gt;= sub_eval_pn_check_exists_status_created</condition></ifPart> <ifPart id="if_pr"><condition>sub_eval_pn_check_rows = count(candidateInternalPns) + 1 and sub_eval_pn_check_missing = 0 and sub_eval_pn_check_exists_status_absent = 0 and sub_eval_pn_check_exists_status_abandoned &lt; count(candidateInternalPns) and parts_profile_missing = 0 and parts_parameter_missing = 0 and parts_document_missing = 0 and parts_profile_rows &gt;= sub_eval_pn_check_exists_status_created and parts_document_rows &gt;= sub_eval_pn_check_exists_status_created and parts_parameter_rows = parts_parameter_distinct_param_name * sub_eval_pn_check_exists_status_created</condition></ifPart>
</sentry> </sentry>
<!-- ③ 评估输入已确认(旗 + 清单有行且必填齐)—— 标准化那一步的入场前提 --> <!-- ③ 评估输入已确认(旗 + 清单有行且必填齐)—— 标准化那一步的入场前提 -->
<sentry id="s_input_confirmed"> <sentry id="s_input_confirmed">
@@ -244,7 +247,10 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该
不给它就回落到必交付物那几张(与这张卡无关),于是件上红带点着「缺品牌名称」而提交钮照样绿, 不给它就回落到必交付物那几张(与这张卡无关),于是件上红带点着「缺品牌名称」而提交钮照样绿,
空手交卷一路放行(2026-08-27 实测)。给了之后,闸与红带、放行门读的是同一个数。 空手交卷一路放行(2026-08-27 实测)。给了之后,闸与红带、放行门读的是同一个数。
④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发): ④ 数据工单 · P-10(同一批料号的资料记录,一张单;在 P-08 那张确认之后再发)——
**与③同一条:发这张卡之前 \`parts.document\` 必须先有骨架行**(名单里每颗料至少一行,
key 是 {internal_pn, doc_seq:"01"});一行都没有的话人打开是空表、什么也补不了,
提交闸会如实拦住(「这一单还一行都没有」),这一单就卡在这儿了(2026-08-27 实测)。
{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{ {"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{
"title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>", "title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>",
"ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。", "ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。",
@@ -383,20 +389,41 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
1. 给名单里每颗料写一行 \`parts.profile\` 骨架(表 1):key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读 1. 给名单里每颗料写一行 \`parts.profile\` 骨架(表 1):key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读
brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ datasheet_ref + datasheet_version; brand_name / specification_description / package / lifecycle_status(active/nrnd/eol/discontinued/risk 之一)/ datasheet_ref + datasheet_version;
没有 datasheet 就只写 internal_pn 这一列(其余留给人填,**别编**)。 没有 datasheet 就只写 internal_pn 这一列(其余留给人填,**别编**)。
2. 同一拍给每颗料写标准化参数集骨架(表 2,\`parts.parameter\`,一行一参数):有 \`datasheets[].text\` 就从里面读出典型参数 2. 同一拍给每颗料写标准化参数集骨架(表 2,\`parts.parameter\`,一行一参数):
**参数名一律用中文,与库里既有料号的那几行同名**(输入电压范围 / 输出电压 / 输出电流 / 开关频率 /
静态电流 / 工作温度 / 使能阈值…)—— \`param_name\` 是主键的一半,写成 \`input_voltage_range\`
与写成「输入电压范围」**不是同一行**:原 PN 那边是中文、候选这边是英文,比对时两套名字对不上,
逐条判成 UNKNOWN,结论就废了(2026-08-27 实测:27 条里 14 条 UNKNOWN)。
封装、工艺、生命周期这些**不是参数**,它们在表 1,别往表 2 里塞。
**建几颗料就用同一套名字**:门上有一条对账 —— 参数行数 = 不同参数名数 × 建过的料数,
只有每颗料的名字集合一模一样时才成立;混着写(这颗中文那颗英文、或同一颗写两套)这一步就过不去。有 \`datasheets[].text\` 就从里面读出典型参数
(如输入电压范围 / 输出电压 / 输出电流 / 开关频率 / 静态电流 / 工作温度 / 使能阈值等,按品类而定)各写一行, (如输入电压范围 / 输出电压 / 输出电流 / 开关频率 / 静态电流 / 工作温度 / 使能阈值等,按品类而定)各写一行,
key {internal_pn, param_name},patch 带 param_value / param_unit / constraint_type(hard/soft)/ param_note(一句话说清这项为什么要卡, key {internal_pn, param_name},patch 带 param_value / param_unit / constraint_type(hard/soft)/ param_note(一句话说清这项为什么要卡,
如「须覆盖客户输入上限」);原厂没写的值写「未披露」(它会进入 UNKNOWN 并挂验证任务)。没有 datasheet 就只写 如「须覆盖客户输入上限」);原厂没写的值写「未披露」(它会进入 UNKNOWN 并挂验证任务)。没有 datasheet 就只写
internal_pn + param_name 骨架(把你判断这颗料该评估的参数名列出来,**param_value 留空给人填,别编**)。 internal_pn + param_name 骨架(把你判断这颗料该评估的参数名列出来,**param_value 留空给人填,别编**)。
3. 同一拍给每颗料写一行 \`parts.document\` 骨架:key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending", 3. 同一拍**给名单里每一颗料各写一行** \`parts.document\` 骨架(两颗就两行,别只写一行 ——
门上有一条对账:建好几颗就得有几行资料记录;少一行,那颗料的资料工单人打开是空的):key {internal_pn, doc_seq: "01"},patch {document_type: "datasheet", document_status: "pending",
brand_name 有就写}(这一行是骨架,下一项 S0d_p10_document 会把它填成正式记录)。 brand_name 有就写}(这一行是骨架,下一项 S0d_p10_document 会把它填成正式记录)。
4. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。 4. 同一拍发说明书③那张 P-08 数据工单(title 里列全料号),发完这一拍结束(挂起)。
**\`created\` 是「这颗料真的建好了」,不是「我打算建」**:只有它在 \`parts.profile\` 里真有一行(件上填好交回来了) **\`created\` 认的是「那张卡交回来了」,不是「表里有行」** —— **写骨架 ≠ 建好了**:
才改成 created —— 门上有一条逐颗对账(建好几颗就得有几行主数据),标了却没建,这一步过不去, 骨架是**你**铺的(只有料号和参数名),值是**人**在件上填的。骨架一铺就把 pn_check 改成 created,
到 S2 写关系时还会被引用检查再拦一次。 等于替人宣布他已经办完了:接着你会以为数据工单收工、去 finish 这一项,而卡根本没发出去 ——
撞上「这一单里从没给它发过卡」,连撞五拍熔断(2026-08-27 实测,整单白跑)。
**顺序只有一个**:写骨架 → **发卡** → 这一拍结束 → 人交回 → 这时才把那几颗改成 created。
门上还有一条逐颗对账(建好几颗就得有几行主数据、几行资料),标了却没建,这一步照样过不去。
【P-08 那张确认回来了】这一拍的动作串**恰好两个,顺序不能反**: 【P-08 那张确认回来了】**先看名单里还有没有 absent 的料号**(\`${D}.pn_check\` 现读):
· **还有** → **这一项千万别 finish**。先回到上面【问题二】那一档:候选总数 ≥2 的话,
**这一颗也要先发一张决策卡问人**(照说明书②,人选 create 才建 —— 每一颗 absent 的候选都有被放弃的权利,
第一颗问过了不代表第二颗也问过);人选 create 之后再把它的骨架写好(profile + parameter,有 datasheet 就预填),
发一张 P-08 卡(标题带那一颗的料号)—— 同一个计划项可以先后发好几张卡,一颗一张也行。
原 PN 与「候选总数 = 1」那两档不问,直接建。
**收掉了就回不去了**:这一项一旦 Completed,再想给它挂第二张卡会被拒「这一步已经办完收掉了」,
而那颗料从此没人建,后面写替代关系时被引用检查逐颗拦下(2026-08-27 实测,一单连拒四次)。
· **一颗都不剩了** → 才走下面这两步。
这一拍的动作串**恰好两个,顺序不能反**:
① 先 \`finish S0d_p08_master\` —— **必须先把这一项收掉**。不收掉,引擎挑工作区画面时按 ① 先 \`finish S0d_p08_master\` —— **必须先把这一项收掉**。不收掉,引擎挑工作区画面时按
「当下还没干完的第一步」取 view,S0d_p08_master 只要还 Active,画面就一直停在 「当下还没干完的第一步」取 view,S0d_p08_master 只要还 Active,画面就一直停在
\`p08-master-create\`(P-08 那两张表);下一拍就算发出了 P-10 的卡,人的工作区也看不见 \`p08-master-create\`(P-08 那两张表);下一拍就算发出了 P-10 的卡,人的工作区也看不见
@@ -89,7 +89,7 @@ export const manifest: ActivityManifest = {
identity: { identity: {
activityId: ACTIVITY_ID, activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估', name: 'ACT-WORK-05 · 差异评估',
packageVersion: '1.2.9', packageVersion: '1.3.6',
category: '需求分析与方案匹配', category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5', sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment', checksum: 'dev-checksum-part-substitution-assessment',
@@ -309,7 +309,32 @@ export const manifest: ActivityManifest = {
label: '标准化参数集(P-08 子表)', label: '标准化参数集(P-08 子表)',
fields: [ fields: [
col({ name: 'internal_pn', type: 'text', label: '所属料号', primaryKey: true, refersTo: 'parts.profile.internal_pn' }), col({ name: 'internal_pn', type: 'text', label: '所属料号', primaryKey: true, refersTo: 'parts.profile.internal_pn' }),
col({ name: 'param_name', type: 'text', label: '参数名', primaryKey: true }), /*
* `distinct` 是为了让「两颗料是不是用同一套参数名」变成**基座算得出的数**
* (`parts_parameter_distinct_param_name`)—— 见 CMMN 的 s_pns_resolved:
* 参数行数 = 不同参数名数 × 建过的料数,只有每颗料的名字集合一模一样时才成立。
* 2026-08-27 实测:模型给第二颗料写英文 key(`input_voltage_range`),
* 而原 PN 在库里是中文「输入电压范围」—— 名字是主键的一半,对不上就没法逐项比。
* 措辞写过两遍都没拦住(设计律第六条:措辞治不了的结构治)。
*/
col({
name: 'param_name',
type: 'text',
label: '参数名',
primaryKey: true,
distinct: true,
/*
* **参数名只能用台账里已经在用的那几个**(自指引用)—— 比对是逐参数对着比的,
* 候选身上有一个原 PN 没有的维度,本来也无从比起;名字对不上更是直接判成 UNKNOWN。
* 2026-08-27 实测三次:模型给新料写英文 key(`input_voltage_range`),
* 而库里既有行是中文「输入电压范围」—— 剧本写过两遍、又加了一条乘法对账
* (行数 = 不同名字数 × 建过的料数)仍拦不住**只建一颗**的那一路(5 = 5×1 恒成立)。
* 自指是唯一在一颗料时也咬得住的:写下去那一刻基座去这一列里查,查无此值当场拒。
* 代价(已知约束):真要引入一个全新的参数维度,得先有一颗料用过它 ——
* MVP 的品类词汇表是固定的七项,够用;将来要放开,改成引用一张参数字典表。
*/
refersTo: 'parts.parameter.param_name',
}),
col({ name: 'param_value', type: 'text', label: '值', required: true }), col({ name: 'param_value', type: 'text', label: '值', required: true }),
col({ name: 'param_unit', type: 'text', label: '单位', expect: true }), col({ name: 'param_unit', type: 'text', label: '单位', expect: true }),
col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }), col({ name: 'constraint_type', type: 'text', label: '约束类型', required: true, tally: ['hard', 'soft'] }),
+26 -2
View File
@@ -191,7 +191,16 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
| 3 | 1 通过 | N 的措辞版没用 → **改结构**;**O 工具被 null 打崩** · P `count` 假线索 | | 3 | 1 通过 | N 的措辞版没用 → **改结构**;**O 工具被 null 打崩** · P `count` 假线索 |
| 4 | 2 通过 | **H 的残留面:零行同时骗过闸与哨兵** | | 4 | 2 通过 | **H 的残留面:零行同时骗过闸与哨兵** |
| 5 | 4 通过 | **created 逐颗对账太松 · S1 卡的前提没写在说明书** | | 5 | 4 通过 | **created 逐颗对账太松 · S1 卡的前提没写在说明书** |
| 6 | **跑到的 4 例全 released,3 例被拒 0** | 案例 4 停在 `Insufficient Balance`(大模型余额) | | 6 | 跑到的 4 例全 released,3 例被拒 0 | 案例 4 停在 `Insufficient Balance`(大模型余额) |
| 7 | 前 3 例 released | 充值后:先撞上我自己拉起的旧稳定实例抢单(`recommend` 无可用实现);**T** P-10 骨架零行 · **U** 脚本一例翻车整套退出 |
| 8 | **5 例全 released,4 例被拒 0** | 案例 3 被拒 6:**V** 建完第一颗就收掉数据工单那一项 |
| 9 | 案例 3 单跑 | 用户纠正「别每次全跑」;逮到一次**假绿**:**W** 参数名写成英文 key · **X** 两颗缺库只问一次 —— 断言收紧 |
| 10–11 | 案例 0/1 被拒 0 | **W 的乘法对账只在 ≥2 颗时咬得住**,单颗照漏 → 自指 `refersTo`;**Y** 脚本按闸的指示「加一行」;案例 2 撞 **Z** |
| 12 | 案例 2 · 4 各单跑 | **Z** 修后 released · 被拒 0 · 卡先发人再答 |
**最终状态(收紧断言下)**:五例全部 `released` · 全部被拒 0。
案例 0/1 验于 1.3.5、案例 3 验于 1.3.4、案例 2/4 验于 1.3.6 —— 1.3.6 只改了 S0d 剧本里「何时标 created」那一句,
案例 0/3 走过那一步、未在最终文本上复验(如实记)。
第 6 轮逐条实证:早收容忍让案例 1/2 的最后 1 次被拒消失(轨迹里是**观察**不是拒回); 第 6 轮逐条实证:早收容忍让案例 1/2 的最后 1 次被拒消失(轨迹里是**观察**不是拒回);
三候选那例**第一次跑通**(前三轮:超时 / 超时 / 烧穿预算),两颗缺库一起办、三条结论全对。 三候选那例**第一次跑通**(前三轮:超时 / 超时 / 烧穿预算),两颗缺库一起办、三条结论全对。
@@ -219,6 +228,13 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
| Q | 模型压拍没写骨架就发数据工单,人打开是空表,而**零行 = 零缺**,闸与哨兵一起放过 | 「缺必填」只数已有行;`_missing = 0` 在零行时恒真 | 平台 `emptyGate`(**卡自报的表一行都没有 = 没完成**)+ 说明书③「发卡前骨架必须先有行」+ 哨兵 `profile_rows >= created` 逐颗对账 | 平台 + 活动 | | Q | 模型压拍没写骨架就发数据工单,人打开是空表,而**零行 = 零缺**,闸与哨兵一起放过 | 「缺必填」只数已有行;`_missing = 0` 在零行时恒真 | 平台 `emptyGate`(**卡自报的表一行都没有 = 没完成**)+ 说明书③「发卡前骨架必须先有行」+ 哨兵 `profile_rows >= created` 逐颗对账 | 平台 + 活动 |
| R | 两颗缺库只办了第一颗,第二颗还 absent 就把 S1 的卡发了出去 | **发卡的授权面是调人预算,不是计划项** —— 它绕得过哨兵;而 S1 那张卡的**前提**只写在剧本里 | 前提写进说明书的卡骨架(「一颗 absent 都不剩」「有几颗 absent 就要办几颗」) | 活动 | | R | 两颗缺库只办了第一颗,第二颗还 absent 就把 S1 的卡发了出去 | **发卡的授权面是调人预算,不是计划项** —— 它绕得过哨兵;而 S1 那张卡的**前提**只写在剧本里 | 前提写进说明书的卡骨架(「一颗 absent 都不剩」「有几颗 absent 就要办几颗」) | 活动 |
| S | 每例白吃一次的「早收」:工具还没轮到调,`finish` 先到了 | 那是**计划次序**的小错,却被记成「行动被拒」去喂熔断计数 | 与「重发已答过的卡」同一把尺:**头一次只落观察,第二次起才算被拒** | **平台** | | S | 每例白吃一次的「早收」:工具还没轮到调,`finish` 先到了 | 那是**计划次序**的小错,却被记成「行动被拒」去喂熔断计数 | 与「重发已答过的卡」同一把尺:**头一次只落观察,第二次起才算被拒** | **平台** |
| T | P-10 数据工单发出来时 `parts.document` 一行都没有 | 说明书③给 P-08 补了「发卡前骨架必须先有行」,④(P-10)没补 | ④ 补同款前提 | 活动 |
| U | 一例卡住,脚本整套抛出退出,后面两例连跑都没跑 | `main` 逐例调用没兜 | 一例翻车判失败、接着跑下一例 —— 一套五例的价值在「哪几例过、哪几例没过」 | 脚本 |
| V | 建完第一颗料就把数据工单那一项收掉,再给第二颗发卡撞「这一步已经办完」 | 剧本【P-08 确认回来了】没分岔「还有没有 absent」 | absent 没清零不许 finish;写明「收掉了就回不去了」 | 活动 |
| W | 参数名写成英文 key(`input_voltage_range`),与库里中文行**不是同一行** → 27 条里 14 条 UNKNOWN | `param_name` 是主键的一半;措辞两遍 + 乘法对账(行数 = 不同名字数 × 料数)都拦不住**单颗**那一路 | **自指 `refersTo: parts.parameter.param_name`** —— 一颗料时也咬得住 | 活动(结构) |
| X | 两颗缺库只问了一次;`expectDecision` 只看张数,审核卡就把它喂饱 | 剧本「还有 absent → 写骨架再发卡」把「先问」压掉;断言太宽 | 剧本补「第二颗也要先问」;脚本新增 `expectAsks`(按问法数) | 活动 + 脚本 |
| Y | 模型漏铺骨架 → 闸如实拦「一行都没有」→ 脚本只会填已有行,连吃 400 抛出 | 脚本没有「加一行」这条路,而人在浏览器里正是这么做的 | 主键齐了就加一行(与件上「加一行」同一条路) | 脚本 |
| Z | 铺完骨架就标 `created`、直接 finish,撞「从没给它发过卡」连拒五拍 | 我 1.3.1 的措辞「表里真有一行才标 created」—— 而骨架是模型自己铺的 | 「created 认的是卡交回来了」;顺序写死:骨架 → 发卡 → 拍结束 → 人交回 → 才标 | 活动 |
**可推广的三条**(已记 `docs/debt.md`): **可推广的三条**(已记 `docs/debt.md`):
@@ -258,7 +274,15 @@ P-11 一条(`LM2596S-3.3 → LM2596S-ADJ | alternate | fae_confirmed`);P-01 回
脚本逮的是**机制**(空手交卷、循环、崩、归属)。发现 L–S 八条没有一条是浏览器走查逮到的 —— 脚本逮的是**机制**(空手交卷、循环、崩、归属)。发现 L–S 八条没有一条是浏览器走查逮到的 ——
因为人手点的时候会先把格子填好再按提交,永远不会去试「空着交上去会怎样」。 因为人手点的时候会先把格子填好再按提交,永远不会去试「空着交上去会怎样」。
5. **拒回喂事实,不喂指针。** 5. **断言报的通过是假的,比报的失败是假的更糟。**
第 9 轮案例 3 报「1/1 通过」,而参数名全是英文、两颗缺库只问了一次 —— 脚本只把它们记成 ⚠️ 就放过。
假失败还有人去查;假通过没有人会看第二眼。此后:点名的行整例跑完仍没出现 = 失败;决策卡按**问的是哪件事**数。
6. **只跑受影响的那一例;改了平台共享面才全跑回归。**
前八轮每次把已绿的四例重跑一遍 —— 每轮多烧一百多万 token,而且什么新东西都测不出。
脚本本来就支持 `w05 N`。
7. **拒回喂事实,不喂指针。**
发现 N 那一轮把「他选的是 approve、下一步写哪面旗」写进拒回,日志里逐字可见,模型照发不误 —— 发现 N 那一轮把「他选的是 approve、下一步写哪面旗」写进拒回,日志里逐字可见,模型照发不误 ——
因为**下一拍它又从头翻一次会滚走的窗口**。把同一件事做成「每拍都在的事实行」之后,一次就好了。 因为**下一拍它又从头翻一次会滚走的窗口**。把同一件事做成「每拍都在的事实行」之后,一次就好了。
同一条道理已经在 worldTouched(2026-08-21)、E04 的 worldTouched 事实行(2026-08-24)上各现过一次。 同一条道理已经在 worldTouched(2026-08-21)、E04 的 worldTouched 事实行(2026-08-24)上各现过一次。
+60
View File
@@ -709,6 +709,8 @@ const SUITES = {
name: '三候选 · 两颗缺库各问一次 · 国产 32V 硬约束不推荐', name: '三候选 · 两颗缺库各问一次 · 国产 32V 硬约束不推荐',
decideSeq: ['create', 'create', 'approve'], decideSeq: ['create', 'create', 'approve'],
expectDecision: true, expectDecision: true,
// 两颗候选缺库 → 按规格各问一次(审核卡也是决策卡,所以要按问法数,不能只看张数)
expectAsks: [{ re: /查无记录/, count: 2 }],
expectPort: 'assessment-released', expectPort: 'assessment-released',
stageEdits: { stageEdits: {
'parts.profile': [ 'parts.profile': [
@@ -906,6 +908,7 @@ async function answerDecisions(taskId, kase, answered) {
body: JSON.stringify({ reply: { k: 'decision', cardId: card.id, value: picked.value, note: custom ? want.slice(7) : '试跑代答' } }), body: JSON.stringify({ reply: { k: 'decision', cardId: card.id, value: picked.value, note: custom ? want.slice(7) : '试跑代答' } }),
}); });
answered.add(card.id); answered.add(card.id);
asked.push(String(card.ask ?? card.title ?? ''));
count += 1; count += 1;
console.log(` 决策卡拍板:${card.ask?.slice(0, 40) ?? ''} → ${picked.label ?? picked.value}`); console.log(` 决策卡拍板:${card.ask?.slice(0, 40) ?? ''} → ${picked.label ?? picked.value}`);
} }
@@ -966,10 +969,34 @@ function buildEdits(stage, drafts, kase = {}) {
if (!target) { if (!target) {
const wanted = allGiven ? keyCols.map((k) => keyVals[k]).join('/') : `待填第${i}行`; const wanted = allGiven ? keyCols.map((k) => keyVals[k]).join('/') : `待填第${i}行`;
const have = rows.map((r) => keyCols.map((k) => r[k]).join('/')).join(', '); const have = rows.map((r) => keyCols.map((k) => r[k]).join('/')).join(', ');
/*
* **点名的行找不到 = 判失败,不是提个醒**(2026-08-27 实测:模型把参数名写成英文 key
* `input_voltage_range`,而库里既有行是中文「输入电压范围」—— 参数名是主键的一半,
* 两套名字对不上,比对时 27 条里 14 条判成 UNKNOWN。而这一例照样报「1/1 通过」:
* 断言报的通过是假的,比断言报的失败是假的更糟 —— 后者还有人去查。)
*/
/*
* **件上没有这一行就「加一行」** —— 人在浏览器里就是这么做的(闸的原话:
* 「先在件上加一行填好再提交」),脚本从前只会填已有行,于是模型漏写骨架时
* 这一例直接卡死(2026-08-27:P-10 零行,连吃三次 400 抛出)。
* 主键齐了才加(凑不出钥匙的行基座也认不出),加完从「缺」的名单里划掉。
*/
if (allGiven) {
lines.push({ ...keyVals, ...cells });
missedRows.delete(`${keyCols.join('+')}=${wanted}`);
console.log(` 件上加一行(${keyCols.join('+')}=${wanted})—— 模型没铺这行骨架,照闸的指示补`);
continue;
}
missedRows.add(`${keyCols.join('+')}=${wanted}`);
console.log(` ⚠️ 找不到要填的行(${keyCols.join('+')}=${wanted});现有行:${have}`); console.log(` ⚠️ 找不到要填的行(${keyCols.join('+')}=${wanted});现有行:${have}`);
continue; continue;
} }
taken.add(sig(target)); taken.add(sig(target));
/*
* **填上了就从「缺」的名单里划掉** —— 同一类卡一例里可能发好几张(两颗缺库各一张 P-08),
* 第一张上第二颗的行还不存在是**正常的**,不是缺;整例跑完仍没填上才是真缺。
*/
if (allGiven) missedRows.delete(`${keyCols.join('+')}=${keyCols.map((k) => keyVals[k]).join('/')}`);
lines.push({ ...keyOf(target), ...cells }); lines.push({ ...keyOf(target), ...cells });
} }
if (stage.fillMissing) { if (stage.fillMissing) {
@@ -1125,6 +1152,8 @@ async function runCase(suite, kase) {
let gateLeaked = false; let gateLeaked = false;
const gateChecked = new Set(); const gateChecked = new Set();
let decisions = 0; let decisions = 0;
missedRows.clear();
asked.length = 0;
/** 人加行只加一次;加的那家进没进计划,在办中盯(放行会把草稿消费掉,收工再看就晚了) */ /** 人加行只加一次;加的那家进没进计划,在办中盯(放行会把草稿消费掉,收工再看就晚了) */
let addedRows = false; let addedRows = false;
let planned = false; let planned = false;
@@ -1351,6 +1380,23 @@ async function runCase(suite, kase) {
ok = false; ok = false;
} }
// 「超限才叫人」是 A4 的要害:该叫的那一单没叫,等于底价门禁是摆设 // 「超限才叫人」是 A4 的要害:该叫的那一单没叫,等于底价门禁是摆设
if (missedRows.size > 0) {
console.log(` ⚠️ 案例点名要填的行,整例跑完也没在件上出现过(${missedRows.size} 处:${[...missedRows].slice(0, 3).join('、')}…)—— 判失败`);
ok = false;
}
/*
* **决策卡要数「问的是哪件事」,不是笼统数张数**(2026-08-27:两颗缺库一张创建卡都没问,
* 而审核卡也是决策卡 —— `decisions > 0` 于是恒真,该问没问照样绿)。
*/
if (kase.expectAsks !== undefined) {
for (const { re, count } of kase.expectAsks) {
const got = asked.filter((a) => re.test(a)).length;
if (got < count) {
console.log(` ⚠️ 该问的没问够:${re} 期望 ≥${count} 张,实际 ${got} 张 —— 判失败`);
ok = false;
}
}
}
if (kase.expectDecision && decisions === 0) { if (kase.expectDecision && decisions === 0) {
console.log(' ⚠️ 超限条目没走决策卡 —— 判失败'); console.log(' ⚠️ 超限条目没走决策卡 —— 判失败');
ok = false; ok = false;
@@ -1401,6 +1447,11 @@ async function runCase(suite, kase) {
return ok && (!kase.expectPort || ports.includes(kase.expectPort)); return ok && (!kase.expectPort || ports.includes(kase.expectPort));
} }
/** 这一例里「案例点名要填、而件上找不到」的行 —— 每例开头清空,收尾判失败 */
const missedRows = new Set();
/** 这一例里模型问过的每一句(决策卡的 ask)—— 收尾按「问的是哪件事」核,不是笼统数张数 */
const asked = [];
async function main() { async function main() {
const which = process.argv[2] ?? 'all'; const which = process.argv[2] ?? 'all';
const only = process.argv[3] === undefined ? undefined : Number(process.argv[3]); const only = process.argv[3] === undefined ? undefined : Number(process.argv[3]);
@@ -1414,7 +1465,16 @@ async function main() {
for (const [i, kase] of suite.cases.entries()) { for (const [i, kase] of suite.cases.entries()) {
if (only !== undefined && i !== only) continue; if (only !== undefined && i !== only) continue;
total += 1; total += 1;
/*
* **一例翻车不该拖垮整套**(2026-08-27 实测:P-10 那张卡零行、闸如实拦着,
* 脚本没东西可填、抛出去,后面两例连跑都没跑)。判失败、记下来、接着跑下一例 ——
* 一套五例的价值就在于「哪几例过、哪几例没过」,死在半路等于什么都没测出来。
*/
try {
if (await runCase(suite, kase)) pass += 1; if (await runCase(suite, kase)) pass += 1;
} catch (error) {
console.log(` ✗ 这一例中途出错,判失败,接着跑下一例:${String(error.message ?? error).slice(0, 200)}`);
}
} }
} }
console.log(`\n${pass}/${total} 通过;记得 pnpm --filter @app/api reset-runs 清运行数据`); console.log(`\n${pass}/${total} 通过;记得 pnpm --filter @app/api reset-runs 清运行数据`);