feat(w05+platform): 确认闸条件档 gateWhen —— 类型不一致不许确认;量高改可缩;卡间互换动画

2026-08-31 用户三连:

① 「提示两颗料不是同一个芯片类型…就不应该能写入并继续下一步」——
  缺格闸只数必填空不空,管不了跨行一致性。平台加**确认闸条件档**:配方 card 声明
  gateWhen(S-FEEL,只读基座读数)+ gateWhenSay,受理回传前在 submitGate 现算
  (rows/missing/tally/distinct 全套读数按声明表现读),为假 400、拒回用发卡人那句话;
  装包静态审与动作级 when 同一套审;评估不出的条件不成墙(坏表达式装包就拦)。
  W05 钉 parts_profile_distinct_chip_type_id <= 1 —— 钉 profile 本表(草稿现读零滞后;
  钉 sub_eval.part 那份要等配方 tick,人存完就点确认会被误拦),profile.chip_type_id
  加 distinct 声明。e2e 案例 2 的空手探针从此吃的就是这道闸。

② 「页面底部没东西还能向下滑一截」—— ViewContainer 量高从
  max(documentElement.scrollHeight, body) 改 body 优先:documentElement 以 iframe 自身
  高度为下限,内容缩回后它缩不回去,量高**只涨不缩**(参数编辑态把卡背拉长过一次,
  底部从此留一截空白滚动)。body(margin 0)才是内容自己的高。

③ 「两张卡片中间做出可替换的动画」—— 单点单向信号换成**对向双信号点**(左右互穿,
  走在卡后、只在缝里现身)+ 中央 ⇄ 徽轻呼吸 —— 这界面是判「可否替换」的,动画说的
  就是这件事。reduced-motion 全静。

