Archived
数据工单只建名单上的料:拼出来的料号与在库的料号写下来都当场拒(W05 1.4.1)
用户在新一单上看到件上多出一行要人补的料号。查实:模型在数据工单里给一颗**不存在的** XL2596S-ADJ 铺了空骨架 —— 库里没有、入参里也没有,是把两个候选的名字拼出来的; 件上于是多出一行、红带点名它缺品牌。存在性核对本身判对了(ADJ found、XL absent), 错在「判对了却多写了一行」—— 事实在模型手里,能判对也能写错。 两道门,都在写入那一刻: - `parts.profile.internal_pn refersTo sub_eval.pn_check.internal_pn` —— 只许建核对表里核过的料号, 凭空拼出来的当场拒(参数/资料两张子表的 internal_pn 本就引用 profile,随之一并拦住)。 - 契约新增对象声明 `createOnly`(平台侧,~25 行):标了的表本单只建不改,键已在正式表当场拒并指路。 它守的是另一形(在库的料被当成新料重建)。只管 Agent 的写路径,人在件上的保存不受此限。 纯函数 `createOnlyViolation` 可测(复合主键按全部键列比);活动侧三张 parts 表标上;剧本补一句。 当前在跑那一单的草稿里拼出来的那行已手工撤掉。2100 测 / typecheck / lint / depcruise 绿;api-report 已刷。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { emptyGate, findKnownRow, gateAddressesOf, gateOf, parseRowKey, resolveRowKey, rowChanged, writesOf } from '../routes/desk.ts';
|
||||
import { missingReadings } from '../wiring/engines.ts';
|
||||
import { createOnlyViolation, missingReadings } from '../wiring/engines.ts';
|
||||
|
||||
/** A1 受理记录的声明(裁到这一段用得着的几列):PN 与年用量必填 */
|
||||
const RECORD = {
|
||||
@@ -550,3 +550,27 @@ describe('落不下的行要点名 —— 不许悄悄丢', () => {
|
||||
).toEqual({ customer_code: 'CU-9527', round_ref: 'COV-1' });
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 只建不改的表(契约 `createOnly`)—— 2026-08-27 W05:数据工单里模型多铺了一行不该建的料号,
|
||||
* 件上多出一行要人填的记录、红带点名它缺品牌。这一项守「在库的料被重建」这一形。
|
||||
*/
|
||||
describe('createOnly:键已在正式表 → 拒并指路', () => {
|
||||
const target = { label: '芯片主数据(P-08)', keys: ['internal_pn'] };
|
||||
const formal = [{ internal_pn: 'LM2596S-ADJ', brand_name: 'TI' }, { internal_pn: 'LM2596S-5.0' }];
|
||||
|
||||
it('在库的料号写下来 → 一句带指路的拒回(点名 drop)', () => {
|
||||
const why = createOnlyViolation(target, { internal_pn: 'LM2596S-ADJ' }, formal);
|
||||
expect(why).toContain('已经有「internal_pn=LM2596S-ADJ」');
|
||||
expect(why).toContain('drop');
|
||||
});
|
||||
|
||||
it('库里没有的料号 → 放行;复合主键按全部键列比', () => {
|
||||
expect(createOnlyViolation(target, { internal_pn: 'XL2596S-5.0' }, formal)).toBeUndefined();
|
||||
const sub = { label: '标准化参数集', keys: ['internal_pn', 'param_name'] };
|
||||
const rows = [{ internal_pn: 'LM2596S-ADJ', param_name: '输出电压' }];
|
||||
expect(createOnlyViolation(sub, { internal_pn: 'LM2596S-ADJ', param_name: '输出电压' }, rows)).toBeDefined();
|
||||
expect(createOnlyViolation(sub, { internal_pn: 'LM2596S-ADJ', param_name: '使能阈值' }, rows)).toBeUndefined();
|
||||
expect(createOnlyViolation(sub, { internal_pn: 'XL2596S-5.0', param_name: '输出电压' }, rows)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -190,6 +190,24 @@ export function localStamp(iso: string): string {
|
||||
return `${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「只建不改」的表:这一笔写的键是不是已经在正式表里了 —— 在就回一句拒回的话(带指路),不在回 undefined。
|
||||
* 纯函数:正式表的行由调用方查好递进来,这里只比键。
|
||||
*/
|
||||
export function createOnlyViolation(
|
||||
target: { readonly label: string; readonly keys: ReadonlyArray<string> },
|
||||
key: Readonly<Record<string, string | number>>,
|
||||
formalRows: ReadonlyArray<Record<string, unknown>>,
|
||||
): string | undefined {
|
||||
const same = (row: Record<string, unknown>) => target.keys.every((k) => String(row[k] ?? '') === String(key[k] ?? ''));
|
||||
if (!formalRows.some(same)) return undefined;
|
||||
const shown = target.keys.map((k) => `${k}=${String(key[k] ?? '')}`).join('、');
|
||||
return (
|
||||
`${target.label}里已经有「${shown}」这一行 —— 这张表在本单只许**新建库里没有的记录**(数据工单是从零建主数据用的),` +
|
||||
'在库的料号不用建、也不许改:别再写它;要是本单草稿里已经替它铺了骨架,用 drop 把那一行撤掉,免得件上多出一行要人填的东西。'
|
||||
);
|
||||
}
|
||||
|
||||
export function rowReadings(
|
||||
mine: Readonly<Record<string, readonly unknown[]>>,
|
||||
declared: ReadonlyArray<{ domain: string; object: string }> = [],
|
||||
@@ -937,6 +955,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
label: d.label ?? `${d.domain}.${d.object}`,
|
||||
keys: d.fields.filter((f) => f.primaryKey).map((f) => f.name),
|
||||
fields: d.fields.map((f) => ({ name: f.name, label: f.label ?? f.name, type: f.type, refersTo: f.refersTo })),
|
||||
createOnly: d.createOnly === true,
|
||||
}));
|
||||
},
|
||||
/**
|
||||
@@ -975,6 +994,23 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
||||
throw new Error(`台账无此列:${strayed.join('、')}(${target.label} 可写列:${[...known].join('、')})`);
|
||||
}
|
||||
|
||||
/*
|
||||
* **只建不改的表,键已在库就拒**(契约 `createOnly`,2026-08-27 W05 实测逼出):
|
||||
* 数据工单让人从零建主数据,模型却可能给在库的料号也写一行空骨架 —— 件上多出一行
|
||||
* 要人填的记录,红带点名它缺品牌,而正式表里它什么都不缺。剧本写了拦不住,写入那一刻查。
|
||||
*/
|
||||
if (target.createOnly && target.keys.length > 0) {
|
||||
const formal = await deps.ledger.query({
|
||||
address: { domain: input.domain, object: input.object },
|
||||
where: {},
|
||||
orderBy: [],
|
||||
limit: 500,
|
||||
offset: 0,
|
||||
});
|
||||
const why = createOnlyViolation(target, input.key, formal.rows as ReadonlyArray<Record<string, unknown>>);
|
||||
if (why !== undefined) throw new Error(why);
|
||||
}
|
||||
|
||||
/*
|
||||
* **引用列查引用**(2026-08-24,措辞治不了的结构治):声明了 `refersTo` 的列,
|
||||
* 值必须在被引用的表里真实存在(正式 + 本单草稿叠加)。起因:经营计划的
|
||||
|
||||
Reference in New Issue
Block a user