mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Parity for the two semantic guards, which Task 8 committed to for exactly this reason: a guard Edit honours and Fix merely implies is worse than no guard, because the inconsistency is invisible to whoever reads only one of them. The hole was not the one I reported at the end of round 2. Probed against the literal S6 batch, `fix.propose_repair` already refuses to hand on a batch carrying any rejection -- unlike Edit it never had a fallback to the first attempt, so the condemned batch was never written. What it did have was a BLIND surface: the engine-grounded reason was discarded and `_no_fix()` returned the generic "No safe automatic fix found", which the handler then stored on `fc.risk` and showed to nobody. The human at the gate was told a fix could not be found and given nothing to act on -- while the guard had just described the aggregator, the node and the selector that would have made a manual fix obvious. So: `_no_fix()` takes an optional reason (one dead-end path, not a second), a `would_run_wrong` verdict fills it in, and `handle_propose` renders it on an ErrorCard beside the reply line -- the same division of labour Edit's refusal uses. The verdict is read off the structured `would_run_wrong` list, never off the rejection prose, and deliberately NOT off `rejections` at large: those inline whole `intent.args` and belong in a re-prompt, not on a card, and a structural refusal says nothing about the batch that remains. A purely structural refusal therefore still ends at the generic sentence, as before. The `would_run_wrong` check is written as its own line rather than left to the broader rejection check it currently duplicates. That redundancy is a property of the line below it, not of the guard: if Fix is ever loosened the way Edit was, this is what still holds -- and it is what carries the reason either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>