测:契约 gateWhen 解析 + runner 透传(变异红)+ conditionGateOf 3 例(含坏表达式不成墙)
+ steps/activity 锚;2531 全仓四门绿;2.12.0 装库核过(gateWhen 入库/swapfx);
:8787 已重建重启。

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 17:43:08 +08:00
co-authored by Claude Fable 5
parent 7515885afa
commit c4219a7b7d
20 changed files with 151 additions and 11 deletions
@@ -628,6 +628,12 @@ describe('件① 评估输入补全', () => {
expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}');
/* 悬浮提示丸:看得出点击可翻 */
expect(html).toContain('点击翻面');
/* 两卡之间的「可替换」动画:对向双信号点 + 中央 ⇄ 徽(2026-08-31 用户定) */
expect(html).toContain('class="swapfx"');
expect(html).toContain('class="sw-dot sw-ltr"');
expect(html).toContain('class="sw-dot sw-rtl"');
expect(html).toContain('@keyframes swltr');
expect(html).toContain('@keyframes swrtl');
});
it('整件零表格(2026-08-31 用户:「这个界面都不再需要表格来展示数据了」;抽屉连表带功能一并退役)', () => {
@@ -241,6 +241,12 @@ describe('S1 配方', () => {
expect(card?.kind === 'card' && card.as).toBe('execution');
expect(card?.kind === 'card' && card.variant).toBe('data');
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.profile', 'parts.parameter']);
/*
* 类型不一致不许确认(2026-08-31 用户定)。钉 parts.profile 本表的 distinct ——
* 提交闸从草稿现读零滞后;钉 sub_eval.part 那份要等配方 tick,人存完就点确认会被误拦。
*/
expect(card?.kind === 'card' && card.gateWhen).toBe('parts_profile_distinct_chip_type_id <= 1');
expect(card?.kind === 'card' && typeof card.gateWhenSay === 'string' && card.gateWhenSay.includes('不是同一个芯片类型')).toBe(true);
expect((card?.kind === 'card' ? card.basis ?? [] : []).length).toBeGreaterThan(0);
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
});
@@ -543,6 +543,15 @@ export const ASSESSMENT_STEPS = [
* 落地无对象、mine: 恒为空 —— 留在名单上反而会被「零行 = 没干完」的闸误拦死。
*/
gateLedgers: ['parts.profile', 'parts.parameter'],
/*
* **类型不一致不许确认**(2026-08-31 用户:「提示两颗料不是同一个芯片类型…就不应该
* 能写入并继续下一步」)。缺格闸管不了这件事(它只数必填空不空),条件档在受理
* 回传前现算 —— 钉 **parts.profile 本表**的 distinct(提交闸从草稿现读,零滞后;
* 钉 sub_eval.part 的那份要等配方 tick 同步,人存完就点确认会被误拦)。
* <= 1:两颗同型(1)或都还没选(0,那时缺格闸已经拦着)都放;>= 2 才是这堵墙。
*/
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
gateWhenSay: '两颗料不是同一个芯片类型 —— 逐项比对无从谈起:换一颗料,或把归错的那颗类型改对,再来确认。',
},
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
],
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
packageVersion: '2.11.0',
packageVersion: '2.12.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -347,7 +347,7 @@ export const manifest: ActivityManifest = {
fields: [
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true, refersTo: 'parts.chip_type.type_id' }),
col({ name: 'chip_type_id', type: 'text', label: '芯片类型', required: true, distinct: true, refersTo: 'parts.chip_type.type_id' }),
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
col({ name: 'specification_description', type: 'text', label: '规格描述', expect: true }),
@@ -208,7 +208,7 @@ ${badgeCss('.rl', [
* data-edit 祖先」,packages/ui-kit/__tests__/view-container-multihost.test.ts 钉着);
* 开哪面的编辑,卡就停在哪面(主数据编辑锁正面、参数编辑自动翻背面,CSS 兜底)。
*/
const DOSSIERS = `<section class="board"><article class="dossiers" data-rows="mine:parts.profile" data-edit="parts.profile"><section class="dossier">
const DOSSIERS = `<section class="board"><span class="swapfx" aria-hidden="true"><i class="sw-dot sw-ltr"></i><i class="sw-dot sw-rtl"></i><svg class="sw-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3 4 7l4 4"/><path d="M4 7h16"/><path d="m16 21 4-4-4-4"/><path d="M20 17H4"/></svg></span><article class="dossiers" data-rows="mine:parts.profile" data-edit="parts.profile"><section class="dossier">
<label class="flip-hit"><input class="flip-tg" type="checkbox" aria-label="翻转这张卡"></label>
<span class="flip-cue"><i class="cue-front">点击翻面 · 核心参数</i><i class="cue-back">点击翻回 · 主数据</i></span>
<span class="fabs">
@@ -269,7 +269,17 @@ table.on tr.jc-row-static td{background:var(--acc-wash);border-top:1px dashed va
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}
/* 「可替换」动画(2026-08-31 用户定:两卡之间要做出可互换的效果)——
两枚信号点沿总线**对向穿行**(左料换到右、右料换到左),中央一枚 ⇄ 徽轻呼吸;
点走在卡片背后(z 序),只在两卡之间的缝里现身 —— 像两颗料在板上换位。 */
.swapfx{position:absolute;left:0;right:0;top:50%;height:0;z-index:0;pointer-events:none;--swx:min(26vw,240px)}
.sw-ic{position:absolute;left:50%;top:0;transform:translate(-50%,-50%);width:34px;height:34px;padding:7px;box-sizing:border-box;color:var(--acc);background:#fff;border:1px solid var(--acc-line);border-radius:999px;box-shadow:0 4px 12px rgba(18,38,63,.14);z-index:2;animation:swbreathe 2.8s ease-in-out infinite}
.sw-dot{position:absolute;left:50%;top:-13px;width:8px;height:8px;margin-left:-4px;border-radius:999px;background:var(--acc);box-shadow:0 0 8px 2px var(--acc-soft)}
.sw-ltr{animation:swltr 3.2s linear infinite}
.sw-rtl{top:7px;animation:swrtl 3.2s linear infinite}
@keyframes swltr{0%{transform:translateX(calc(-1 * var(--swx)));opacity:0}12%{opacity:1}88%{opacity:1}100%{transform:translateX(var(--swx));opacity:0}}
@keyframes swrtl{0%{transform:translateX(var(--swx));opacity:0}12%{opacity:1}88%{opacity:1}100%{transform:translateX(calc(-1 * var(--swx)));opacity:0}}
@keyframes swbreathe{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}
.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:#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)}
@@ -428,6 +438,7 @@ html.editable .dossier:not(.on):not(:has(.pback.on)):has(.flip-tg:checked) .pen-
@media(max-width:620px){
.card-head{padding:16px 14px 18px}
.dz-grid{grid-template-columns:1fr}
.swapfx{display:none}
.pback{grid-template-columns:1fr}
.ftiles{grid-template-columns:1fr}
.card-bd{padding:16px 14px 6px}
+8
View File
@@ -907,6 +907,8 @@ export declare const taskDetailSchema: z.ZodObject<{
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
@@ -1498,6 +1500,8 @@ export declare const taskDetailSchema: z.ZodObject<{
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
@@ -2831,6 +2835,8 @@ export declare const taskDetailRoute: {
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
@@ -3422,6 +3428,8 @@ export declare const taskDetailRoute: {
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
+3
View File
@@ -118,6 +118,9 @@ export type AutoAction = {
}[] | undefined;
/** 这张卡的提交闸盯哪几张表 */
readonly gateLedgers?: readonly string[] | undefined;
/** 确认闸的条件档(S-FEEL,只读基座读数;为假不受理确认)+ 拒回那句话 —— 见 card.ts 同名字段 */
readonly gateWhen?: string | undefined;
readonly gateWhenSay?: string | undefined;
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
readonly options?: readonly {
readonly value: string;
+4
View File
@@ -862,6 +862,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
@@ -1482,6 +1484,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
}, z.core.$strip>>;
submitLabel: z.ZodOptional<z.ZodString>;
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
gateWhen: z.ZodOptional<z.ZodString>;
gateWhenSay: z.ZodOptional<z.ZodString>;
gate: z.ZodOptional<z.ZodObject<{
ready: z.ZodBoolean;
why: z.ZodString;
@@ -689,3 +689,20 @@ describe('decisionReadings:按 card-reply 数', () => {
expect(decisionReadings([])).toEqual({ decision_last: '', decision_approve_total: 0, decision_reject_total: 0, decision_custom_total: 0 });
});
});
describe('conditionGateOf:确认闸的条件档(2026-08-31,「类型不一致还能确认」逼出)', () => {
it('条件为假 → 不受理,拒回用发卡人那句话;为真 / 没声明 → 不拦', async () => {
const { conditionGateOf } = await import('../routes/desk.ts');
const readings = { parts_profile_distinct_chip_type_id: 2 };
const blocked = conditionGateOf('parts_profile_distinct_chip_type_id <= 1', '两颗料不是同一个芯片类型 —— 先改对再确认。', readings);
expect(blocked?.ready).toBe(false);
expect(blocked?.why).toContain('不是同一个芯片类型');
expect(conditionGateOf('parts_profile_distinct_chip_type_id <= 1', '话', { parts_profile_distinct_chip_type_id: 1 })).toBeUndefined();
expect(conditionGateOf(undefined, undefined, readings)).toBeUndefined();
});
it('表达式炸了不成墙(装包静态审管坏表达式;运行时评估不出的条件不该锁死一单)', async () => {
const { conditionGateOf } = await import('../routes/desk.ts');
expect(conditionGateOf('((((', '话', {})).toBeUndefined();
});
});
+37 -2
View File
@@ -45,7 +45,8 @@ function rowsOf<T>(result: unknown): T[] {
}
import { staffOf } from '../http-env.ts';
import type { AppEnv } from '../http-env.ts';
import { coerceToColumn, missingReadings, rowLabel } from '../wiring/engines.ts';
import { evaluateCondition } from '@sfeel';
import { coerceToColumn, distinctReadings, missingReadings, rowLabel, rowReadings, tallyReadings } from '../wiring/engines.ts';
import { orderChat } from '../chat-order.ts';
@@ -816,7 +817,41 @@ async function submitGate(
for (const draft of await container.ledger.taskDrafts(taskId)) {
(mine[`mine:${draft.domain}.${draft.object}`] ??= []).push({ ...draft.key, ...draft.patch });
}
return emptyGate(mine, watched, declared) ?? gateOf(missingReadings(mine, watched), watched);
const base = emptyGate(mine, watched, declared) ?? gateOf(missingReadings(mine, watched), watched);
if (!base.ready) return base;
/*
* **条件档**(2026-08-31,W05「两颗料不是同一个芯片类型还能确认」逼出):缺格闸都过了,
* 发卡的人还声明了一条 S-FEEL 条件的,再按**全部声明表的基座读数**现算一遍 —— 为假就
* 不受理。读数从本单草稿现读(rows/missing/tally/distinct 同一套函数),没有 tick 滞后:
* 人刚在件上改完类型、点确认,算的就是改完之后的样子。
*/
return (
conditionGateOf(order.gateWhen, order.gateWhenSay, {
...rowReadings(mine, manifest.declarations.ledger),
...missingReadings(mine, manifest.declarations.ledger),
...tallyReadings(mine, manifest.declarations.ledger),
...distinctReadings(mine, manifest.declarations.ledger),
}) ?? base
);
}
/**
* 确认闸的条件档 —— S-FEEL 为假就交不了,拒回用发卡人写的那句话(display 与服务端同一段)。
* 条件只读基座读数(机检纪律)。表达式炸了按「不拦」:坏表达式装包时静态审就该拦
* (checkWhen 已审),运行时评估不出的条件不该成墙(「没有依据的时候不拦人」)。
*/
export function conditionGateOf(
when: string | undefined,
say: string | undefined,
readings: Readonly<Record<string, string | number>>,
): { ready: boolean; why: string; blanks: string[] } | undefined {
if (when === undefined || when.trim() === '') return undefined;
try {
if (evaluateCondition(when, readings)) return undefined;
} catch {
return undefined;
}
return { ready: false, why: say ?? '这张卡声明的确认条件还不成立 —— 回件上把它办对再来确认。', blanks: [] };
}
/**
@@ -612,6 +612,8 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
const where = `${at}.do[${index}]`;
/* 动作级 `when`:语法 + 名字都审(它看得见的只有 scope 与 let,没有行内列) */
if (action.when !== undefined) checkWhen(action.when, where, letNames);
/* 确认闸的条件档与动作级 when 同一作用域(基座读数),同一套审 */
if (action.kind === 'card' && action.gateWhen !== undefined) checkWhen(action.gateWhen, where, letNames);
if (action.kind === 'project') {
checkAddress(action.into, where);
walkSource(action.from, where);
@@ -183,6 +183,10 @@
(2026-08-30 e2e 案例 2 的死锁,当时以「W05 ②③ 两笔不带 prune」绕开,代价是**没落地的**
陈旧行也不清 —— 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」)。
模型那一路不传:那句「已入库撤不掉」的拒回是它的行动指南,一个字不变。
- 配方 `card` 多两个可选键 `gateWhen` / `gateWhenSay`(2026-08-31):确认闸的**条件档** ——
除了「必填缺几格」,发卡的人还可声明一条 S-FEEL 条件(只读基座读数,装包静态审同
`when` 一套审),受理回传前在 app 层 `submitGate` 现算,为假不受理、拒回用发卡人那句话
(W05 第一例:两颗料不是同一个芯片类型不许确认)。运行时评估不出的条件不成墙。
同批三条不变量:
- **配方由 `liveStep` 驱动,不由 `current` 的兜底驱动**:兜底(在场的第一个有剧本的项)不过滤
@@ -398,6 +398,8 @@ describe('card 与 submit', () => {
standard: '必填格全部填上,红带消失',
basis: [{ k: '候选料号', v: { in: 'pn' } }],
gateLedgers: ['parts.profile', 'parts.parameter'],
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
gateWhenSay: '两颗料不是同一个芯片类型 —— 先改对再确认。',
},
],
},
@@ -415,6 +417,9 @@ describe('card 与 submit', () => {
standard: '必填格全部填上,红带消失',
basis: [{ k: '候选料号', v: 'XL2596S-5.0' }],
gateLedgers: ['parts.profile', 'parts.parameter'],
/* 确认闸条件档随卡走(2026-08-31)—— 服务端 submitGate 靠它拦「类型不一致还确认」 */
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
gateWhenSay: '两颗料不是同一个芯片类型 —— 先改对再确认。',
fields: [],
},
});
@@ -371,6 +371,9 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
...(action.guide === undefined ? {} : { steps: [...action.guide] }),
...(action.standard === undefined ? {} : { standard: evalText(action.standard, base) }),
...(action.gateLedgers === undefined ? {} : { gateLedgers: [...action.gateLedgers] }),
...(action.gateWhen === undefined ? {} : { gateWhen: action.gateWhen }),
...(action.gateWhenSay === undefined ? {} : { gateWhenSay: action.gateWhenSay }),
},
});
return { wrote: 0, dropped: 0, cardId: id, stop: true };
@@ -35,6 +35,13 @@ describe('auto 配方契约', () => {
expect(junk.success).toBe(false);
});
it('card 收确认闸条件档 gateWhen/gateWhenSay(2026-08-31,类型不一致不许确认)', () => {
const ok = autoRecipeSchema.safeParse({
do: [{ kind: 'card', as: 'execution', title: 't', gateWhen: 'a_b_distinct_c <= 1', gateWhenSay: '先改对再确认' }],
});
expect(ok.success).toBe(true);
});
it('prune 只许挂在 project 上', () => {
const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'say', text: 'x', prune: true }] });
expect(parsed.success).toBe(false);
+5
View File
@@ -139,6 +139,9 @@ export type AutoAction =
readonly basis?: readonly { readonly k: string; readonly v: AutoExpr }[] | undefined;
/** 这张卡的提交闸盯哪几张表 */
readonly gateLedgers?: readonly string[] | undefined;
/** 确认闸的条件档(S-FEEL,只读基座读数;为假不受理确认)+ 拒回那句话 —— 见 card.ts 同名字段 */
readonly gateWhen?: string | undefined;
readonly gateWhenSay?: string | undefined;
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
readonly options?:
| readonly { readonly value: string; readonly label: string; readonly detail?: string | undefined }[]
@@ -181,6 +184,8 @@ export const autoActionSchema: z.ZodType<AutoAction> = z.discriminatedUnion('kin
standard: autoExprSchema.optional(),
basis: z.array(z.object({ k: z.string().min(1), v: autoExprSchema }).strict()).optional(),
gateLedgers: z.array(z.string().min(1)).optional(),
gateWhen: z.string().min(1).optional(),
gateWhenSay: z.string().min(1).optional(),
options: z
.array(z.object({ value: z.string().min(1), label: z.string().min(1), detail: z.string().optional() }).strict())
.optional(),
+8
View File
@@ -378,6 +378,14 @@ export const executionWorkOrderSchema = z.object({
* 优先级:卡上绑了表(`blocks`)> 这一项 > 必交付物声明。
*/
gateLedgers: z.array(z.string()).optional(),
/**
* **确认闸的条件档**(2026-08-31,W05「类型不一致还能确认」逼出):除了「必填缺几格」,
* 发卡的人还可以声明一条 S-FEEL 条件(只读基座读数),为假就不受理确认 ——
* 服务端在受理回传前现算(与缺格闸同一段 submitGate),display 与拒回同一句话
* (`gateWhenSay`)。机检只读基座数的纪律在这儿同样成立:条件里只许出现读数名。
*/
gateWhen: z.string().optional(),
gateWhenSay: z.string().optional(),
gate: z
.object({
/** 能交了吗 */
+9 -2
View File
@@ -476,8 +476,15 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
* 填回表的尾巴(实测 A2 工作单:量得 2402、实际 2524)。
*/
bindRef.current();
// 取 body 与 documentElement 的较大者:两者谁撑开高度随文档结构而异
const next = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
/*
* **量 body,不量 documentElement**(2026-08-31 用户:「页面底部都没东西了,
* 怎么还可以向下滑动一截」):documentElement.scrollHeight 以 iframe 自身高度为下限 ——
* 内容一旦把 iframe 撑高过一次(比如参数编辑态输入框把卡背拉长),收起后它也缩不回去,
* 量高从此**只涨不缩**,底部留一截空白滚动。body(margin 0、height auto)的 scrollHeight
* 才是内容自己的高;body 读不出(异常文档)再退回 documentElement。
*/
const body = doc.body?.scrollHeight ?? 0;
const next = body > 0 ? body : doc.documentElement.scrollHeight;
if (next > 0) setHeight(next);
}, []);
+3 -3
View File
@@ -747,9 +747,9 @@ const SUITES = {
name: '两颗都在库但分属不同芯片类型 · 硬拦(填别的格子绕不过去,只有改类型才行)· 审核通过',
// gate_reason 要点名「不是同一个芯片类型」——数据卡一出现就查这句话,latch 住
expectGateReason: /芯片类型/,
// 唯一的修复是把其中一颗转挂到另一颗的类型上;缺的那一个新必填参数一并补上,
// 不改类型光填别的格子过不去(S1 的 doneWhen/distinct 与提交闸的必填检查不是同一件事——
// 提交闸不查 distinct,这条路真正的硬拦在 S2 的入场哨兵,详见任务报告)。
// 唯一的修复是把其中一颗转挂到另一颗的类型上;缺的那一个新必填参数一并补上。
// 2026-08-31 起确认闸多了条件档(gateWhen 钉 parts_profile_distinct_chip_type_id):
// 类型不一致时确认直接 400 —— 上面的空手交卷探针吃到的就是它;修好类型再确认才收。
stageEdits: {
'parts.profile': [{ internal_pn: 'LM2596S-5.0', chip_type_id: '2' }],
// 转去 LDO 类型后,LM2596S-5.0 多出一条它从没记过的必填参数「压差」