fix(w05): 终审两条 Critical + 地基缺陷的另一半 + 文档与欠账收口

**C1 新建芯片类型会改写既有字典行(共享主数据被静默污染)**
链条逐行复核成立:件上种子行漏挂 data-col → barred 与 isSeq 双保护同时失效
(isSeq 那条是因为 Task 7b 把 type_id 从 integer 改成了 text)→ 落进「全表一致」,
而 H35 的自投影恰好保证这张表只有一行 → 人新建的类型被认成既有 type_id=1 →
确认时随范围放行把既有字典行改名并落进正式表,屏上只说「改了 1 行」。
三条各自成立的裁定叠出来的静默污染。
修法:种子行改成 disabled 的真实 <input data-col=type_id>(desk.ts 零改动),
格子里摆「由系统生成」——不能一声不吭地拿掉,那样人会以为这张档少了一格。
测试不止断言结果:另加两条用**真实 resolveRowKey** 重放病灶/修法两种 cells 形状,
直接证明因果机制。平台根治(全表一致应排除 auto 列)记 debt。

**C2 两个启动参数都选填 → 留空则整单永久无解**
gate_reason 说「在下面把两格补上」,而块① 没有加一行、也不在 gateLedgers ——
一句指向空气的指路。改为必填;第一档改口为唯一真无解的那条路径(两格填了同一个号)。

**I1 地基缺陷只修了一半:recipe.let 同样按对象键序求值**
与 fields 是一模一样的假设,同样住 jsonb 列、同样被 PG 重排;今天不炸只因 W05 那五条
互不引用,而 ADR-014 是给后续所有活动用的。新增 orderLets/letReferences
(把 {in:X} 视为依赖 —— 语义与 fields 不同,故单独实现),auto-runner 与静态审同步接入。

**I2 审核卡按「重做」照样收步推进** —— 坐实为真实平台缺陷(「卡答过了就收步」快道不看裁决),
并证明活动侧加选项救不了、chosenGuidance 那条也救不了(那是喂模型的提示词,
而这条快道存在的目的就是跳过模型)。按裁定只留复现测试,平台未改。

**I4** 同一堵墙撞第二次即发兜底执行工单交人 + 立监听(头一次软化留痕),
不再空转到 exhausted 而人无处可改。

debt +4(C1 根治 / modules→app / thead-after-tbody / data-by 落回外层),
并补写此前被两处引用却从不存在的「refersTo 传递闭包」条目。
文档:ADR-014:201「按声明顺序求值」改对(照它写配方的人会重犯本轮的地基缺陷)、
陈旧注释四处、spec 四处分叉逐条判断处置、端到端测试记录标为历史、
CLAUDE.md 纠正「新建类型端到端通了」那句(它没通过真人路)并更新版本与文档表。

