- vault.update content-guard hint pointed at vault.patch, which does not
exist — patch lives at edit.patch. Bad recovery hint is worse than no
hint.
- edit.window guard test claimed "before searching the file" but only
asserted on mutations; the mock's 'original' content doesn't contain
'undefined' so even a leaked guard wouldn't show a write. Add a reads
recorder and assert getFile is never called, making the test honest
about what it proves.
- Strip a stray space from the lock-key guard comment.
#210 reported that `vault.update` with no `content` writes the literal
9-byte string `"undefined"` to the target file — `String(undefined)`
coerced silently, the validator accepted the 9-byte string, and the
formatter rendered the misleading heading `✓ Updated: undefined`.
The same `String(params.x)` footgun existed at every vault-mutating
dispatch site: `edit.append` (would append "undefined"), `edit.window`
(would search for or replace with "undefined"), plus path-only cases
that produced cryptic "File not found: undefined" errors instead of a
clean dispatch-level rejection.
Fix the class, not just the instance:
- Add `requireParamStr(params, key, action, hint?)` in operations/shared.
Throws at the boundary when a required string param is missing or
wrong-typed, so a malformed MCP call cannot reach a vault sink.
- Convert the corruption-class sites (`vault.update`, `vault.create`,
`vault.delete`, `edit.append`, `edit.window`) and the noisy path-only
sites (`edit.patch`, `edit.at_line`, `edit.from_buffer`, `view.file`,
`view.window`, `view.open_in_obsidian`) to use the helper.
- `vault.update`'s `content` guard carries a hint pointing at
`vault.patch` / `edit.window` — that's the wrong-tool path that
produced the #210 corruption in the wild.
- Lift `edit.*`'s path guard above `FileLockManager.withLock` so a
missing path can't take a lock on the literal string "undefined".
- `updateFile` / `deleteFile` now return `{ success, path }` so
`formatFileWrite` renders `Updated: <path>` — belt-and-suspenders for
the second issue called out in #210.
Tests: `tests/dispatch-param-guards.test.ts` records every mutation a
mock API receives; for each malformed call we assert the route rejects
AND no mutation was recorded, proving the guard runs before any sink.