feat(w05): 比对基准合一卡 + prune:'live' —— 类型与字段跟手,只显当前那一种

2026-08-31 用户第二轮:「芯片类型列表还是有问题…和核心参数字段不能合并成一个卡片吗…
两个芯片都选一样的就应该只显示一种,现在只会跟着变多不会跟着变少」+
「卡片下面那个容器黑色和页面背景不搭」。

① 比对基准卡(类型 + 字段合一,只读、跟手):不再列 mine: 草稿镜像(镜像里积着改选前的
  旧类型 —— 撤行只在确认那一拍跑,F3 不许在人编辑时静默删行),按案卷锚
  (sub_eval.case.chip_type_id,配方每拍现算)嵌套查字典:卡上改选、按保存,quiet 拍
  project 照跑,这张卡当场换脸 —— 两颗料同类型时恰好只有那一种;没人选时空态指路。
  新建类型收进 details 抽屉(零脚本),两张编辑表原样在抽屉里(种子行/加行机制不动)。

② 契约新增 prune:'live'(quiet 拍也撤):只给**人不会在件上加行**的表用 —— F3 防的是
  「人刚加的行被撤」,人加不了行的表没有这个风险,而「改了类型,旧类型的参数行还摆在
  屏上」是另一种撒谎。W05 参数骨架 ⑤ 改 'live':改选类型,参数行跟手换。
  static-audit 的 prune×humanWrites 审计盖住 'live';规范 §4.6 纪律 5 补这条反面特例;
  两档都跳过已落地的行并出声(skipLanded)。

③ 板底改浅色蓝图:深色容器与纸面色阶断裂(用户点名),换强调色极浅晕染 + 淡格线,
  走线与信号点用强调色 —— 电路语汇留着,整页只剩 hero 一处深色。

