docs(plan): R8 —— 全 auto 的步要能声明「没有 guide」,并进 Task 9

Task 7 的评审挖出来的:manifest.ts 的 guide 是 z.string().min(1)(必填非空),
而全 auto 的步按设计就没有 guide —— 引擎正是按 guide.trim() === '' 分半 auto 与全 auto。
照现在的 schema,第二份计划里四步全 auto 的活动会在装包那一刻被拒。
改成 superRefine:声明了 auto 才允许 guide 为空;没有 auto 的照旧拦
——「作者忘了写做法」是个老错,那道门要留着。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 04:42:54 +08:00
co-authored by Claude Opus 5
parent fbd5867206
commit 80e9b01f84
@@ -2622,6 +2622,86 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
}
```
- [ ] **Step 4(R8):让全 auto 的步能声明「没有 guide」**
**这一条是 Task 7 的评审挖出来的,不在原计划里。** `packages/contracts/src/manifest.ts:122` 现在写着
`guide: z.string().min(1)` —— 必填且非空。而**全 auto 的步按设计就没有 guide**(引擎按
`guide.trim() === ''` 分「半 auto」与「全 auto」,见 Task 7 插入的那一段)。照现在的 schema,
第二份计划里四步全 auto 的活动会**在装包那一刻被拒**。
先写失败的测试,追加到 `packages/contracts/src/__tests__/auto-recipe.test.ts` 末尾:
```ts
import { planStepSchema } from '../manifest.ts';
describe('全 auto 的步可以没有 guide', () => {
const base = { item: 'S1', guide: '', writes: [], reads: [], tools: [] };
it('声明了 auto 就允许 guide 为空 —— 那一步压根不问模型', () => {
const parsed = planStepSchema.safeParse({
...base,
auto: { do: [{ kind: 'say', text: '办完了' }] },
});
expect(parsed.success).toBe(true);
});
it('没有 auto 又不写 guide → 拒 —— 「作者忘了写做法」这个老错照旧要拦', () => {
const parsed = planStepSchema.safeParse(base);
expect(parsed.success).toBe(false);
});
it('有 auto 但 guide 写了内容 → 照收(半 auto:确定的先做完,剩下的仍问模型)', () => {
const parsed = planStepSchema.safeParse({
...base,
guide: '把结论写实',
auto: { do: [{ kind: 'say', text: 'x' }] },
});
expect(parsed.success).toBe(true);
});
});
```
Run: `pnpm exec vitest run packages/contracts/src/__tests__/auto-recipe.test.ts -t "全 auto"`
Expected: 第一条 FAIL(`guide` 被 `.min(1)` 拒),后两条 PASS。
改法:把 `packages/contracts/src/manifest.ts:122` 的
```ts
guide: z.string().min(1),
```
改成
```ts
guide: z.string(),
```
并在 `planStepSchema` 的 `});` 之后接一段 `superRefine`(注意:`planStepSchema` 从此是 `ZodEffects`,
不能再 `.extend()` / `.omit()` —— 改之前先 `grep -rn "planStepSchema\." packages apps` 确认没有这类用法):
```ts
.superRefine((step, ctx) => {
/*
* **全 auto 的步没有 guide 是正常形态**(2026-08-29):引擎按 `guide.trim() === ''`
* 分「半 auto(确定的先做完,剩下的仍问模型)」与「全 auto(这一步一次都不问模型)」。
* 而 `.min(1)` 让后者压根写不出来 —— 四步全 auto 的活动会在装包那一刻被拒。
* 非空这条要求仍留给没有 auto 的步:**「作者忘了写做法」是个老错,照旧要拦**。
*/
if (step.auto === undefined && step.guide.trim() === '') {
ctx.addIssue({
code: 'custom',
path: ['guide'],
message: '这一步的做法不能为空 —— 只有声明了 auto 配方的步才允许没有 guide',
});
}
});
```
Run: `pnpm exec vitest run packages/contracts/src/__tests__/auto-recipe.test.ts`
Expected: 全绿。
契约变更四步照走:`pnpm api:check --update && git add api-report`。
- [ ] **Step 4: 挂进 `staticAudit`**
在 `staticAudit(manifest, external)` 函数体里,找到它 `out.push(...loopFindings(manifest))` 那一行(或等价写法),
@@ -2631,19 +2711,20 @@ export function autoRecipeFindings(manifest: ActivityManifest): StaticAuditFindi
out.push(...autoRecipeFindings(manifest));
```
- [ ] **Step 5: 跑测试与全量门禁**
- [ ] **Step 6: 跑测试与全量门禁**
Run:
```bash
pnpm exec vitest run apps/api/src/modules/activity-registry
pnpm exec vitest run packages/contracts
pnpm gates
```
Expected: 全绿。**现有三个活动包(E04 / I01 / W05)必须照装不误** —— 它们没有 `auto`,这一段整个不进。
- [ ] **Step 6: 提交**
- [ ] **Step 7: 提交**
```bash
git add apps/api/src/modules/activity-registry
git add apps/api/src/modules/activity-registry packages/contracts api-report
git commit -m "$(cat <<'EOF'
feat(activity-registry): 配方静态审 —— 写错地址在装包那一刻就拒