包版本 2.1.9(已装,SQL 直查库内 version/required/gate_reason/视图 HTML 四项核实)。
pnpm gates 退出码 0(2476 通过 / 34 跳过);E04 92/92、I01 55/55 无回归。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-30 16:09:18 +08:00
co-authored by Claude Opus 5
parent 9f5b1c99d3
commit 45cec82984
19 changed files with 1078 additions and 69 deletions
@@ -127,9 +127,14 @@ describe('清单本身合规', () => {
* 2026-08-29 重写:启动表单从「需求 + 原PN + 候选数组 + datasheet」四件收到「原PN + 候选」两件
* (单数,一次评估恰一对一候选)—— `requestDescription` / `candidateInternalPns`(数组)/
* `datasheets` 三键随 P-01 需求主记录一起撤下(本活动不再碰它)。逐条断言见「契约 > 启动两参」。
*
* **2026-08-30 终审 Critical 2 改判**:两格从「都选填」改成**都必填**——旧断言
* `toEqual([])` 钉的正是那条留空即永久卡死在 S1 的死路(gate_reason 指向一条不存在的
* 「加一行」路径),详见「契约 > 启动两参」那条测试与 `manifest.ts`/`case-model.ts`
* 里的大注释。「客户与需求两类字段已随重写撤下」这半句结论不变。
*/
it('起单一格必填都不设(2026-08-28 用户定);客户与需求两类字段已随重写从启动表单撤下', () => {
expect(manifest.contract.input.data['required']).toEqual([]);
it('起单两格都必填(2026-08-30 修 Critical 2 改判);客户与需求两类字段已随重写从启动表单撤下', () => {
expect(manifest.contract.input.data['required']).toEqual(['originalInternalPn', 'candidateInternalPn']);
const launchProps = Object.keys(manifest.contract.input.data['properties'] as Record<string, unknown>);
expect(launchProps).not.toContain('customerCode');
expect(launchProps).not.toContain('customerName');
@@ -262,11 +267,20 @@ describe('台账声明', () => {
});
describe('契约', () => {
it('启动两参,都选填 —— 缺了由第一步派工单补', () => {
/*
* **2026-08-30 终审 Critical 2**:两颗料号是这件事的身份,不是可以事后补的细节 ——
* 都选填时留空,`sub_eval.part` 的两行会因为同一个空主键 `internal_pn=''` 撞成一行,
* `gate_reason` 判「还没填全」而块① 没有「加一行」的路,doneWhen 永不成立,整单
* 永久停在 S1(链条见 `manifest.ts`/`case-model.ts` 的大注释)。改必填不是把人堵死——
* 平台既有的「受理归一」机制(`task-service.ts` 的 `missingRequired`/`askCompletion`)
* 接住它:起单缺这两格,任务落成 `awaiting-human`,一张补全卡问负责人要,答完才开跑,
* 不是门口拒单。主数据(品牌/类型/参数)仍然缺了才由第一步派数据工单补。
*/
it('启动两参,都必填 —— 两颗料号是身份不是细节,留空会让整单永久卡死在 S1', () => {
const props = manifest.contract.input.data['properties'] as Record<string, Record<string, unknown>>;
expect(Object.keys(props)).toEqual(['originalInternalPn', 'candidateInternalPn']);
expect(manifest.contract.input.data['required']).toEqual([]);
/* 运营端启动表单:两格都是「可选可新写」的下拉 */
expect(manifest.contract.input.data['required']).toEqual(['originalInternalPn', 'candidateInternalPn']);
/* 运营端启动表单:两格仍是「可选可新写」的下拉 —— 必填只挡空值,不挡「库里没有就自己写一个新的」 */
for (const key of ['originalInternalPn', 'candidateInternalPn']) {
expect(props[key]?.['x-control']).toBe('combo');
expect(props[key]?.['x-options']).toMatchObject({ ledger: 'parts.profile', value: 'internal_pn' });
@@ -574,15 +588,29 @@ describe('件① 评估输入补全', () => {
});
/*
* `auto:true` 的主键(`type_id`)**不该出现在种子行里**——自增列不问人(与联系人编码
* `contact_code` 同一条纪律)。种子行只有 `type_name`/`description` 两个手写输入格。
* `auto:true` 的主键(`type_id`)**人不填它**——自增列不问人(与联系人编码
* `contact_code` 同一条纪律)。种子行只有 `type_name`/`description` 两个可填输入格,
* `type_id` 自己那一格**必须**是一个真的、禁用的 `data-col` 输入格(值恒空),
* **不能干脆不给** `data-col`。
*
* **2026-08-30 终审 Critical 1 的回归钉**(反着来的教训:这条测试原来断言
* `not.toContain('data-col="type_id"')`,而正是那个「不给 data-col」的形状让
* `desk.ts` 的 `resolveRowKey` 认不出「这一列人碰过没有」,退回「全表一致」把
* 新建的类型误判成种子字典里唯一那一行、静默改名覆盖它——详见 `views/report.ts`
* `CHIP_TYPE_TABLE` 头上的大注释与 `apps/api/src/app/__tests__/view-table-edit.test.ts`
* 里对真实 `resolveRowKey` 的因果验证)。这条测试当年"证明"的是病灶本身,现在改成
* 钉住修法:`type_id` 必须挂 `data-col`,且必须是禁用的 —— 人看得见「由系统生成」
* 这句占位,但怎么点都改不了它的值。
*/
it('**新建类型的种子行不问 type_id**(自增列不问人):已有行的 type_id 只读、没有孪生输入框', () => {
expect(html).toContain('系统生成');
// 「系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行,断言里面没有 data-col="type_id"
const chipTypeStaticRow = /<tr class="jc-row-static">(?:(?!<\/tr>)[\s\S])*?系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
it('**新建类型的种子行:type_id 挂 data-col、但禁用恒空**(2026-08-30 修 Critical 1 —— 漏挂会让 resolveRowKey 的"全表一致"把新建的类型误判成既有那一行,静默覆盖它)', () => {
expect(html).toContain('由系统生成');
// 「由系统生成」只出现在 chip_type 那条种子行里 —— 精确抓出那一行
const chipTypeStaticRow = /<tr class="jc-row-static">(?:(?!<\/tr>)[\s\S])*?由系统生成(?:(?!<\/tr>)[\s\S])*?<\/tr>/.exec(html)?.[0] ?? '';
expect(chipTypeStaticRow).not.toBe('');
expect(chipTypeStaticRow).not.toContain('data-col="type_id"');
/* 必须是一个真的 <input data-col>(collect() 只认 input/select/textarea[data-col]),
不能是纯文本 <span> —— 否则这一格永远不会出现在回传的 cells 里,barred 的保护无从谈起 */
expect(chipTypeStaticRow).toMatch(/<input[^>]*data-col="type_id"[^>]*disabled/);
/* disabled 意味着人怎么点都改不了它的值 —— 保存时这一格恒是空串,才谈得上「barred」 */
expect(chipTypeStaticRow).toContain('data-col="type_name"');
expect(chipTypeStaticRow).toContain('data-col="description"');
});
@@ -194,6 +194,23 @@ describe('S1 配方', () => {
expect(fields?.['exists_status']).toEqual({ when: 'found_pn_ref != ""', then: 'found', else: 'absent' });
});
/*
* **2026-08-30 修 Critical 2**:两个启动参数改成必填之后(`manifest.ts` 的
* `contract.input.required`),`sub_eval_part_rows < 2` 只剩一条能触发它的路径 ——
* 两颗料号填成了同一个(`internal_pn` 撞成同一把主键,两行覆盖成一行)。旧文案
* 「原料号与候选料号还没填全 —— 在下面把两格补上」指向一条不存在的路(块① 没有
* 「加一行」),必须换成说实情、给真出路的话:没法比出差异,终止重开。
*/
it('gate_reason 第一档:两参必填之后只剩"填成同一个号"这条残余路径,文案不再指向不存在的"补两格"', () => {
const kase = acts[5];
const fields = kase?.kind === 'project' ? kase.fields : undefined;
const gateReason = fields?.['gate_reason'] as { when?: string; then?: string } | undefined;
expect(gateReason?.when).toBe('sub_eval_part_rows < 2');
expect(gateReason?.then).not.toContain('还没填全');
expect(gateReason?.then).not.toContain('把两格补上');
expect(gateReason?.then).toContain('终止');
});
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
const params = acts[4];
expect(params?.kind === 'project' && params.prune).toBe(true);
@@ -335,7 +335,12 @@ export const ASSESSMENT_STEPS = [
eval_ref: { in: 'evalRef' },
internal_pn: { col: 'pn' },
role: { col: 'role' },
/* 先取依据,再按依据判 —— 字段按声明顺序求值,算出来的进同一行的 S-FEEL 作用域 */
/*
* 先取依据,再按依据判 —— `exists_status` 的 `when` 引用 `found_pn_ref`,
* `orderFields` 按这条依赖排出安全求值序(不是「声明顺序恰好写对了」,
* 2026-08-30 治 jsonb 键序重排那次改的:见 `auto-order.ts` 头注),
* 算出来的进同一行的 S-FEEL 作用域。
*/
found_pn_ref: { lookup: 'parts.profile', by: { internal_pn: { col: 'pn' } }, take: 'internal_pn', else: '' },
exists_status: { when: 'found_pn_ref != ""', then: 'found', else: 'absent' },
chip_type_id: { lookup: 'parts.profile', by: { internal_pn: { col: 'pn' } }, take: 'chip_type_id', else: '' },
@@ -397,13 +402,18 @@ export const ASSESSMENT_STEPS = [
* 修法与 ② 完全同构:按当下选中的 `chip_type_id` 把那个类型的核心参数字段清单
* 整套原样抄回自己身上(值没变,幂等,不产生多余写入)——**新建的类型也走这条路**:
* 人在件上新增的字段行本就已经在 `mine:parts.type_param` 里,这一笔重新投一遍
* 原样命中、照样幂等跳过,不会覆盖人刚填的内容。`prune:true` 清掉的只是「先前选了
* 某个类型、后来改选了另一个」时那些不再相关的陈旧行。
* 原样命中、照样幂等跳过,不会覆盖人刚填的内容。
*
* **同样不带 `prune`**(与 ② 同一处修正,同一个理由:字段清单也在 ⑥ 的
* `gateLedgers` 上,人交回数据工单那一刻就会跟着落地;已落地的行 `prune` 撤不掉,
* 带上 `prune:true` 会在「先前定过某个类型的字段清单、后来改选了另一个类型」这条
* 路上原样复现 H35 e2e 案例 2 那个死锁)。历史选型的陈旧字段行留在草稿层不算错,
* 与 ② 的字典行是同一条道理。
*/
{
kind: 'project' as const,
into: 'parts.type_param',
/* 同上一笔同一个理由,同一次修正 —— 见 ② 那笔头上的大注释,不重复贴 */
/* 同上一笔同一个理由,同一次修正(含"不带 prune") —— 见 ② 那笔头上的大注释,不重复贴 */
from: {
each: 'mine:sub_eval.part',
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
@@ -480,6 +490,20 @@ export const ASSESSMENT_STEPS = [
/*
* ⑥ 案卷一行:`gate_reason` 是**摆在件上给人看**的那句话(条件判断另用基座读数,见 ⑦)。
*
* **第一档改口(2026-08-30 修 Critical 2)**:两个启动参数现在是**必填**
* (`contract.input.required`),`missingRequired` 挡在起单那一刻 —— 空字符串永远
* 到不了这一步。`sub_eval_part_rows < 2` 因此只剩**一条**能触发它的真实路径:
* `originalInternalPn === candidateInternalPn`(人把两颗料填成了同一个号)——
* ① 那一笔 `project` 用它们铸 `sub_eval.part` 的主键 `internal_pn`,两行撞成同一个键,
* 第二笔 `draftWrite` 覆盖第一笔,表里只剩一行。旧文案「原料号与候选料号还没填全 ——
* 在下面把两格补上」在这条残余路径上是**两处都错**:①两格根本不是空的(必填墙已经
* 拦住空值),说「没填全」是失实;②块① 的行源是 `mine:sub_eval.part`、人不加行
* (没有「+加一行」按钮),即便真缺一行也补不出来 —— 那句「补上」指向一条不存在的路
* (墙要配路,这句连墙都不是墙)。改口直说实情与唯一出路:两颗料号是这件事的身份,
* 填成同一个号没法比出差异,只能终止重开——这是**真正没有其他出路**的场景(与
* 「留空」不同,「重复」没有任何一步能在本单内部把两个身份重新分开),不算失职地
* 指错方向。
*
* **排在 ④⑤ 之后 —— 这是承重的次序,不是随手写的**(2026-08-29 评审 Important 修正):
* `gate_reason` 的 when 链最深一层要读 `parts_profile_missing + parts_parameter_missing`,
* 而这两个数是 ④⑤ 刚铺出来的骨架撑出来的基座读数。这一笔若排在 ④⑤ **之前**
@@ -502,7 +526,7 @@ export const ASSESSMENT_STEPS = [
chip_type_id: { lookup: 'mine:sub_eval.part', by: { role: 'original' }, take: 'chip_type_id', else: '' },
gate_reason: {
when: 'sub_eval_part_rows < 2',
then: '原料号与候选料号还没填全 —— 在下面把两格补上。',
then: '原料号与候选料号填成了同一个 —— 没法比出差异,请终止本单,换两个不同的料号重开。',
else: {
when: 'sub_eval_part_exists_status_absent > 0',
then: '有料号库里查不到 —— 在下面把它的主数据建起来。',
@@ -650,8 +674,10 @@ export const ASSESSMENT_STEPS = [
},
/*
* ②→④ **`RELATION_PROJECTION` 写两遍,中间夹着差异明细** —— 不是笔误
* (2026-08-30,真跑 e2e 第一次撞见的循环依赖,`docs/ACT-WORK-05-端到端测试记录.md`
* 追加记录):`substitution.difference_item.substitution_relation_id` 声明了
* (2026-08-30,真跑 e2e 第一次撞见的循环依赖;**不是**记进
* `docs/ACT-WORK-05-端到端测试记录.md`——那份文档整篇是四步重写[2026-08-29]之前
* 的旧九步设计,已在文档开头标为历史、没有再追加过):
* `substitution.difference_item.substitution_relation_id` 声明了
* `refersTo: 'substitution.relation.substitution_relation_id'`(要求引用的那一行
* 已经真实存在),而 `substitution.relation` 的 `recommendation_level`/`risk_level`/
* `difference_summary`/`recommendation_reason` 这几列**要读差异明细的
@@ -9,12 +9,12 @@
* 「本来就该是确定的」那几步(编号抄别单、判在库判错、数据工单派给在库的料…),
* 真正需要判断的只有一件事:**这一条参数,候选跟原厂比算不算事**。
*
* 这份清单是 Task 2 的产物,只动 `declarations.ledger` 与 `contract` 两段,以及 `assets`
* 里已经不自洽的那几处(spec §8 的退役清单前移到这一步,见「计划裁定 G3」)。**案例模型
* (`case-model.ts`)还是旧的九步、旧的 `guide` 文本** —— 下一个任务(Task 3)才整体重写成
* spec §4 的四步(`S1_inputs` / `S2_assess` / `S3_review` / `T_release`)+ 四道哨兵 + 零 `guide`
* (行为改由 `PlanStep.auto` 声明式配方接管)。旧剧本引用本次删掉的台账地址而导致的包内自测红,
* 已按台账逐条 `it.skip` 并注明「Task 3 会补回」,不删测试(§12.5 铁律)。
* **2026-08-30 更新**:这份注释原写于重写 Task 2(只动 `declarations.ledger` 与
* `contract` 两段那一刻),彼时 `case-model.ts` 还是旧九步——**Task 3 已把它整体重写成
* spec §4 的四步**(`S1_inputs` / `S2_assess` / `S3_review` / `T_release`)+ 四道哨兵 +
* 零 `guide`(行为改由 `PlanStep.auto` 声明式配方接管),清单与案例模型此刻同版一致,
* 不再有「一边新一边旧」的过渡态。`assets` 里当时已不自洽的那几处(spec §8 的退役清单,
* 见「计划裁定 G3」)同批处理完毕。
*
* ## 九张台账声明(spec §3)
* 两张新字典(`parts.chip_type` / `parts.type_param`,Task 1 已物化 + 播种)承接「约束类型按类型
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.1.8',
packageVersion: '2.1.9',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -66,21 +66,41 @@ export const manifest: ActivityManifest = {
version: '1.0.0',
input: {
/*
* 两格,都选填(spec §4.1):库里有就选,没有就自己写一个新的 —— 启动后由第一步的
* 配方核对存在性、缺了就派数据工单补齐。运营端启动表单渲染这两格为
* 「选择 + 新增」下拉(`x-control: combo`),候选从 `parts.profile.internal_pn`
* 现读(`x-options`);`x-order` 定表单次序(清单存在 jsonb 里,书写顺序到不了运营端)。
* requestDescription / candidateInternalPns(数组)/ datasheets 三键已随重写撤下 ——
* 这一版没有「需求主记录」「N 候选并行」「预填 datasheet」这几件事。
* **两格,都必填**(2026-08-30 修 Critical 2,推翻 spec §4.1 原来的「都选填」)——
* 一次评估恰是「一对一候选」,这两颗料号是这件事的**身份**,不是可以事后补的细节:
* 库里有就选,没有就自己写一个新的(合法的新料号字符串,不是留空)。
* 「启动后由第一步的配方核对存在性、缺了就派数据工单补齐」这句话说的是料号的**主数据**
* (品牌/类型/参数),从来不是料号**本身**——旧措辞把两件事含糊成了一件事。
*
* 都选填时留空的下场(评审逐行核过的链条):S1 配方 `project into sub_eval.part` 拿
* `originalInternalPn`/`candidateInternalPn` 铸两行的主键 `internal_pn`,两个空字符串
* 是**同一个主键**——第二笔 `draftWrite` 覆盖第一笔,`sub_eval.part` 收成**一行**不是
* 两行;`gate_reason` 的 `sub_eval_part_rows < 2` 当场为真,提示「在下面把两格补上」——
* 而块① 的行源是 `mine:sub_eval.part`(人不加行,没有「+加一行」按钮),那两格根本
* 不存在。`doneWhen` 永不成立,提交也按不下去,**唯一出路是终止重开**,且五条样例与
* e2e 五案例都没填过空值,两条验证线都照不到这条死路。
*
* 改必填**不是关上「留空」这扇门就完事**——平台早有配套机制接住它(2026-07-30 立的
* 「受理归一」:缺必填不拒单,任务落成 `awaiting-human`,一张补全卡问负责人要那几个
* 字段,答完合并进 `data` 才转 `running`;`apps/api/src/modules/task/internal/usecase/
* task-service.ts` 的 `missingRequired`/`askCompletion`,`__tests__/intake.test.ts`
* 已经把这条路测得很实)。必填不是墙,是把「两颗料号是什么」这件唯一要人先说清楚的
* 事挪到起单那一刻问清楚,不留给配方去猜一个猜不出来的空字符串。
*
* 运营端启动表单渲染这两格为「选择 + 新增」下拉(`x-control: combo`),候选从
* `parts.profile.internal_pn` 现读(`x-options`);`x-order` 定表单次序(清单存在
* jsonb 里,书写顺序到不了运营端)。requestDescription / candidateInternalPns(数组)/
* datasheets 三键已随重写撤下 —— 这一版没有「需求主记录」「N 候选并行」「预填
* datasheet」这几件事。
*/
data: {
type: 'object',
required: [],
required: ['originalInternalPn', 'candidateInternalPn'],
properties: {
originalInternalPn: {
type: 'string',
title: '原内部料号',
description: '差异评估基准料号;库里有就选,没有就自己写一个新的,启动后由第一步派数据工单补齐。',
description: '差异评估基准料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。',
examples: ['LM2596S-5.0'],
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
@@ -89,7 +109,7 @@ export const manifest: ActivityManifest = {
candidateInternalPn: {
type: 'string',
title: '候选内部料号',
description: '被评估的候选料号;库里有就选,没有就自己写一个新的,启动后由第一步派数据工单补齐。',
description: '被评估的候选料号(必填);库里有就选,没有就自己写一个新的 —— 它的主数据(品牌/类型/参数)缺了才由第一步派数据工单补齐。',
examples: ['XL2596S-5.0'],
'x-control': 'combo',
'x-options': { ledger: 'parts.profile', value: 'internal_pn', label: 'internal_pn' },
@@ -180,8 +200,11 @@ export const manifest: ActivityManifest = {
predicate: 'validation_record_rows = 1 and validation_record_missing = 0',
},
/*
* **两条 L4 补了谓词**(2026-08-30,真跑 e2e 撞见的结构性发现,记入
* `docs/ACT-WORK-05-端到端测试记录.md`)——「裁量项带谓词时谓词说了算」这条纪律
* **两条 L4 补了谓词**(2026-08-30,真跑 e2e 撞见的结构性发现 —— **不是**记进
* `docs/ACT-WORK-05-端到端测试记录.md`:那份文档整篇是 2026-08-27 及更早的旧
* 九步设计,四步重写[2026-08-29]之后没有再往里追加过任何一条,已在文档开头标为
* 历史;这条发现连同其余 2026-08-30 的都只活在这份源码注释与 `docs/debt.md` 里)。
* 「裁量项带谓词时谓词说了算」这条纪律
* (2026-08-02 立,`engines.ts` 的 `criterionCheck`:`settledByData`)本仓已有先例,
* 从前 W05 这两条 L4 是裸的(无 `predicate`),而 T_release 全程 `auto` 驱动、
* `submit` 是五个声明式动词之一(`packages/contracts/src/auto-recipe.ts`),
@@ -505,7 +528,7 @@ export const manifest: ActivityManifest = {
/*
* DMN 判定表已退役:D01(逐参数四态)改由 S2 唯一一次 analyze 判定;D02(推荐等级)的规则
* 搬进配方的 when 链(spec §3.5)—— 规则只许有一处真源,再留一份 DMN 就是同一件事两处
* 各存一份。Task 3 会把 `steps` 换成四步、`guide` 全部清空(auto 配方接管行为)。
* 各存一份。`steps` 已是四步、`guide` 全部清空(auto 配方接管行为,Task 3 完成)。
*/
businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: { drd: [], tables: [] }, steps: ASSESSMENT_STEPS, phases: ASSESSMENT_PHASES },
/* 三条技能已随重写退役(全程无 agent,没有临场编排可言);skills.ts 已在 Task 10 删除 */
@@ -330,9 +330,32 @@ const PART_TABLE = `<div class="jc-tbl-wrap"><table id="tbl1">
/**
* 块②a 新建芯片类型 —— `parts.chip_type`(Task 7b,2026-08-29 补齐)。类型下拉(块①)里
* 没有合适的,人在这里现造一个:类型名称(必填)+ 说明(建议)。`type_id` 是自增主键
* (`auto:true`,声明改 `text` 的理由见 manifest 那条大注释)—— **不进表单**,自增列
* 不问人(与联系人编码 `contact_code` 同一条纪律,2026-08-02 立);只读态摆 `ro()`,
* 连孪生输入框都不给。
* (`auto:true`,声明改 `text` 的理由见 manifest 那条大注释)—— **人不填它**,自增列
* 不问人(与联系人编码 `contact_code` 同一条纪律,2026-08-02 立);已有行只读态摆 `ro()`。
*
* **种子行里 `type_id` 必须挂一个真实的(禁用、恒空)`data-col`,不能干脆不给 ——
* 这是 2026-08-30 终审 Critical 1 的病灶与修法**(链条评审逐行核实过,详见
* `docs/debt.md`「auto:true 又是整数型的主键」条目的姊妹坑):第一版图省事,把这一格
* 写成纯文本 `<span>系统生成</span>`,不带 `data-col` —— 保存时 `collect()` 只认
* `input[data-col]/select[data-col]/textarea[data-col]`,`type_id` 于是**压根不出现在
* 回传的 `cells` 里**。而 `desk.ts` 的 `resolveRowKey` 用 `k in cells`(不是「值是否非空」)
* 判断「这一列人碰过没有」——漏挂 `data-col` 在它眼里等同「这张表压根没有这一列」,
* 专门为「auto 列件上摆着却空着」设计的 `barred` 保护(`(input.autos??[]).filter((k)=>k in
* cells && ...)`)因此从未触发,退回给「全表一致」那条兜底路径。而 Task 7b 把
* `type_id` 从 `integer` 改成 `text` 之后,「序号型主键不走全表一致」(2026-08-23 那条
* 保护,认的是 `isSeq`)也顺带失效 —— 两条保护同时缺位,「全表一致」直接接管:
* `parts.chip_type` 此刻**恰好一行**(Ruling H35 的自投影②保证了这一点),
* `seen.size===1` 当场把人新建的类型判成「就是那唯一一行」,一次保存就把种子字典行
* (如 `type_id=1` 的「DC-DC 降压稳压器」)**静默改名**——`mintable`/`autoKeys()`(真正
* 为它设计的续号器)永远轮不到。
* **修法**:给这一格换成一个真的 `<input data-col="type_id" disabled>`(值恒空,
* 只显示「由系统生成」占位)——`'type_id' in cells` 从此为真、值为空,`barred` 正确
* 命中,`resolveRowKey` 交回 `undefined`,`mintable` 才轮得到、续出一个不撞任何既有行
* 的新号。**平台侧 `resolveRowKey` 本身一行没改**——这是活动侧绕开,`isSeq`/「全表
* 一致」两条平台保护的根治仍记在 `docs/debt.md`(下一个建「单一主键又是 auto 列」的
* 活动仍会踩,除非那两条也一并治了)。回归钉在 `__tests__/activity.test.ts`
* (报告层的 markup)与 `apps/api/src/app/__tests__/view-table-edit.test.ts`
* (真实 `resolveRowKey` 的因果机制,两种 `cells` 形状各自的结果)。
*
* **零行照样加得出第一行**(这是这一块存在的全部理由):`mine:parts.chip_type` 每单
* 开局是零行,`ViewContainer.addRow` 靠克隆 `tr:last-of-type` 添新行,零行时找不到可
@@ -381,7 +404,7 @@ const CHIP_TYPE_TABLE = `<div class="jc-tbl-wrap"><table data-edit="parts.chip_t
</tr></tbody>
<tbody><tr class="jc-row-static">
<td class="rn"><span class="jc-rn-num"></span></td>
<td><span class="jc-vcell ro">系统生成</span></td>
<td><input class="jc-cell mono" data-col="type_id" placeholder="由系统生成" disabled></td>
<td><input class="jc-cell" data-col="type_name" placeholder="新类型名称"></td>
<td><input class="jc-cell" data-col="description" placeholder="这个类型是什么(供选型参考)"></td>
</tr></tbody>