Archived
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:
@@ -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}
|
||||
|
||||
+10
-2
@@ -72,8 +72,16 @@ export type AutoAction = {
|
||||
readonly when?: string | undefined;
|
||||
/** 台账地址「域.对象」—— 必须在 `declarations.ledger` 里(静态审) */
|
||||
readonly into: string;
|
||||
/** 这一笔投影出来的行**就是这张表在本单草稿里的全部**,多出来的撤掉。只动草稿层 */
|
||||
readonly prune?: boolean | undefined;
|
||||
/**
|
||||
* 这一笔投影出来的行**就是这张表在本单草稿里的全部**,多出来的撤掉。只动草稿层。
|
||||
* `true`:只在非 quiet 拍撤 —— 人正对着件编辑的那几拍不动他的行(2026-08-29 终审 F3:
|
||||
* 人刚在件上加的行,配方的数据源里当然没有,quiet 撤行就是静默数据丢失)。
|
||||
* `'live'`(2026-08-31):quiet 拍也撤 —— **只给人不会在件上加行的表用**(行集全由配方铺,
|
||||
* 如 W05 的参数骨架):F3 防的是「人刚加的行被撤」,人加不了行的表没有这个风险,
|
||||
* 而「改了类型,旧类型的参数行还摆在屏上」是另一种撒谎 —— 跟手比守候更诚实。
|
||||
* 已落地的行两档都跳过并出声(draftDrop 的 skipLanded)。
|
||||
*/
|
||||
readonly prune?: boolean | 'live' | undefined;
|
||||
readonly from: AutoSource;
|
||||
readonly fields: Readonly<Record<string, AutoExpr>>;
|
||||
} | {
|
||||
|
||||
@@ -628,7 +628,7 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
|
||||
* 说的是「这张表归人填,模型没有来源」—— 人加的行配方的数据源里当然没有,
|
||||
* 下一拍对齐就把它撤了。两条声明放在同一张表上,意思是相反的。
|
||||
*/
|
||||
if (action.prune === true && (step.humanWrites ?? []).includes(action.into)) {
|
||||
if ((action.prune === true || action.prune === 'live') && (step.humanWrites ?? []).includes(action.into)) {
|
||||
out.push({
|
||||
code: 'schema-invalid',
|
||||
severity: 'warn',
|
||||
|
||||
@@ -795,6 +795,23 @@ describe('prune:人在填的时候不撤行(F3)', () => {
|
||||
expect(out.dropped).toBe(1);
|
||||
expect([...drafts.values()].map((d) => d.key['internal_pn'])).toEqual(['A']);
|
||||
});
|
||||
|
||||
/*
|
||||
* `prune: 'live'`(2026-08-31):quiet 拍也撤 —— 只给**人不会在件上加行**的表用
|
||||
* (行集全由配方铺,如 W05 参数骨架):F3 防的是「人刚加的行被撤」,人加不了行的表
|
||||
* 没有这个风险;而「改了类型,旧类型的参数行还摆在屏上」是另一种撒谎(用户实测点名
|
||||
* 「只会跟着变多,不会跟着变少」)—— 跟手比守候更诚实。
|
||||
*/
|
||||
it("prune:'live' 在 quiet 拍也对齐 —— 人加不了行的表,改选后旧行跟手消失", async () => {
|
||||
const { runner, drafts, ctx } = harness({});
|
||||
await runner.run(SEED, ctx);
|
||||
const out = await runner.run(
|
||||
{ do: [{ kind: 'project', into: 'sub_eval.part', prune: 'live', from: { rows: [{ pn: 'A' }] }, fields: { eval_ref: 'W05-1', internal_pn: { col: 'pn' } } }] },
|
||||
{ ...ctx, quiet: true },
|
||||
);
|
||||
expect(out.dropped).toBe(1);
|
||||
expect([...drafts.values()].map((d) => d.key['internal_pn'])).toEqual(['A']);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -218,7 +218,9 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
||||
* (2026-08-02 为人工撤行立过「撤要说出来、要点名哪几行」的规矩,这里连说都没说)。
|
||||
* 有卡在等人(quiet)= 人正对着这张件填,这一拍只铺不撤;等他交回之后那一拍再对齐。
|
||||
*/
|
||||
if (action.prune === true && !context.quiet) {
|
||||
/* true:非 quiet 拍才撤(F3,人正编辑不动他的行);'live':quiet 也撤 ——
|
||||
只给人不会在件上加行的表用(契约注释与静态审都写着),跟手比守候更诚实 */
|
||||
if (action.prune === 'live' || (action.prune === true && !context.quiet)) {
|
||||
for (const stale of before) {
|
||||
const key: Record<string, string> = {};
|
||||
for (const column of decl.keys) key[column] = String(stale[column] ?? '');
|
||||
|
||||
@@ -363,6 +363,11 @@ W05 的两张数据工单(人填的值已入库、必填不缺、逐颗对账)
|
||||
的规矩,这条路连说都没说)。基座给了两道护栏:有卡在等人(人正对着件填)那几拍**不 prune**;
|
||||
同一张表既被 `prune` 又列在这一步 `humanWrites` 里,装包时报 warn。护栏不是许可 ——
|
||||
要人加行的表就别 `prune` 它。
|
||||
**`prune: 'live'`(2026-08-31)是这条纪律的反面特例**:行集**全由配方铺、人在件上加不了行**
|
||||
的表(如 W05 的参数骨架 —— 行按「料 × 类型字段」派生,件上没有加行钮),quiet 拍也照撤 ——
|
||||
F3 防的风险在这种表上不存在,而「改了类型,旧类型的参数行还摆在屏上」是另一种撒谎
|
||||
(实测:用户改选类型,行「只会跟着变多,不会跟着变少」)。有加行钮的表**永远不许用 `'live'`**。
|
||||
两档都跳过已落地的行并出声(`draftDrop` 的 `skipLanded`)。
|
||||
6. **`card` 之后的那几笔,要等人交回之后的那一拍才跑。** 卡真发出去那一拍到此为止;
|
||||
人交回之后那一拍,`card` 这一笔被跳过、后面的照跑。所以「铺格 → 发卡 → 收口整理」读起来像一拍连做,
|
||||
其实是两拍 —— 收口那一笔看得见人填的值,这正是它该在的位置。装包时会为此报一条 warn 提醒。
|
||||
|
||||
@@ -28,6 +28,13 @@ describe('auto 配方契约', () => {
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it("prune 收 'live' 档 —— quiet 拍也撤,只给人不会在件上加行的表用(其余字符串拒)", () => {
|
||||
const live = autoRecipeSchema.safeParse({ do: [{ kind: 'project', into: 'a.b', prune: 'live', from: { once: true }, fields: { k: 'v' } }] });
|
||||
expect(live.success).toBe(true);
|
||||
const junk = autoRecipeSchema.safeParse({ do: [{ kind: 'project', into: 'a.b', prune: 'always', from: { once: true }, fields: { k: 'v' } }] });
|
||||
expect(junk.success).toBe(false);
|
||||
});
|
||||
|
||||
it('prune 只许挂在 project 上', () => {
|
||||
const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'say', text: 'x', prune: true }] });
|
||||
expect(parsed.success).toBe(false);
|
||||
|
||||
@@ -97,8 +97,16 @@ export type AutoAction =
|
||||
readonly when?: string | undefined;
|
||||
/** 台账地址「域.对象」—— 必须在 `declarations.ledger` 里(静态审) */
|
||||
readonly into: string;
|
||||
/** 这一笔投影出来的行**就是这张表在本单草稿里的全部**,多出来的撤掉。只动草稿层 */
|
||||
readonly prune?: boolean | undefined;
|
||||
/**
|
||||
* 这一笔投影出来的行**就是这张表在本单草稿里的全部**,多出来的撤掉。只动草稿层。
|
||||
* `true`:只在非 quiet 拍撤 —— 人正对着件编辑的那几拍不动他的行(2026-08-29 终审 F3:
|
||||
* 人刚在件上加的行,配方的数据源里当然没有,quiet 撤行就是静默数据丢失)。
|
||||
* `'live'`(2026-08-31):quiet 拍也撤 —— **只给人不会在件上加行的表用**(行集全由配方铺,
|
||||
* 如 W05 的参数骨架):F3 防的是「人刚加的行被撤」,人加不了行的表没有这个风险,
|
||||
* 而「改了类型,旧类型的参数行还摆在屏上」是另一种撒谎 —— 跟手比守候更诚实。
|
||||
* 已落地的行两档都跳过并出声(draftDrop 的 skipLanded)。
|
||||
*/
|
||||
readonly prune?: boolean | 'live' | undefined;
|
||||
readonly from: AutoSource;
|
||||
readonly fields: Readonly<Record<string, AutoExpr>>;
|
||||
}
|
||||
@@ -145,7 +153,7 @@ export const autoActionSchema: z.ZodType<AutoAction> = z.discriminatedUnion('kin
|
||||
kind: z.literal('project'),
|
||||
when: z.string().min(1).optional(),
|
||||
into: z.string().regex(/^[a-z_]+\.[a-z_]+$/, '台账地址写成「域.对象」'),
|
||||
prune: z.boolean().optional(),
|
||||
prune: z.union([z.boolean(), z.literal('live')]).optional(),
|
||||
from: autoSourceSchema,
|
||||
fields: z.record(z.string(), autoExprSchema),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user