Archived
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:
@@ -628,6 +628,12 @@ describe('件① 评估输入补全', () => {
|
|||||||
expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}');
|
expect(html).toContain('.dossier:has(.pback.on) .flip-in{transform:rotateY(180deg)}');
|
||||||
/* 悬浮提示丸:看得出点击可翻 */
|
/* 悬浮提示丸:看得出点击可翻 */
|
||||||
expect(html).toContain('点击翻面');
|
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 用户:「这个界面都不再需要表格来展示数据了」;抽屉连表带功能一并退役)', () => {
|
it('整件零表格(2026-08-31 用户:「这个界面都不再需要表格来展示数据了」;抽屉连表带功能一并退役)', () => {
|
||||||
|
|||||||
@@ -241,6 +241,12 @@ describe('S1 配方', () => {
|
|||||||
expect(card?.kind === 'card' && card.as).toBe('execution');
|
expect(card?.kind === 'card' && card.as).toBe('execution');
|
||||||
expect(card?.kind === 'card' && card.variant).toBe('data');
|
expect(card?.kind === 'card' && card.variant).toBe('data');
|
||||||
expect(card?.kind === 'card' && card.gateLedgers).toEqual(['parts.profile', 'parts.parameter']);
|
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.basis ?? [] : []).length).toBeGreaterThan(0);
|
||||||
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
|
expect((card?.kind === 'card' ? card.guide ?? [] : []).length).toBeGreaterThanOrEqual(3);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -543,6 +543,15 @@ export const ASSESSMENT_STEPS = [
|
|||||||
* 落地无对象、mine: 恒为空 —— 留在名单上反而会被「零行 = 没干完」的闸误拦死。
|
* 落地无对象、mine: 恒为空 —— 留在名单上反而会被「零行 = 没干完」的闸误拦死。
|
||||||
*/
|
*/
|
||||||
gateLedgers: ['parts.profile', 'parts.parameter'],
|
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: '已核对两颗料的在库状态与参数齐备情况。' },
|
{ kind: 'say' as const, text: '已核对两颗料的在库状态与参数齐备情况。' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const manifest: ActivityManifest = {
|
|||||||
activityId: ACTIVITY_ID,
|
activityId: ACTIVITY_ID,
|
||||||
name: 'ACT-WORK-05 · 差异评估',
|
name: 'ACT-WORK-05 · 差异评估',
|
||||||
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
|
/* 重写不是改 —— 同版重装是 no-op(静默丢失,踩过) */
|
||||||
packageVersion: '2.11.0',
|
packageVersion: '2.12.0',
|
||||||
category: '需求分析与方案匹配',
|
category: '需求分析与方案匹配',
|
||||||
sdkVersion: '0.1.0-m5',
|
sdkVersion: '0.1.0-m5',
|
||||||
checksum: 'dev-checksum-part-substitution-assessment',
|
checksum: 'dev-checksum-part-substitution-assessment',
|
||||||
@@ -347,7 +347,7 @@ export const manifest: ActivityManifest = {
|
|||||||
fields: [
|
fields: [
|
||||||
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
|
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
|
||||||
/* 与 `parts.chip_type.type_id` 同一条身份链路,同一个理由改成 text —— 见该字段的大注释 */
|
/* 与 `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: 'brand_name', type: 'text', label: '品牌名称', required: true }),
|
||||||
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
|
col({ name: 'lifecycle_status', type: 'text', label: '生命周期状态', required: true, tally: ['active', 'nrnd', 'eol', 'discontinued', 'risk'] }),
|
||||||
col({ name: 'specification_description', type: 'text', label: '规格描述', expect: true }),
|
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 钉着);
|
* data-edit 祖先」,packages/ui-kit/__tests__/view-container-multihost.test.ts 钉着);
|
||||||
* 开哪面的编辑,卡就停在哪面(主数据编辑锁正面、参数编辑自动翻背面,CSS 兜底)。
|
* 开哪面的编辑,卡就停在哪面(主数据编辑锁正面、参数编辑自动翻背面,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>
|
<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="flip-cue"><i class="cue-front">点击翻面 · 核心参数</i><i class="cue-back">点击翻回 · 主数据</i></span>
|
||||||
<span class="fabs">
|
<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),
|
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%)}
|
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::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}
|
.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{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)}
|
.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){
|
@media(max-width:620px){
|
||||||
.card-head{padding:16px 14px 18px}
|
.card-head{padding:16px 14px 18px}
|
||||||
.dz-grid{grid-template-columns:1fr}
|
.dz-grid{grid-template-columns:1fr}
|
||||||
|
.swapfx{display:none}
|
||||||
.pback{grid-template-columns:1fr}
|
.pback{grid-template-columns:1fr}
|
||||||
.ftiles{grid-template-columns:1fr}
|
.ftiles{grid-template-columns:1fr}
|
||||||
.card-bd{padding:16px 14px 6px}
|
.card-bd{padding:16px 14px 6px}
|
||||||
|
|||||||
@@ -907,6 +907,8 @@ export declare const taskDetailSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
why: z.ZodString;
|
||||||
@@ -1498,6 +1500,8 @@ export declare const taskDetailSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
why: z.ZodString;
|
||||||
@@ -2831,6 +2835,8 @@ export declare const taskDetailRoute: {
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
why: z.ZodString;
|
||||||
@@ -3422,6 +3428,8 @@ export declare const taskDetailRoute: {
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
why: z.ZodString;
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ export type AutoAction = {
|
|||||||
}[] | undefined;
|
}[] | undefined;
|
||||||
/** 这张卡的提交闸盯哪几张表 */
|
/** 这张卡的提交闸盯哪几张表 */
|
||||||
readonly gateLedgers?: readonly string[] | undefined;
|
readonly gateLedgers?: readonly string[] | undefined;
|
||||||
|
/** 确认闸的条件档(S-FEEL,只读基座读数;为假不受理确认)+ 拒回那句话 —— 见 card.ts 同名字段 */
|
||||||
|
readonly gateWhen?: string | undefined;
|
||||||
|
readonly gateWhenSay?: string | undefined;
|
||||||
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
|
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
|
||||||
readonly options?: readonly {
|
readonly options?: readonly {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
|
|||||||
+4
@@ -862,6 +862,8 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
why: z.ZodString;
|
||||||
@@ -1482,6 +1484,8 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
submitLabel: z.ZodOptional<z.ZodString>;
|
submitLabel: z.ZodOptional<z.ZodString>;
|
||||||
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
gateLedgers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||||
|
gateWhen: z.ZodOptional<z.ZodString>;
|
||||||
|
gateWhenSay: z.ZodOptional<z.ZodString>;
|
||||||
gate: z.ZodOptional<z.ZodObject<{
|
gate: z.ZodOptional<z.ZodObject<{
|
||||||
ready: z.ZodBoolean;
|
ready: z.ZodBoolean;
|
||||||
why: z.ZodString;
|
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 });
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ function rowsOf<T>(result: unknown): T[] {
|
|||||||
}
|
}
|
||||||
import { staffOf } from '../http-env.ts';
|
import { staffOf } from '../http-env.ts';
|
||||||
import type { AppEnv } 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';
|
import { orderChat } from '../chat-order.ts';
|
||||||
|
|
||||||
|
|
||||||
@@ -816,7 +817,41 @@ async function submitGate(
|
|||||||
for (const draft of await container.ledger.taskDrafts(taskId)) {
|
for (const draft of await container.ledger.taskDrafts(taskId)) {
|
||||||
(mine[`mine:${draft.domain}.${draft.object}`] ??= []).push({ ...draft.key, ...draft.patch });
|
(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}]`;
|
const where = `${at}.do[${index}]`;
|
||||||
/* 动作级 `when`:语法 + 名字都审(它看得见的只有 scope 与 let,没有行内列) */
|
/* 动作级 `when`:语法 + 名字都审(它看得见的只有 scope 与 let,没有行内列) */
|
||||||
if (action.when !== undefined) checkWhen(action.when, where, letNames);
|
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') {
|
if (action.kind === 'project') {
|
||||||
checkAddress(action.into, where);
|
checkAddress(action.into, where);
|
||||||
walkSource(action.from, where);
|
walkSource(action.from, where);
|
||||||
|
|||||||
@@ -183,6 +183,10 @@
|
|||||||
(2026-08-30 e2e 案例 2 的死锁,当时以「W05 ②③ 两笔不带 prune」绕开,代价是**没落地的**
|
(2026-08-30 e2e 案例 2 的死锁,当时以「W05 ②③ 两笔不带 prune」绕开,代价是**没落地的**
|
||||||
陈旧行也不清 —— 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」)。
|
陈旧行也不清 —— 2026-08-31 用户实测点名的「改回同一类型,下面还是两个类型」)。
|
||||||
模型那一路不传:那句「已入库撤不掉」的拒回是它的行动指南,一个字不变。
|
模型那一路不传:那句「已入库撤不掉」的拒回是它的行动指南,一个字不变。
|
||||||
|
- 配方 `card` 多两个可选键 `gateWhen` / `gateWhenSay`(2026-08-31):确认闸的**条件档** ——
|
||||||
|
除了「必填缺几格」,发卡的人还可声明一条 S-FEEL 条件(只读基座读数,装包静态审同
|
||||||
|
`when` 一套审),受理回传前在 app 层 `submitGate` 现算,为假不受理、拒回用发卡人那句话
|
||||||
|
(W05 第一例:两颗料不是同一个芯片类型不许确认)。运行时评估不出的条件不成墙。
|
||||||
|
|
||||||
同批三条不变量:
|
同批三条不变量:
|
||||||
- **配方由 `liveStep` 驱动,不由 `current` 的兜底驱动**:兜底(在场的第一个有剧本的项)不过滤
|
- **配方由 `liveStep` 驱动,不由 `current` 的兜底驱动**:兜底(在场的第一个有剧本的项)不过滤
|
||||||
|
|||||||
@@ -398,6 +398,8 @@ describe('card 与 submit', () => {
|
|||||||
standard: '必填格全部填上,红带消失',
|
standard: '必填格全部填上,红带消失',
|
||||||
basis: [{ k: '候选料号', v: { in: 'pn' } }],
|
basis: [{ k: '候选料号', v: { in: 'pn' } }],
|
||||||
gateLedgers: ['parts.profile', 'parts.parameter'],
|
gateLedgers: ['parts.profile', 'parts.parameter'],
|
||||||
|
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
|
||||||
|
gateWhenSay: '两颗料不是同一个芯片类型 —— 先改对再确认。',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -415,6 +417,9 @@ describe('card 与 submit', () => {
|
|||||||
standard: '必填格全部填上,红带消失',
|
standard: '必填格全部填上,红带消失',
|
||||||
basis: [{ k: '候选料号', v: 'XL2596S-5.0' }],
|
basis: [{ k: '候选料号', v: 'XL2596S-5.0' }],
|
||||||
gateLedgers: ['parts.profile', 'parts.parameter'],
|
gateLedgers: ['parts.profile', 'parts.parameter'],
|
||||||
|
/* 确认闸条件档随卡走(2026-08-31)—— 服务端 submitGate 靠它拦「类型不一致还确认」 */
|
||||||
|
gateWhen: 'parts_profile_distinct_chip_type_id <= 1',
|
||||||
|
gateWhenSay: '两颗料不是同一个芯片类型 —— 先改对再确认。',
|
||||||
fields: [],
|
fields: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -371,6 +371,9 @@ export function createAutoRunner(deps: AutoRunnerDeps): {
|
|||||||
...(action.guide === undefined ? {} : { steps: [...action.guide] }),
|
...(action.guide === undefined ? {} : { steps: [...action.guide] }),
|
||||||
...(action.standard === undefined ? {} : { standard: evalText(action.standard, base) }),
|
...(action.standard === undefined ? {} : { standard: evalText(action.standard, base) }),
|
||||||
...(action.gateLedgers === undefined ? {} : { gateLedgers: [...action.gateLedgers] }),
|
...(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 };
|
return { wrote: 0, dropped: 0, cardId: id, stop: true };
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ describe('auto 配方契约', () => {
|
|||||||
expect(junk.success).toBe(false);
|
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 上', () => {
|
it('prune 只许挂在 project 上', () => {
|
||||||
const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'say', text: 'x', prune: true }] });
|
const parsed = autoRecipeSchema.safeParse({ do: [{ kind: 'say', text: 'x', prune: true }] });
|
||||||
expect(parsed.success).toBe(false);
|
expect(parsed.success).toBe(false);
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ export type AutoAction =
|
|||||||
readonly basis?: readonly { readonly k: string; readonly v: AutoExpr }[] | undefined;
|
readonly basis?: readonly { readonly k: string; readonly v: AutoExpr }[] | undefined;
|
||||||
/** 这张卡的提交闸盯哪几张表 */
|
/** 这张卡的提交闸盯哪几张表 */
|
||||||
readonly gateLedgers?: readonly string[] | undefined;
|
readonly gateLedgers?: readonly string[] | undefined;
|
||||||
|
/** 确认闸的条件档(S-FEEL,只读基座读数;为假不受理确认)+ 拒回那句话 —— 见 card.ts 同名字段 */
|
||||||
|
readonly gateWhen?: string | undefined;
|
||||||
|
readonly gateWhenSay?: string | undefined;
|
||||||
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
|
/** 决策卡的选项;`variant:'review'` 时允许只有一个 */
|
||||||
readonly options?:
|
readonly options?:
|
||||||
| readonly { readonly value: string; readonly label: string; readonly detail?: string | undefined }[]
|
| 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(),
|
standard: autoExprSchema.optional(),
|
||||||
basis: z.array(z.object({ k: z.string().min(1), v: autoExprSchema }).strict()).optional(),
|
basis: z.array(z.object({ k: z.string().min(1), v: autoExprSchema }).strict()).optional(),
|
||||||
gateLedgers: z.array(z.string().min(1)).optional(),
|
gateLedgers: z.array(z.string().min(1)).optional(),
|
||||||
|
gateWhen: z.string().min(1).optional(),
|
||||||
|
gateWhenSay: z.string().min(1).optional(),
|
||||||
options: z
|
options: z
|
||||||
.array(z.object({ value: z.string().min(1), label: z.string().min(1), detail: z.string().optional() }).strict())
|
.array(z.object({ value: z.string().min(1), label: z.string().min(1), detail: z.string().optional() }).strict())
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@@ -378,6 +378,14 @@ export const executionWorkOrderSchema = z.object({
|
|||||||
* 优先级:卡上绑了表(`blocks`)> 这一项 > 必交付物声明。
|
* 优先级:卡上绑了表(`blocks`)> 这一项 > 必交付物声明。
|
||||||
*/
|
*/
|
||||||
gateLedgers: z.array(z.string()).optional(),
|
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
|
gate: z
|
||||||
.object({
|
.object({
|
||||||
/** 能交了吗 */
|
/** 能交了吗 */
|
||||||
|
|||||||
@@ -476,8 +476,15 @@ export function ViewContainer({ html, title = '活动视图', editable = false,
|
|||||||
* 填回表的尾巴(实测 A2 工作单:量得 2402、实际 2524)。
|
* 填回表的尾巴(实测 A2 工作单:量得 2402、实际 2524)。
|
||||||
*/
|
*/
|
||||||
bindRef.current();
|
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);
|
if (next > 0) setHeight(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -747,9 +747,9 @@ const SUITES = {
|
|||||||
name: '两颗都在库但分属不同芯片类型 · 硬拦(填别的格子绕不过去,只有改类型才行)· 审核通过',
|
name: '两颗都在库但分属不同芯片类型 · 硬拦(填别的格子绕不过去,只有改类型才行)· 审核通过',
|
||||||
// gate_reason 要点名「不是同一个芯片类型」——数据卡一出现就查这句话,latch 住
|
// gate_reason 要点名「不是同一个芯片类型」——数据卡一出现就查这句话,latch 住
|
||||||
expectGateReason: /芯片类型/,
|
expectGateReason: /芯片类型/,
|
||||||
// 唯一的修复是把其中一颗转挂到另一颗的类型上;缺的那一个新必填参数一并补上,
|
// 唯一的修复是把其中一颗转挂到另一颗的类型上;缺的那一个新必填参数一并补上。
|
||||||
// 不改类型光填别的格子过不去(S1 的 doneWhen/distinct 与提交闸的必填检查不是同一件事——
|
// 2026-08-31 起确认闸多了条件档(gateWhen 钉 parts_profile_distinct_chip_type_id):
|
||||||
// 提交闸不查 distinct,这条路真正的硬拦在 S2 的入场哨兵,详见任务报告)。
|
// 类型不一致时确认直接 400 —— 上面的空手交卷探针吃到的就是它;修好类型再确认才收。
|
||||||
stageEdits: {
|
stageEdits: {
|
||||||
'parts.profile': [{ internal_pn: 'LM2596S-5.0', chip_type_id: '2' }],
|
'parts.profile': [{ internal_pn: 'LM2596S-5.0', chip_type_id: '2' }],
|
||||||
// 转去 LDO 类型后,LM2596S-5.0 多出一条它从没记过的必填参数「压差」
|
// 转去 LDO 类型后,LM2596S-5.0 多出一条它从没记过的必填参数「压差」
|
||||||
|
|||||||
Reference in New Issue
Block a user