测:契约 'live' 解析 + runner quiet-live 对齐(变异验证)+ 基准卡锚(变异验证)+
steps ⑤ 钉 'live';108 包内 + 全仓四门绿;2.8.1 装库 SQL 核过;:8787 已跑新构建
(watcher 又断,按应急程序手起 node dist/main.js)。

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 14:47:25 +08:00
co-authored by Claude Fable 5
parent 5a7bb9b58f
commit 11375eaf93
13 changed files with 172 additions and 27 deletions
@@ -581,6 +581,31 @@ describe('件① 评估输入补全', () => {
expect(html).not.toContain(':has(+ select.jc-cell:not(:has(');
});
/*
* 2026-08-31 用户第二轮:「芯片类型列表和核心参数字段,不能合并成一个卡片吗…两个芯片都
* 选一样的,就应该只显示一种」。合并 = 比对基准卡(类型名 + 字段清单一张只读卡);
* 「只显示一种、跟手」= 不列 mine: 草稿镜像(会积旧选择),按案卷锚(case.chip_type_id,
* 配方每拍现算)嵌套查字典 —— quiet 拍 project 照跑,保存即换脸。
* 新建类型收进 details 抽屉(零脚本),两张编辑表原样在抽屉里(种子行/加行机制不动)。
*/
it('比对基准卡:case 锚嵌套查字典,类型与字段合一、全只读;编辑表收进抽屉', () => {
expect(html).toContain('data-rows="mine:sub_eval.case"');
const basis = /<article class="basis"[\s\S]*?<\/article>/.exec(html)?.[0] ?? '';
expect(basis).not.toBe('');
expect(basis).toContain('data-rows="parts.chip_type" data-by="type_id=chip_type_id"');
expect(basis).toContain('data-rows="parts.type_param" data-by="type_id=chip_type_id"');
/* 全只读:选类型在档案卡,建类型在抽屉 —— 基准卡上一个编辑面都不许有 */
expect(basis).not.toContain('data-col');
expect(basis).not.toContain('data-edit');
/* 抽屉存在,且两张编辑表都住在里面 */
const labAt = html.indexOf('<details class="lab">');
const labEnd = html.indexOf('</details>');
expect(labAt).toBeGreaterThanOrEqual(0);
expect(html.indexOf('<table data-edit="parts.chip_type">')).toBeGreaterThan(labAt);
expect(html.indexOf('<table data-edit="parts.type_param">')).toBeGreaterThan(labAt);
expect(labEnd).toBeGreaterThan(html.indexOf('<table data-edit="parts.type_param">'));
});
it('三级占位:必填红 / 建议橘 —— 空格子直说要不要填(档案卡与参数值表两处)', () => {
expect(html).toContain('.fcell.req .jc-vcell:empty::after{content:"必填"');
expect(html).toContain('.fcell.sug .jc-vcell:empty::after{content:"建议"');
@@ -744,14 +769,14 @@ describe('件① 评估输入补全', () => {
* 只读态与编辑态下「#」列逐行都是空白。钉死 `.jc-tbl-wrap` 现在也接了这五条规则,
* 且这张件确实只用 `.jc-tbl-wrap`(没有表在用没打这条补丁就会数不出来的 `.tbl-wrap`)。
*/
it('三张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
it('四张表的序号列真的会数:.jc-tbl-wrap 也接了 CSS 计数器(不再只认 .tbl-wrap)', () => {
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s*,\s*\.tbl-wrap\s+table\s*\{\s*counter-reset:\s*rn;?\s*\}/);
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s*\{\s*counter-increment:\s*rn;?\s*\}/);
expect(html).toMatch(/\.jc-tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*,\s*\.tbl-wrap\s+table\s+td\.rn\s+\.jc-rn-num::before\s*\{\s*content:\s*counter\(rn\);?\s*\}/);
// 这张件的三张表都走 .jc-tbl-wrap(没有 .tbl-wrap)—— 2026-08-31 块① 换成档案卡(不是表),4→3
// 四张:比对基准卡的字段表 + 抽屉里两张编辑表 + 参数值表(2026-08-31 类型/字段并成基准卡后的账)
expect(html).not.toContain('class="tbl-wrap"');
const jcTblWrapCount = html.match(/class="jc-tbl-wrap"/g)?.length ?? 0;
expect(jcTblWrapCount).toBe(3);
expect(jcTblWrapCount).toBe(4);
});
});
@@ -214,7 +214,8 @@ describe('S1 配方', () => {
it('参数骨架:两颗料都铺、值回填现值 —— 「在库但当年参数没录全」也要被拦住', () => {
const params = acts[4];
expect(params?.kind === 'project' && params.prune).toBe(true);
/* 'live':人加不了行的表,quiet 拍也撤 —— 改类型后旧参数行跟手消失(2026-08-31 用户点名「只会变多不会变少」) */
expect(params?.kind === 'project' && params.prune).toBe('live');
const from = params?.kind === 'project' ? params.from : undefined;
/* 外层是本单两颗料,里层是该类型的字段清单 —— 人没挑类型时 where 一行不命中,什么也不铺 */
expect(from).toMatchObject({ each: 'mine:sub_eval.part', then: { each: 'parts.type_param' } });
@@ -492,7 +492,13 @@ export const ASSESSMENT_STEPS = [
{
kind: 'project' as const,
into: 'parts.parameter',
prune: true,
/*
* **`prune: 'live'`(2026-08-31 用户:「切换料号类型时…只会跟着变多,不会跟着变少」)**:
* 参数骨架的行集全由这一笔按「料 × 类型字段」铺,人在件上**加不了行**(没有加行钮),
* F3 那条「quiet 拍不撤行」防的风险在这张表上不存在 —— 人改了类型、按保存,旧类型的
* 参数行当场撤掉,屏上跟手。已落地的行照旧跳过并出声(skipLanded)。
*/
prune: 'live' as const,
from: {
each: 'mine:sub_eval.part',
then: { each: 'parts.type_param', where: { type_id: { outer: 'chip_type_id' } } },
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.7.0',
packageVersion: '2.8.1',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -204,6 +204,43 @@ const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mi
</div>
</section></article></section>`;
/**
* 比对基准卡 —— 芯片类型与它的核心参数字段**合成一张只读卡**(2026-08-31 用户:
* 「芯片类型列表和核心参数字段,不能合并成一个卡片吗…两个芯片都选一样的,就应该只显示一种」)。
*
* **显示永远跟手、永远只有当前那一种**:不列 `mine:parts.chip_type` 草稿(镜像里会积着
* 改选前的旧类型 —— 撤行只在确认工单那一拍跑,F3 不许在人编辑时静默删行),而是按**案卷锚**
* (`sub_eval.case.chip_type_id`,配方 ⑥ 每拍从原 PN 的选择现算)嵌套查字典:人卡上改选、
* 按保存,quiet 拍照样跑 project,锚跟着动,这张卡当场换脸 —— 两颗料选同一类型时,这里
* 恰好就是那一种;还没人选类型时零行,空态一句话指路。
*
* 全只读(一个 data-col 都没有)—— 选类型在档案卡上,建类型在下面的工作抽屉里。
* 嵌套三层:case(恰一行)→ chip_type 按锚查名 → type_param 按锚列字段;
* 宿主标签各不与行模版里的标签撞(article / span / tbody)。
*/
const BASIS_CARD = `<article class="basis" data-rows="mine:${D}.case"><section class="basis-in">
<header class="basis-hd">
<span class="basis-lb">比对基准</span>
<span class="basis-chip" data-rows="parts.chip_type" data-by="type_id=chip_type_id"><b data-slot="type_name" data-blank></b><i class="basis-desc" data-slot="description" data-blank></i></span>
</header>
<div class="jc-tbl-wrap"><table>
<thead><tr>
<th style="width:38px">#</th>
<th data-h="参数名">参数名</th>
<th data-h="参数说明">参数说明</th>
<th data-h="约束类型">约束(硬/软)</th>
<th data-h="参考单位">参考单位</th>
</tr></thead>
<tbody data-rows="parts.type_param" data-by="type_id=chip_type_id"><tr>
<td class="rn"><span class="jc-rn-num"></span></td>
<td class="cust" data-slot="param_name" data-blank></td>
<td class="desc" data-slot="param_desc" data-blank></td>
<td><span class="ct" data-class-of="constraint_type"></span></td>
<td data-slot="param_unit" data-blank></td>
</tr></tbody>
</table></div>
</section></article>`;
/**
* 块②a 新建芯片类型 —— `parts.chip_type`。类型下拉(块①)里没有合适的,人在这里现造一个。
* `type_id` 是自增主键 —— **人不填它**,自增列不问人;已有行只读态摆 `ro()`。
@@ -352,16 +389,19 @@ export const INPUT_SUPPLEMENT = doc({
editGate('parts.profile', '两张档案卡上的主数据核对 / 补齐,点「编辑」开卡') +
gapbar('parts_profile', '必填') +
DOSSIERS +
zone('芯片类型', '本单选用 / 新建的类型;卡上改选之后,旧选择在你确认工单那一刻自动清掉(已入库的留档)', ZIC_TABLE) +
editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在上面的下拉里') +
zone('比对基准 · 芯片类型与核心参数字段', '两颗料归到哪个类型,就按哪个类型的字段逐项比 —— 档案卡上改选,这张卡当场跟着换', ZIC_TABLE) +
BASIS_CARD +
gapbar('parts_chip_type', '必填') +
gapbar('parts_type_param', '必填') +
`<details class="lab"><summary class="lab-sm"><b>类型库里没有合适的?</b>展开新建一个类型,并把它的核心参数字段定下来 —— 定完回到上面的档案卡挑它<span class="lab-caret"></span></summary><div class="lab-bd">` +
editGate('parts.chip_type', '新建一个类型:表尾那一行填类型名称与说明,保存后立刻出现在档案卡的下拉里') +
CHIP_TYPE_TABLE +
addrow('parts.chip_type', '+ 再建一个类型', '两颗料若都要新建、且类型不同,先各自保存,「加一行」能再建一个') +
zone('核心参数字段', '这个类型逐条比对时要看哪些参数(新建类型时必填,*为必填列)', ZIC_TABLE) +
`<p class="lab-sub">这个类型逐条比对时要看哪些参数 —— 名字 / 说明 / 硬软约束(*为必填列)</p>` +
editGate('parts.type_param', '字段名字 / 说明 / 硬软约束,点「编辑」核对;表尾那一行 / 「加一行」补字段') +
gapbar('parts_type_param', '必填') +
TYPE_PARAM_TABLE +
addrow('parts.type_param', '+ 加一行(补一个核心参数字段)', '所属类型 / 参数名 / 参数说明 / 约束类型必须写;参考单位选填') +
`</div></details>` +
zone('参数值', '逐条核心参数的实测 / 规格值(一字段一行,*为必填列;单位由字典定,这里不改)', ZIC_TABLE) +
editGate('parts.parameter', '逐条参数值核对 / 补齐,点「编辑」开表') +
gapbar('parts_parameter', '必填') +
@@ -246,18 +246,19 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
*{animation:none !important;transition:none !important}
}
/* ================= 电路板工作台(件① 的两张档案卡)================= */
.board{position:relative;margin:0 0 6px;padding:20px 16px 18px;border-radius:16px;
/* ================= 电路蓝图工作台(件① 的两张档案卡)=================
2026-08-31 用户:「卡片下面那个容器黑色和页面背景不搭」—— 深色板底退场,换**浅色蓝图**:
底是强调色的极浅晕染 + 淡格线,走线与信号点用强调色 —— 电路语汇留着,纸面色阶不断裂。 */
.board{position:relative;margin:0 0 6px;padding:18px 14px 16px;border-radius:16px;border:1px solid var(--acc-line);
background:
repeating-linear-gradient(0deg, rgba(255,255,255,.03) 0 1px, transparent 1px 26px),
repeating-linear-gradient(90deg, rgba(255,255,255,.03) 0 1px, transparent 1px 26px),
radial-gradient(700px 260px at 16% -30%, var(--hero-tint), transparent 62%),
linear-gradient(158deg, #13212f 0%, var(--board) 58%, #0f1d2b 100%)}
.board::before{content:"";position:absolute;left:4%;right:4%;top:50%;border-top:2px dashed var(--glow-line)}
.board::after{content:"";position:absolute;top:50%;left:4%;width:9px;height:9px;margin-top:-4px;border-radius:999px;background:var(--glow);box-shadow:0 0 14px 3px var(--glow-line),0 0 4px 1px var(--glow);animation:sig 3.4s linear infinite}
repeating-linear-gradient(0deg, rgba(46,107,255,.045) 0 1px, transparent 1px 26px),
repeating-linear-gradient(90deg, rgba(46,107,255,.045) 0 1px, transparent 1px 26px),
linear-gradient(158deg, var(--acc-wash) 0%, #fff 62%, var(--acc-wash) 100%)}
.board::before{content:"";position:absolute;left:4%;right:4%;top:50%;border-top:2px dashed var(--acc-line)}
.board::after{content:"";position:absolute;top:50%;left:4%;width:8px;height:8px;margin-top:-4px;border-radius:999px;background:var(--acc);box-shadow:0 0 8px 2px var(--acc-soft);animation:sig 3.4s linear infinite}
.dossiers{position:relative;z-index:1;display:grid;grid-template-columns:repeat(auto-fit,minmax(272px,1fr));gap:16px}
.dossier{display:flex;flex-direction:column;gap:11px;background:rgba(255,255,255,.97);border-radius:16px;padding:15px 16px 14px;box-shadow:0 14px 30px rgba(3,10,18,.5),inset 0 1px 0 rgba(255,255,255,.7);transition:box-shadow .18s,transform .18s}
.dossier:hover{transform:translateY(-2px);box-shadow:0 18px 36px rgba(3,10,18,.55),inset 0 1px 0 rgba(255,255,255,.7)}
.dossier{display:flex;flex-direction:column;gap:11px;background:#fff;border:1px solid var(--line-soft);border-radius:16px;padding:15px 16px 14px;box-shadow:0 8px 22px rgba(18,38,63,.09);transition:box-shadow .18s,transform .18s}
.dossier:hover{transform:translateY(-2px);box-shadow:0 12px 28px rgba(18,38,63,.13)}
.dz-top{display:flex;align-items:center;gap:12px}
.dz-top .csvg{width:64px;flex:none}
.dz-id{display:flex;flex-direction:column;gap:4px;min-width:0}
@@ -297,6 +298,31 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
.jc-tbl-wrap td.pv .jc-vcell:empty::after{content:"必填";font-size:10.5px;font-weight:800;color:var(--danger);background:var(--danger-soft);border-radius:999px;padding:0 9px}
.jc-tbl-wrap td.sg .jc-vcell:empty::after{content:"建议";font-size:10.5px;font-weight:800;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:0 9px}
/* ================= 比对基准卡(类型 + 字段合一,只读、跟手)================= */
.basis{display:block;margin:0 0 10px}
.basis:empty{border:1.5px dashed var(--line);border-radius:12px;padding:16px;display:block}
.basis:empty::after{content:"还没定比对基准 —— 先在上面任一张档案卡上挑一个芯片类型,或展开下面的抽屉新建一个。";font-size:12.5px;color:var(--ink-3)}
.basis-in{display:flex;flex-direction:column;gap:10px;border:1px solid var(--acc-line);border-radius:12px;background:var(--acc-wash);padding:12px 14px 14px}
.basis-hd{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.basis-lb{flex:none;font-size:10.5px;font-weight:800;letter-spacing:.08em;color:var(--acc-deep)}
.basis-chip b{display:inline-block;font-size:13px;font-weight:800;color:#fff;background:var(--acc);border-radius:999px;padding:2px 14px;margin-right:8px}
.basis-desc{font-style:normal;font-size:11.5px;color:var(--ink-3)}
.basis-hd:has(.basis-chip:empty)::after{content:"待选择";font-size:11px;font-weight:700;color:var(--amber);background:var(--amber-soft);border:1px dashed #eeda9e;border-radius:999px;padding:1px 10px}
.basis-in:has(tbody:empty) .jc-tbl-wrap{display:none}
.basis-in .jc-tbl-wrap{background:#fff}
/* ================= 新建类型工作抽屉(details,零脚本)================= */
.lab{margin:0 0 6px;border:1.5px dashed var(--line);border-radius:12px;background:var(--soft)}
.lab[open]{border-style:solid;background:#fff}
.lab-sm{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;padding:11px 14px;font-size:12.5px;color:var(--ink-3);user-select:none}
.lab-sm::-webkit-details-marker{display:none}
.lab-sm b{color:var(--ink-2)}
.lab-sm:hover{color:var(--acc-deep)}
.lab-caret{margin-left:auto;flex:none;width:8px;height:8px;border-right:2px solid var(--ink-4);border-bottom:2px solid var(--ink-4);transform:rotate(45deg);transition:transform .16s}
.lab[open] .lab-caret{transform:rotate(-135deg)}
.lab-bd{padding:2px 14px 14px}
.lab-sub{margin:16px 0 8px;font-size:11.5px;color:var(--ink-3)}
/* ================= 窄屏 ================= */
@media(max-width:620px){
.card-head{padding:16px 14px 18px}