diff --git a/docs/superpowers/plans/2026-08-29-deterministic-steps-auto-recipe.md b/docs/superpowers/plans/2026-08-29-deterministic-steps-auto-recipe.md index a930e7d..1966371 100644 --- a/docs/superpowers/plans/2026-08-29-deterministic-steps-auto-recipe.md +++ b/docs/superpowers/plans/2026-08-29-deterministic-steps-auto-recipe.md @@ -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): 配方静态审 —— 写错地址在装包那一刻就拒