Archived
fix(w05): 终审两条 Critical + 地基缺陷的另一半 + 文档与欠账收口
**C1 新建芯片类型会改写既有字典行(共享主数据被静默污染)**
链条逐行复核成立:件上种子行漏挂 data-col → barred 与 isSeq 双保护同时失效
(isSeq 那条是因为 Task 7b 把 type_id 从 integer 改成了 text)→ 落进「全表一致」,
而 H35 的自投影恰好保证这张表只有一行 → 人新建的类型被认成既有 type_id=1 →
确认时随范围放行把既有字典行改名并落进正式表,屏上只说「改了 1 行」。
三条各自成立的裁定叠出来的静默污染。
修法:种子行改成 disabled 的真实 <input data-col=type_id>(desk.ts 零改动),
格子里摆「由系统生成」——不能一声不吭地拿掉,那样人会以为这张档少了一格。
测试不止断言结果:另加两条用**真实 resolveRowKey** 重放病灶/修法两种 cells 形状,
直接证明因果机制。平台根治(全表一致应排除 auto 列)记 debt。
**C2 两个启动参数都选填 → 留空则整单永久无解**
gate_reason 说「在下面把两格补上」,而块① 没有加一行、也不在 gateLedgers ——
一句指向空气的指路。改为必填;第一档改口为唯一真无解的那条路径(两格填了同一个号)。
**I1 地基缺陷只修了一半:recipe.let 同样按对象键序求值**
与 fields 是一模一样的假设,同样住 jsonb 列、同样被 PG 重排;今天不炸只因 W05 那五条
互不引用,而 ADR-014 是给后续所有活动用的。新增 orderLets/letReferences
(把 {in:X} 视为依赖 —— 语义与 fields 不同,故单独实现),auto-runner 与静态审同步接入。
**I2 审核卡按「重做」照样收步推进** —— 坐实为真实平台缺陷(「卡答过了就收步」快道不看裁决),
并证明活动侧加选项救不了、chosenGuidance 那条也救不了(那是喂模型的提示词,
而这条快道存在的目的就是跳过模型)。按裁定只留复现测试,平台未改。
**I4** 同一堵墙撞第二次即发兜底执行工单交人 + 立监听(头一次软化留痕),
不再空转到 exhausted 而人无处可改。
debt +4(C1 根治 / modules→app / thead-after-tbody / data-by 落回外层),
并补写此前被两处引用却从不存在的「refersTo 传递闭包」条目。
文档:ADR-014:201「按声明顺序求值」改对(照它写配方的人会重犯本轮的地基缺陷)、
陈旧注释四处、spec 四处分叉逐条判断处置、端到端测试记录标为历史、
CLAUDE.md 纠正「新建类型端到端通了」那句(它没通过真人路)并更新版本与文档表。
包版本 2.1.9(已装,SQL 直查库内 version/required/gate_reason/视图 HTML 四项核实)。
pnpm gates 退出码 0(2476 通过 / 34 跳过);E04 92/92、I01 55/55 无回归。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* 配方字段求值序 —— 纯函数,零 IO(编码规范 §5.3)。
|
||||
* 配方字段(与 `let`)求值序 —— 纯函数,零 IO(编码规范 §5.3)。
|
||||
*
|
||||
* `AutoAction.fields` / `analyze.input.fields` 存进 `sys.activity_package.manifest`
|
||||
* (`jsonb` 列)之后,对象键的**原始书写顺序不再保证**:PostgreSQL 的 `jsonb` 按
|
||||
* 「先短后长、同长字典序」把每一层嵌套对象的键重新排列(实测见
|
||||
* `AutoAction.fields` / `analyze.input.fields` / `AutoRecipe.let` 存进
|
||||
* `sys.activity_package.manifest`(`jsonb` 列)之后,对象键的**原始书写顺序不再保证**:
|
||||
* PostgreSQL 的 `jsonb` 按「先短后长、同长字典序」把每一层嵌套对象的键重新排列(实测见
|
||||
* `.superpowers/sdd/2026-08-29-act-work-05-rewrite/judgement-class-probe.md`)。
|
||||
*
|
||||
* `auto-runner` 逐字段求值时把已经算出来的值放进 `ctx.computed`,后面的字段能通过
|
||||
@@ -15,14 +15,35 @@
|
||||
* 这个文件按**字段名之间的真实依赖**(而不是声明顺序 / jsonb 存取后的顺序)排出一个
|
||||
* 安全的求值序 —— 与存储格式无关,`fields` 不必改成数组(那是破坏性契约变更)。
|
||||
* 真正的环(A 依赖 B、B 依赖 A)排不出来,直接抛错 —— 不默默兜底,兜底正是这次出事的形态。
|
||||
*
|
||||
* **`AutoRecipe.let` 是同一个地基缺陷的第二个受害者**(2026-08-30 全分支终审 Important 1,
|
||||
* H47 当时只治了 `fields` 那一半):`auto-runner.ts` 从前逐 `Object.entries(recipe.let)`
|
||||
* 按声明顺序求值,而 `let` 同样住在这份 jsonb 里、同样会被重排。且 `let` 与 `fields` 的
|
||||
* 依赖读法**不一样**——`fields` 的字段之间只能通过 `when` 字符串里的裸标识符互相看见
|
||||
* (`{in:X}` 在 `fields` 里读的是 `lets`/`scope`,永远读不到同一个 `fields` 对象里的
|
||||
* 另一个字段);而 `let` 的**官方读法就是 `{in:X}`**(见 `packages/contracts/src/
|
||||
* auto-recipe.ts` 的注释:「先算一遍,后面所有表达式都能 {in:'名'} 引用」,运行时
|
||||
* `evalExpr` 的 `'in' in expr` 分支优先读 `ctx.lets[X]`)——`whenReferences` 故意不认
|
||||
* 裸 `{in:X}` 是对的(对 `fields` 而言),但直接照搬给 `let` 用就会**漏掉最主要的那种
|
||||
* 依赖**,排序等于没排。所以 `let` 用的是下面单独一份 `letReferences`/`orderLets`,
|
||||
* 不是简单复用 `whenReferences`/`orderFields`。
|
||||
*/
|
||||
import type { AutoExpr } from '@contracts';
|
||||
import { staticCheck } from '@sfeel';
|
||||
import { topoOrder } from '@utils';
|
||||
|
||||
/**
|
||||
* `noun` 让同一个错误类型给 `fields`(默认「字段」)与 `let`(见下面 `orderLets`)
|
||||
* 各自说准话:两者的依赖读法不一样(`let` 多认 `{in:X}`,提示语也得多提这一句),
|
||||
* 但都是"排不出安全求值序"这同一件事,不必另开一个类。
|
||||
*/
|
||||
export class AutoFieldCycleError extends Error {
|
||||
constructor(readonly cycle: readonly string[]) {
|
||||
super(`字段之间存在循环依赖,求不出安全的求值序:${cycle.join('、')}(检查这几个字段的 when 是否互相引用了对方)`);
|
||||
constructor(
|
||||
readonly cycle: readonly string[],
|
||||
noun: '字段' | 'let' = '字段',
|
||||
) {
|
||||
const how = noun === 'let' ? '互相用 `{in:…}` 或 when 引用了对方' : '互相引用了对方';
|
||||
super(`${noun}之间存在循环依赖,求不出安全的求值序:${cycle.join('、')}(检查这几个${noun}是否${how})`);
|
||||
this.name = 'AutoFieldCycleError';
|
||||
}
|
||||
}
|
||||
@@ -80,3 +101,53 @@ export function orderFields(fields: Readonly<Record<string, AutoExpr>>): Readonl
|
||||
if (!result.ok) throw new AutoFieldCycleError(result.cycle);
|
||||
return result.order.map((name) => [name, byName.get(name)!] as const);
|
||||
}
|
||||
|
||||
/**
|
||||
* `letReferences` 与 `whenReferences` 几乎同一套遍历,只多认一种入口:
|
||||
* **裸 `{in:X}` 本身就是依赖**——`let` 唯一的官方跨条目读法(见文件头注)。
|
||||
* `when`/`lookup.by`/`cat`/`fmt` 四种嵌套形态与 `whenReferences` 完全一致
|
||||
* (`let` 的 `when` 子句同样进的是含 `...ctx.lets` 的合并作用域,见 `auto-expr.ts`
|
||||
* `evalExpr` 最后那段 `scope` 拼装,裸标识符引用另一个 `let` 的名字同样成立)。
|
||||
*
|
||||
* **与 `activity-registry/internal/domain/static-audit.ts` 里的 `letReferences` 是
|
||||
* 同一个算法,写了两遍**——理由与 `whenReferences` 头注同款(L4 装包静态审不能 import
|
||||
* L5 配方执行器)。改动这个函数时,记得看看那边要不要一起改。
|
||||
*/
|
||||
function letReferences(expr: AutoExpr): readonly string[] {
|
||||
if (typeof expr !== 'object' || expr === null) return [];
|
||||
if ('in' in expr) return [expr.in];
|
||||
if ('when' in expr) {
|
||||
const verdict = staticCheck(expr.when);
|
||||
const names = verdict.references.map((path) => path.split('.')[0] ?? path);
|
||||
return [...names, ...letReferences(expr.then), ...letReferences(expr.else)];
|
||||
}
|
||||
if ('lookup' in expr) {
|
||||
const byNames = Object.values(expr.by).flatMap((value) => letReferences(value));
|
||||
return expr.else === undefined ? byNames : [...byNames, ...letReferences(expr.else)];
|
||||
}
|
||||
if ('cat' in expr) return expr.cat.flatMap((part) => letReferences(part));
|
||||
if ('fmt' in expr) return letReferences(expr.fmt);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 `recipe.let` 排出一个安全的求值序 —— 与 `orderFields` 同一个用法(调用方直接拿它
|
||||
* 替掉 `Object.entries(recipe.let)`),只是依赖读法换成 `letReferences`(认 `{in:X}`)。
|
||||
* 找不出安全序(真正的环,如 `a: {in:'b'}, b: {in:'a'}`)抛 `AutoFieldCycleError`
|
||||
* (`noun: 'let'`,提示语说的是 `let` 不是字段)。
|
||||
*/
|
||||
export function orderLets(lets: Readonly<Record<string, AutoExpr>>): ReadonlyArray<readonly [string, AutoExpr]> {
|
||||
const entries = Object.entries(lets);
|
||||
const names = new Set(entries.map(([name]) => name));
|
||||
const deps = new Map<string, readonly string[]>(
|
||||
entries.map(([name, expr]) => [name, [...new Set(letReferences(expr))].filter((ref) => ref !== name && names.has(ref))]),
|
||||
);
|
||||
const byName = new Map(entries);
|
||||
|
||||
const result = topoOrder(
|
||||
entries.map(([name]) => name),
|
||||
(name) => deps.get(name) ?? [],
|
||||
);
|
||||
if (!result.ok) throw new AutoFieldCycleError(result.cycle, 'let');
|
||||
return result.order.map((name) => [name, byName.get(name)!] as const);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user