fix(widgets): reject unknown patch edit fields, list accepted aliases#31
Open
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Open
fix(widgets): reject unknown patch edit fields, list accepted aliases#31nsyring wants to merge 1 commit intoagent0ai:mainfrom
nsyring wants to merge 1 commit intoagent0ai:mainfrom
Conversation
The widget patch validator previously accepted edits with unrecognized
field names, silently producing destructive results. An LLM emitting
`{ path: "renderer", line: 264, replaceWith: "..." }` would have its
`replaceWith` payload dropped (not in the recognized aliases set). The
remaining `line` field is read as both `from` and `to`, the absent
replacement falls through to `contentLines: []`, and the validator
reports success on what is actually a single-line deletion. The runtime
prints the standard "saved, rendered ok" status and the agent treats
the destructive patch as the intended result.
Two changes close the loophole:
- `WIDGET_PATCH_KNOWN_EDIT_FIELDS` lists every recognized edit field
(content aliases, coordinate aliases, snippet aliases); the new
`listUnknownWidgetPatchEditFields(...)` helper returns anything
outside that set, and `normalizeWidgetPatchEdit(...)` throws on the
first unknown field with a message that lists the valid replacement
and coordinate aliases so the agent can self-correct.
- `replaceWith`, `replacement`, and `newText` are added to the
recognized content aliases. LLM patch outputs default to those names
from VS Code, OpenAI tool-call, and similar conventions; they unblock
the common patch shape without forcing every caller to rewrite the
field name first.
The existing insert-without-replacement error message is widened to
list all accepted replacement-text aliases instead of only `content`.
No API change, no Skill rewrite, no tests touched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(widgets): reject unknown fields and document accepted aliases on patchWidget edits
Summary
spaces/storage.jsaccepts widget patch edits with unrecognized field names (e.g.replaceWith,path,with) without surfacing an error and silently produces a no-op or destructive patch. This PR adds an unknown-field validator and lists the accepted replacement-text aliases in the existing error messages so a misnamed edit fails fast with a precise hint instead of corrupting the renderer.Why
LLM patch outputs frequently default to field names from VS Code edit APIs, OpenAI tool-call schemas, jsondiffpatch, etc. — for example
replaceWithinstead of the canonicalcontent/replace/text/valuealiases the widget patch validator currently accepts. Today the validator silently drops unrecognized fields, producing two failure modes I reproduced repeatedly during local widget development:Failure mode A — silent line deletion. An edit like
is accepted by the validator.
lineis treated as alias for bothfromandtoso each edit becomes a single-line replace at line 264, 265, etc. None of thereplaceWithpayloads is read becausereplaceWithis not in the recognized aliases set. The replace path falls through tocontentLines: hasContent ? ... : [], treating the missing replacement as an explicit "delete this line" intent. Six replacement edits become six silent line deletions, the runtime reportsWidget "X" saved, rendered ok, loaded to TRANSIENT, and the agent receives a positive result for what was actually a destructive change.Failure mode B — uninformative error. When a more obviously broken edit does fail (e.g. an insert without any replacement field), the existing message —
"Insert edits must include replacement text in \content`."— does not list the other accepted aliases, so the agent often retries with another non-canonical name (with,body,newContent`, ...) and still fails.The widget-skill SKILL.md already pins the canonical shapes and explicitly notes that
line,startLine/endLine,range,text, andreplaceare tolerated aliases. The validator did not carry that contract through to the actual checks.What changed
Single-file change in
app/L0/_all/mod/_core/spaces/storage.js:WIDGET_PATCH_CONTENT_FIELD_ALIASESconstant lists the recognized replacement-text aliases:content(canonical),text,replace,value, plus newly addedreplaceWith,replacement,newText. The existingreadWidgetPatchContentField(...)is rewritten to iterate this list rather than hard-code a four-branch ladder. No behavior change for existing callers; the three new aliases unblock common LLM patch shapes that previously had to be hand-corrected.WIDGET_PATCH_KNOWN_EDIT_FIELDSconstant captures the full set of edit-object fields the validator recognizes: every content alias plusfind,search,from,to,line,startLine,endLine,range,mode,kind. NewlistUnknownWidgetPatchEditFields(...)helper returns any keys outside that set.normalizeWidgetPatchEdit(...)rejects unknown fields at the top of the function, before any other validation. The error message lists the accepted replacement-text aliases, the accepted coordinate aliases, and thefind/searchsnippet shape so the agent can self-correct on the next turn. This closes the silent-deletion path because a typo'd content field name now fails with a clear name rather than being interpreted as an absent replacement.Existing insert-error message is widened to list all accepted replacement-text aliases instead of only
content, matching the rest of the validator's diagnostic style.space-widgets/SKILL.mdupdated to match the validator's new strict-mode contract. The "patch vs rewrite" subsection now distinguishes coordinate aliases from content aliases, listsreplaceWith/replacement/newTextalongside the previously documented content fields, and explicitly states that other field names will be rejected fast.path,replaceText, andbodyare called out as common typo'd field names so the agent does not generate them.No API change, no test touched (the patch validator is not exercised by unit tests in this repo today; adding one would require either exporting
applyWidgetPatchEditsor providing a module resolver for the/mod/...absolute imports thatspaces/storage.jsuses, both of which are outside this PR's scope).Worked example — silent deletion case
Before:
After:
The agent receives the diagnostic, drops
path, replacesreplaceWithwithcontentif it must, and retries.Test plan
node --check app/L0/_all/mod/_core/spaces/storage.jspassestests/spaces_widget_import_test.mjsandtests/spaces_prompt_context_test.mjscontinue to pass (validator is not exercised but the file imports cleanly){find,replace},{from,to,content},{line,replace},{range,text}all return zero unknowns;{path,line,replaceWith}flagspath;{from,to,with}flagswith;{line,replaceWith}returns zero unknowns (i.e. now passes through as expected)npm run desktop:packbuilds:path: "renderer", line: N, replaceWith: ...shape) and confirmed it now fails fast with the new error message; the agent self-corrects tofind/replaceWithon the next turn and the patch then applies as intendedfind/replaceshapes that already passed the validator pre-PR, and continues to pass post-PR. No regression for well-formed editsOut of scope (possible follow-ups)
delete: truemarker on line edits to disambiguate "intentional delete" from "missing content field," replacing the implicitcontent-omitted convention. The current PR keeps the implicit convention to avoid touching the SKILL contract.name,cols,rows, etc.) handled by the surroundingpatchWidgetbody. Those are a separate code path and outside the line-edit validator changed here.🤖 Generated with Claude Code