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:
@@ -132,6 +132,12 @@ describe('台账声明:授权面与《统一方案》口径', () => {
|
|||||||
expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' });
|
expect(byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'abandon_reason')?.requiredWhen).toEqual({ column: 'exists_status', value: 'abandoned' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('三张主数据表标 createOnly:数据工单只建库里没有的料,在库的写下来当场拒(2026-08-27 实测:模型在数据工单里多铺了一行不该建的料号,件上多出一行要人填)', () => {
|
||||||
|
for (const obj of ['profile', 'parameter', 'document'] as const) expect(byAddress('parts', obj)?.createOnly, obj).toBe(true);
|
||||||
|
// 凭空拼出来的料号(实测 XL2596S-ADJ)由这一道管:只许建核对表里核过的
|
||||||
|
expect(byAddress('parts', 'profile')?.fields.find((f) => f.name === 'internal_pn')?.refersTo).toBe(`${D}.pn_check.internal_pn`);
|
||||||
|
});
|
||||||
|
|
||||||
it('判「在库」必须填得出引用依据,基座拿它去主数据里查(2026-08-27 实测:原 PN 不在库而模型把两颗都写成 found,硬阻塞那条路整个被跳过)', () => {
|
it('判「在库」必须填得出引用依据,基座拿它去主数据里查(2026-08-27 实测:原 PN 不在库而模型把两颗都写成 found,硬阻塞那条路整个被跳过)', () => {
|
||||||
const ref = byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'found_pn_ref');
|
const ref = byAddress(D, 'pn_check')?.fields.find((f) => f.name === 'found_pn_ref');
|
||||||
expect(ref?.refersTo).toBe('parts.profile.internal_pn');
|
expect(ref?.refersTo).toBe('parts.profile.internal_pn');
|
||||||
|
|||||||
@@ -362,7 +362,9 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
|
|||||||
【名单】= \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选 —— **一律建,不问人**
|
【名单】= \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选 —— **一律建,不问人**
|
||||||
(2026-08-27 用户定:只要有缺失就自动派数据工单;从前那张「派数据工单还是放弃这颗候选」的决策卡已退役,别再发)。
|
(2026-08-27 用户定:只要有缺失就自动派数据工单;从前那张「派数据工单还是放弃这颗候选」的决策卡已退役,别再发)。
|
||||||
**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍,
|
**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍,
|
||||||
而真正缺的那颗还是没人建、单子照样卡死。名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。
|
而真正缺的那颗还是没人建、单子照样卡死。**这三张表在本单只建不改、且只建核对表里核过的料号**:在库的料号写下来(哪怕只是一行空骨架)会被当场拒,
|
||||||
|
凭空拼出来的料号(2026-08-27 实测:模型写了一颗不存在的 XL2596S-ADJ,把两个候选的名字拼在了一起)也会被当场拒;
|
||||||
|
真写了就用 drop 撤掉那一行,别留在件上让人去填。**名单只有一个来源:pn_check 里 absent 的那几行,一个字都别改。**名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。
|
||||||
**名单里的料号全放进同一张 P-08 卡**(标题列全料号,件上一颗一行,参数各归各的料号)—— 别一颗一张。
|
**名单里的料号全放进同一张 P-08 卡**(标题列全料号,件上一颗一行,参数各归各的料号)—— 别一颗一张。
|
||||||
|
|
||||||
【「建」的名单定了 → 写骨架行,发 P-08 数据工单(一张单,件上两张表)】
|
【「建」的名单定了 → 写骨架行,发 P-08 数据工单(一张单,件上两张表)】
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const manifest: ActivityManifest = {
|
|||||||
identity: {
|
identity: {
|
||||||
activityId: ACTIVITY_ID,
|
activityId: ACTIVITY_ID,
|
||||||
name: 'ACT-WORK-05 · 差异评估',
|
name: 'ACT-WORK-05 · 差异评估',
|
||||||
packageVersion: '1.4.0',
|
packageVersion: '1.4.1',
|
||||||
category: '需求分析与方案匹配',
|
category: '需求分析与方案匹配',
|
||||||
sdkVersion: '0.1.0-m5',
|
sdkVersion: '0.1.0-m5',
|
||||||
checksum: 'dev-checksum-part-substitution-assessment',
|
checksum: 'dev-checksum-part-substitution-assessment',
|
||||||
@@ -285,10 +285,17 @@ export const manifest: ActivityManifest = {
|
|||||||
{
|
{
|
||||||
domain: 'parts',
|
domain: 'parts',
|
||||||
object: 'profile',
|
object: 'profile',
|
||||||
|
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
|
||||||
|
createOnly: true,
|
||||||
kind: 'master',
|
kind: 'master',
|
||||||
label: '芯片主数据(P-08)',
|
label: '芯片主数据(P-08)',
|
||||||
fields: [
|
fields: [
|
||||||
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true }),
|
/*
|
||||||
|
* **只许建本单核过的料号**(2026-08-27 实测):模型给一颗**不存在的** XL2596S-ADJ 铺了空骨架 ——
|
||||||
|
* 库里没有、入参里也没有,是把两个候选的名字拼出来的;件上于是多出一行要人填的记录。
|
||||||
|
* 引用核对表:料号必须是这一单(或别单)在 pn_check 里核过的,凭空拼出来的当场拒。
|
||||||
|
*/
|
||||||
|
col({ name: 'internal_pn', type: 'text', label: '内部料号', primaryKey: true, refersTo: `${D}.pn_check.internal_pn` }),
|
||||||
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
|
col({ name: 'brand_name', type: 'text', label: '品牌名称', required: true }),
|
||||||
col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }),
|
col({ name: 'manufacturer_name', type: 'text', label: '制造商名称', expect: true }),
|
||||||
col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }),
|
col({ name: 'specification_description', type: 'text', label: '规格描述', required: true }),
|
||||||
@@ -305,6 +312,8 @@ export const manifest: ActivityManifest = {
|
|||||||
{
|
{
|
||||||
domain: 'parts',
|
domain: 'parts',
|
||||||
object: 'parameter',
|
object: 'parameter',
|
||||||
|
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
|
||||||
|
createOnly: true,
|
||||||
kind: 'master',
|
kind: 'master',
|
||||||
label: '标准化参数集(P-08 子表)',
|
label: '标准化参数集(P-08 子表)',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -345,6 +354,8 @@ export const manifest: ActivityManifest = {
|
|||||||
{
|
{
|
||||||
domain: 'parts',
|
domain: 'parts',
|
||||||
object: 'document',
|
object: 'document',
|
||||||
|
/* 数据工单从零建主数据:本单只建库里没有的,在库的料号写下来当场拒(2026-08-27) */
|
||||||
|
createOnly: true,
|
||||||
kind: 'master',
|
kind: 'master',
|
||||||
label: '原厂技术资料库(P-10)',
|
label: '原厂技术资料库(P-10)',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export declare const objectDeclarationSchema: z.ZodObject<{
|
|||||||
derived: "derived";
|
derived: "derived";
|
||||||
}>>;
|
}>>;
|
||||||
caseKey: z.ZodOptional<z.ZodString>;
|
caseKey: z.ZodOptional<z.ZodString>;
|
||||||
|
createOnly: z.ZodOptional<z.ZodBoolean>;
|
||||||
file: z.ZodOptional<z.ZodObject<{
|
file: z.ZodOptional<z.ZodObject<{
|
||||||
titleColumn: z.ZodString;
|
titleColumn: z.ZodString;
|
||||||
bodyColumn: z.ZodString;
|
bodyColumn: z.ZodString;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { emptyGate, findKnownRow, gateAddressesOf, gateOf, parseRowKey, resolveRowKey, rowChanged, writesOf } from '../routes/desk.ts';
|
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 与年用量必填 */
|
/** A1 受理记录的声明(裁到这一段用得着的几列):PN 与年用量必填 */
|
||||||
const RECORD = {
|
const RECORD = {
|
||||||
@@ -550,3 +550,27 @@ describe('落不下的行要点名 —— 不许悄悄丢', () => {
|
|||||||
).toEqual({ customer_code: 'CU-9527', round_ref: 'COV-1' });
|
).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())}`;
|
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(
|
export function rowReadings(
|
||||||
mine: Readonly<Record<string, readonly unknown[]>>,
|
mine: Readonly<Record<string, readonly unknown[]>>,
|
||||||
declared: ReadonlyArray<{ domain: string; object: string }> = [],
|
declared: ReadonlyArray<{ domain: string; object: string }> = [],
|
||||||
@@ -937,6 +955,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
|
|||||||
label: d.label ?? `${d.domain}.${d.object}`,
|
label: d.label ?? `${d.domain}.${d.object}`,
|
||||||
keys: d.fields.filter((f) => f.primaryKey).map((f) => f.name),
|
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 })),
|
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('、')})`);
|
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` 的列,
|
* **引用列查引用**(2026-08-24,措辞治不了的结构治):声明了 `refersTo` 的列,
|
||||||
* 值必须在被引用的表里真实存在(正式 + 本单草稿叠加)。起因:经营计划的
|
* 值必须在被引用的表里真实存在(正式 + 本单草稿叠加)。起因:经营计划的
|
||||||
|
|||||||
@@ -52,3 +52,7 @@
|
|||||||
此前 label 在建表那一步就丢了,治理台只剩 `project__intake_record` 这种键给人看。
|
此前 label 在建表那一步就丢了,治理台只剩 `project__intake_record` 这种键给人看。
|
||||||
用注释而非另建元数据表:名字与表同生共死,不会对不上。
|
用注释而非另建元数据表:名字与表同生共死,不会对不上。
|
||||||
`comment on` 是工具语句不吃占位符,字面量在 adapter 内转义单引号。
|
`comment on` 是工具语句不吃占位符,字面量在 adapter 内转义单引号。
|
||||||
|
|
||||||
|
近变(2026-08-27):**对象声明加 `createOnly`** —— 标了的表在本单**只建不改**:Agent 写入那一刻查正式表,
|
||||||
|
键已在库当场拒并指路(撤掉草稿里那一行)。数据工单「从零建主数据」的三张表用它。只管 Agent 的写路径
|
||||||
|
(`engines.draftWrite` 那道门),人在件上的保存不受此限。
|
||||||
|
|||||||
@@ -218,6 +218,17 @@ export const objectDeclarationSchema = z.object({
|
|||||||
* 安装期就能核:它在不在 fields 里、是不是主键的一部分。
|
* 安装期就能核:它在不在 fields 里、是不是主键的一部分。
|
||||||
*/
|
*/
|
||||||
caseKey: identifier.optional(),
|
caseKey: identifier.optional(),
|
||||||
|
/**
|
||||||
|
* **本单只许新建正式表里没有的行**(2026-08-27,W05 数据工单逼出来的)。
|
||||||
|
*
|
||||||
|
* 数据工单让人「从零建一张主数据」—— 这张表在本单里的语义就是**只建不改**。
|
||||||
|
* 实测(W05):模型在数据工单里多铺了一行不该建的料号 —— 件上于是多出一行要人填的记录,
|
||||||
|
* 红带点名它缺品牌;剧本写了「found 的一行都不建」拦不住。这一项守的是其中一种形:
|
||||||
|
* **在库的料号被当成新料重建**。标了它的表,写入那一刻查正式表:键已经在库 → 当场拒并指路
|
||||||
|
* (墙要配路:撤掉那一行)。凭空拼出来的料号由 `refersTo` 核对表那道门管。
|
||||||
|
* 只管 Agent 的写(人在件上的保存走另一条路,不受此限)。
|
||||||
|
*/
|
||||||
|
createOnly: z.boolean().optional(),
|
||||||
/**
|
/**
|
||||||
* **这张表的每一行还是一份资料文件** —— 哪几列合起来是那份文件(2026-08-23 加)。
|
* **这张表的每一行还是一份资料文件** —— 哪几列合起来是那份文件(2026-08-23 加)。
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user