Archived
fix: 配方只认 liveStep;保存不再无端叫醒任务;prune 不在人填的时候撤行
**I6 · 配方只认 `liveStep`**:`current` 有一条兜底(`liveStep ?? 在场的第一个有剧本的项`), 它不过滤 `settledIds`。当初这只影响提示词内容;配方接进来之后它驱动**副作用** —— 再写一遍台账、再 prune 一遍,carded 为假时甚至再发一张卡。兜底继续只服务提示词。 同处 `settledIds` 改取 sweep 之后的 `stateRuntime`(`liveStepOf` 吃的是 sweep 之后的 `stateEnabled`,两边得同一份局面)。 **注**:这一形状(既已收掉又在场)我没能在当前 lifecycle 里构造出可达来路 —— `publishEnabled` 把 Completed/Terminated 挡在 entries 与 inProgress 之外, `reenter` 又是就地改状态、不留旧实例。用例用的是 `live-step.ts` 注释描述的那种局面 (同 id 两份实例),守的是不变式;详见最终报告。 **F1 · `afterSave` 只在有未答的卡时才 `wake`**:`wake` 对 awaiting-event / awaiting-human 一律拨成 running,推进器随即推一拍并**记一步预算**。而现有九个活动包一个 auto 都没有: 人每按一次保存就白烧一步、界面闪一次「AI 正在推进这单」;任务本来 awaiting-human (等人裁决)时被叫醒,那一拍结束还会把它写成 awaiting-event —— 从「等人办事」降级成 「等外部回音」,而等的明明还是人。**这是界面撒谎。**「保存即铺格」的场景恰恰都有卡在场。 用既有的 `humanTool.pendingCards`,不新造。`wake` 失败不再 `.catch(() => false)` 静默 —— 它是「保存即铺格」的唯一触发器,吞掉等于人按了保存、格子没出现、系统一个字不说。 **F3 · `prune` 不在人填的时候撤行**:人在件上「加一行」加出来的行,配方的数据源里当然没有 —— 编辑中途把它悄悄撤掉是最不能接受的一种静默数据丢失。有卡在等人(quiet)那几拍只铺不撤; 同一张表既被 prune 又列在该步 `humanWrites` 里,装包报 warn;规范 §4.6 纪律补两条 (prune 的表不该是人能加行的表 · card 之后那几笔要等人交回之后那一拍才跑)。 I6 2 测(做过变异验证)· F1 5 测 · F3 2+1 测。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -641,6 +641,31 @@ describe('配方静态审:写错地址 = 每一单都错,装的时候就拒', ()
|
||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('没给主键列:case_ref'))).toBe(true);
|
||||
});
|
||||
|
||||
it('prune 的表又列在 humanWrites 里 → warn(人加的行会被下一拍静默撤掉)', () => {
|
||||
const base = manifestFixture();
|
||||
const manifest = {
|
||||
...base,
|
||||
assets: {
|
||||
...base.assets,
|
||||
businessModel: {
|
||||
...base.assets.businessModel,
|
||||
steps: [
|
||||
{
|
||||
item: 'S1',
|
||||
guide: '',
|
||||
writes: [],
|
||||
reads: [],
|
||||
tools: [],
|
||||
humanWrites: ['demo.record'],
|
||||
auto: { do: [{ kind: 'project', into: 'demo.record', prune: true, from: { once: true }, fields: { case_ref: 'a' } }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never;
|
||||
expect(autoRecipeFindings(manifest).some((f) => f.severity === 'warn' && f.message.includes('humanWrites'))).toBe(true);
|
||||
});
|
||||
|
||||
it('card 没有 basis → error(基座拒发 basis 为空的工单,跑到那儿就停了)', () => {
|
||||
const findings = autoRecipeFindings(withRecipe({ do: [{ kind: 'card', as: 'execution', title: 'x' }] }));
|
||||
expect(findings.some((f) => f.severity === 'error' && f.message.includes('没有 basis'))).toBe(true);
|
||||
|
||||
@@ -508,6 +508,23 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
|
||||
out.push({ code: 'schema-invalid', severity: 'error', message: `配方写的列不在 ${action.into} 的声明里:${name}`, at: where });
|
||||
}
|
||||
}
|
||||
/*
|
||||
* **prune 的表不该是人在件上能加行的表**(2026-08-29 终审 F3)。
|
||||
* `prune` 说的是「这一笔投影出来的行就是全部,多出来的撤掉」;而 `humanWrites`
|
||||
* 说的是「这张表归人填,模型没有来源」—— 人加的行配方的数据源里当然没有,
|
||||
* 下一拍对齐就把它撤了。两条声明放在同一张表上,意思是相反的。
|
||||
*/
|
||||
if (action.prune === true && (step.humanWrites ?? []).includes(action.into)) {
|
||||
out.push({
|
||||
code: 'schema-invalid',
|
||||
severity: 'warn',
|
||||
message:
|
||||
`${action.into} 既被这一笔 prune 投影、又列在这一步的 humanWrites 里 —— ` +
|
||||
'人在件上加的行配方的数据源里没有,下一拍对齐会把它撤掉(静默);' +
|
||||
'要么别 prune 这张表,要么它就不该是人能加行的表',
|
||||
at: where,
|
||||
});
|
||||
}
|
||||
/*
|
||||
* **主键列一个都不能少**(2026-08-29 终审 I4)。运行时 `auto-runner` 拿
|
||||
* `decl.keys` 逐列取值,取不到就当空主键 —— 那一行**静默 `continue`**:
|
||||
|
||||
Reference in New Issue
Block a user