数据工单只建名单上的料:拼出来的料号与在库的料号写下来都当场拒(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:
xcs
2026-08-27 13:28:25 +08:00
co-authored by Claude Fable 5
parent 51c87af583
commit fa27cef60e
8 changed files with 99 additions and 4 deletions
+36
View File
@@ -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` 的列,
* 值必须在被引用的表里真实存在(正式 + 本单草稿叠加)。起因:经营计划的