人手改的那一手真正落得下去;界面不撒谎那几处补齐

手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
2026-08-02 13:41:44 +08:00
co-authored by Claude Opus 5
parent 94f1c7a4ef
commit 259192f736
228 changed files with 27850 additions and 2847 deletions
+111
View File
@@ -64,13 +64,124 @@ export const fieldDeclarationSchema = z.object({
primaryKey: z.boolean().default(false),
/** 治理:脱敏 / 保留期 / 可学习标记(OS 8.1 治理先行) */
masked: z.boolean().default(false),
/**
* **这一列缺了就是活没干完** —— 于是基座数得出「本单这张表缺必填的有几行」。
*
* 不建 NOT NULL:草稿层的本分就是「过程中允许缺」,缺口要摆给人看、由人补,
* 而不是让写入当场炸掉。这一维只служ两件事:
* · 读数 `域_对象_missing` / `_missing_keys` —— 基座数出来的数,判据可以放心读;
* · 报告模版按它标红胶囊(A1 那张清单的「必填 / 选填」两色就是它)。
*
* 为什么要有它(2026-08-01,连跑十轮里三轮死在这):A1 的
* 「必填齐(PN / 年用量)」是 L1 机检,读的却是**活动自己写的** `missing_key_total` ——
* 人在卡上补了年用量、基座写回了草稿层,而那个汇总列要模型自己重算;
* 它算漏一次,这条判据就永远绿不了,连拒五拍熔断。
* **机检判据只许读基座数得出来的数** —— 这一维就是让基座数得出来的那把尺。
*
* 用 `optional` 不用 `default(false)`:后者会把它变成出参必填,
* 六个活动两百来行字段声明都得补一个 `required: false` —— 那是给纪律收噪音税。
* 不写就是不必填,读的那一侧一律 `=== true` 才当真。
*/
required: z.boolean().optional(),
/**
* **这一列是分类列** —— 给出它的取值域,基座就按值现数本单每一类几行。
*
* 出的读数是 `域_对象_<列>_<值>`(值消毒成小写标识符),例如
* `quote_intake_record_type_tag_a`。**没出现的值也铺 0** —— 取值域是声明给的,
* 所以数得全;零条也是一个数,判据才算得下去。
*
* 为什么非要基座来数(2026-08-01 用户当场指出):三分类的条数从前是活动自己写的
* `type_a_total / type_b_total / type_c_total`,而分布条、放行时的去向谓词、验收判据
* 读的全是它们。于是**人在放行页把九条料全改成 A,类徽变了,分布条还是 5/3/1、
* 去向还是「去选型」** —— 逐行数据变了,汇总数没人重算。
* 而**手工修改的全部意义就是纠正 AI 的判断**:改了类标而流向不跟着变,
* 这个兜底功能就是假的。能由数据算出来的,一律别让活动自报。
*/
tally: z.array(z.string().min(1)).optional(),
/**
* **这一列缺了不拦单,但那一问不能省** —— 基座据此数「还有几行缺选填」。
*
* 与 `required` 的分工:必填缺一条都不许放行(它是验收标准);
* 选填缺照样往下走,只是**必须经过人**——问过客户、或放行时确认可以先受理。
*
* 为什么也得基座数(2026-08-02 用户实测:「我这次补了目标价,为啥 L4 还在那乱判」):
* 那条 L4 裁的依据是活动自己写的 `missing_minor_total`。人在件上补了目标价、
* 基座已经写回草稿层,而那个数要模型自己重算 —— 它没算,于是照旧裁「不满足」,
* 人明明补齐了却被告知还缺。**裁量项可以由人来判,但它判的事实必须是真的。**
*/
expect: z.boolean().optional(),
/**
* **这一列由基座生成,既不问人也不让模型编**(2026-08-02 用户:「联系人ID自增啊,不用填」)。
*
* 用在**内部流水号**那一类主键上:联系人编码、行号 —— 它们不承载业务含义,
* 只是「同一位客户下的第几位」。让人填是白占一格他答不上来的表单
* (他哪知道这家客户已经有几位联系人);让模型编更糟,编重了就是覆盖别人那一行。
*
* 生成规则是**声明出来的一条**,不是每处各写一套:
* 取同组其余主键的值作前缀,后面跟两位序号 —— `CU-9001` + `-01`、`-02`……
* 序号数的是**同组已有的行**(正式版 + 本单草稿),所以同一单里加两位联系人也不撞。
* 没有其余主键(它自己就是唯一那把键)时退回纯序号 `01`。
*
* 落在哪一层:人按下确认那一刻由基座补上 —— 与「该由人亲手落下的别经 AI 转述」
* 同一条规矩的另一半:**人答不上来的,也别摆到他面前假装他该答**。
*/
auto: z.boolean().optional(),
});
export type FieldDeclaration = z.infer<typeof fieldDeclarationSchema>;
/**
* 台账对象的**性质** —— 这条数据归谁管、活多久、谁有权改它。
*
* 从前台账只有「地址 + 列」,没有这一维,于是**没人需要回答「这张表是什么」**——
* 后果不是不好看,是主键写歪了还没人发现:A4 的报价草案本该一单一份,
* 主键却只有 `pn`,第二单报同一个料号直接 upsert 覆盖上一单
* (实测:跑了四轮 A4,`quote_pricing.draft` 只剩最后一轮的 3 行;
* 而主键带了单号的 `quote_intake.record` 是 61 行 / 20 单,一单不少)。
*
* 分三类,判据是**机械的**,不靠感觉:
*
* · `record` 单据 —— 一单一份的过程与结论。主键**必含单标识**(`caseKey`),
* 放行即定稿、只增不改。它不需要「管理」,需要的是查得到、回放得出。
* · `master` 主数据 —— 跨单积累的经营资产(客户、供应商、料号规格、历史价)。
* 主键是**业务实体**,会被反复更新,下一单读它做判断。治理都在这一类上:
* 谁能改、依据是什么、怎么去重、多久算过期。
* · `derived` 派生 —— 能由前两类算出来的指标(落地率、命中率)。
* **本不该手写**:手写的派生值能和它的来源打架,而没人会发现。
* 标出来至少让它可被重算校验。
*/
export const LEDGER_OBJECT_KINDS = ['record', 'master', 'derived'] as const;
export const ledgerObjectKindSchema = z.enum(LEDGER_OBJECT_KINDS);
export type LedgerObjectKind = z.infer<typeof ledgerObjectKindSchema>;
export const LEDGER_KIND_LABEL: Readonly<Record<LedgerObjectKind, string>> = {
record: '单据',
master: '主数据',
derived: '派生',
};
export const LEDGER_KIND_DESC: Readonly<Record<LedgerObjectKind, string>> = {
record: '一单一份的过程与结论 —— 放行即定稿,只增不改;查得到、回放得出就够了',
master: '跨单积累的经营资产 —— 会被反复更新,下一单读它做判断;治理都在这一类上',
derived: '能由单据与主数据算出来的指标 —— 不该手写,手写的派生值会和来源打架',
};
export const objectDeclarationSchema = z.object({
domain: identifier,
object: identifier,
label: z.string().optional(),
/**
* 这张表是什么性质 —— 缺省 `record`,因为活动写下的绝大多数是单据。
* 缺省不是「随便」:安装静态审会按 kind 各自核一遍(见 `ledgerKindFindings`)。
*/
kind: ledgerObjectKindSchema.default('record'),
/**
* 单标识字段名 —— **只有 `record` 需要**,且必须是它主键的一部分。
*
* 为什么要显式声明而不去猜:猜的规则(比如「主键里得有个 `_ref` 结尾的列」)
* 迟早猜错,而错的那一次是**静默覆盖数据**。让活动自己说清哪一列是「单」,
* 安装期就能核:它在不在 fields 里、是不是主键的一部分。
*/
caseKey: identifier.optional(),
fields: z.array(fieldDeclarationSchema).min(1),
});
export type ObjectDeclaration = z.infer<typeof objectDeclarationSchema>;