Repository navigation
feat(cli): bind intent flags one level beneath an object preset - #83
Conversation
…t preset An intent's preset was merged into a caller-supplied --body/--body-param per top-level key only, so a partial object for a preset key (e.g. setting one option of a union-typed property) replaced the preset object whole and dropped the union discriminator the preset pinned. Object presets now fill the missing keys of the caller's object at the merge points the schema proves safe; caller keys always win. The linker walks each object preset alongside its schema (named properties, map entries through additionalProperties, and properties declared beside a union) and records a merge point per JSON pointer: plain objects and map entries unconditionally, and union members only when the preset identifies exactly one member through the union's declared discriminator, or a const/single-enum key whose preset value no other member accepts, with explicit and implicit mapping aliases. At runtime a caller object is filled only at a merge point and, on a union, only while its discriminator is absent or selects the preset's member. Elsewhere the caller's object is kept whole, so union members are not mixed.
…nd composed maps - Keep union members that can still accept an object (no type, or an open object) when flattening, so a remaining constrained member is not treated as the only candidate. - In anyOf unions, a member that does not declare the candidate discriminator and is not closed also accepts the preset's value, so the const-inferred discriminator no longer identifies a member there. oneOf keeps the inference: an object matching a const member cannot also be valid for a catch-all member. - Scope discriminator mappings to their propertyName and treat a value that layers map to different members as ambiguous, instead of letting the last layer win. - Read additionalProperties schemas through allOf layers, so preset objects inside composed maps get merge points. - Compare discriminator values with exact JSON numbers, so distinct large integers no longer collide through float64.
a4bea69 to
598a809
Compare
b7dd612 to
1f315c5
Compare
… accept it A merge point inferred from a const discriminator was emitted when another member could still accept the caller's object, so preset fields from one member could be filled into an object valid only for another. - A member that does not declare the inferred key now counts as a possible match unless it is closed or places no constraint on an object at all (a catch-all, which no fill can invalidate); this replaces the oneOf/anyOf distinction, which also misread a oneOf composed through allOf. - A property declared both beside a union and on one of its members is the conjunction of the two, so a member's broad redeclaration no longer hides an inherited nested union. - Members without a type whose const or enum lists an object are kept as object members instead of being dropped.
An intent flag could only bind a top-level body field, so a field of an
object the command presets (one option of a union-typed property the preset
pins to a member) had no flag and was reachable only through --body.
A flag's `to:` may now name a field one level beneath an object preset
(`$.format.width` under `preset: {$.format: {kind: png}}`). The linker
resolves the field in the property's object, or in the union member the
preset selects, and infers its type, enum suggestions and any preset default.
It rejects a parent without an object preset, a union member the preset does
not uniquely identify, the union's discriminator, positional args, route
dispatch commands and operation-declared flags; deeper paths stay decode
errors.
Without a body the value is written into the preset object. With --body or
--body-param it is merged into the caller's object, created when absent, and
the preset then fills the discriminator. A caller object naming another
member, a non-object parent, and a field the caller already set are usage
errors.
1f315c5 to
1ee3e9b
Compare
…to const/enum values
An object restricted to const or enum values must equal one of them, so
filling preset keys into a caller's object there can only produce a value
matching none: preset {kind: png} filled into a caller's {preset: vector}
under enum [{kind: png}, {preset: vector}] sent {preset: vector, kind: png}.
Such objects, untyped or object-typed, standalone or as a union member, or
under a union whose layer declares the restriction, no longer get a merge
point.
The union-through-allOf fixture now uses anyOf: a oneOf with a member that
accepts every object cannot validate any object the pinned member accepts.
# Conflicts: # templates/templates/cli/README.md
…onst/enum values A field written by a flag into an object the schema restricts to const or enum values makes the object match none of them, so the linker now rejects such a bind instead of resolving the field.
…object A member typed as a list that includes object (e.g. type: [object, "null"]) with no other constraint accepts every object just like type: object, but was not recognized as a catch-all, so an otherwise identified preset member lost its merge point.
There was a problem hiding this comment.
All reported issues were addressed across 14 files
Shadow auto-approve: would not auto-approve because issues were found.
View guided diff | Turn on auto-fix | Re-trigger cubic
… own properties - A flag bound beneath an object preset to a readOnly field is now a decode error, as a preset targeting a readOnly property already is. - Preset coverage checks used `in`, so a key named like an Object.prototype member (toString, constructor) counted as preset and a required input was treated as covered; they now check own properties.
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. Adds a user-facing CLI feature that relaxes the previous one-level bind limit (nested intent flags under object presets), a product/contract decision; the dismissed stdin-body preset bug is only deferred to a separate #89 not visible in this diff.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 14 files
Shadow auto-approve: would not auto-approve because issues were found.
View guided diff | Turn on auto-fix | Re-trigger cubic
…on has one object member A union with a single object member (the others non-object) skipped the pinned-discriminator check, so a flag could overwrite the declared discriminator the preset pins and send an object matching no member.
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Shadow auto-approve: would require human review. Adds a user-facing CLI feature: nested intent flags under object presets are now accepted and rendered, changing the manifest contract and body/stdin merge rules; the chosen scope and the dismissed {} union-arm edge case need human sign-off.
View guided diff | Turn on auto-fix | Re-trigger cubic
Why
An intent command flag could only bind a top-level body field. When a command presets an object — for example one member of a union-typed property,
preset: {$.format: {kind: png}}— the fields of that object had no flag and could only be set through--body, which also requires repeating the preset's discriminator.What changed
cli_commands.go): a flag'sto:may name a field one level beneath an object preset ($.format.width). Deeper paths and array indexes stay decode errors.cli_commands_link.go):nestedBindScoperesolves the field in the property's object, or in the union member the preset selects (reusingflattenUnion/presetUnionMergePoint), then infers type, enum suggestions and a preset default as for top-level flags. Rejected with a targeted error: a parent without an object preset, a union member the preset does not uniquely identify, the union's discriminator as the field, positional args, route-dispatch commands, operation-declared flags, a parent restricted toconst/enumvalues, areadOnlyfield, another input binding the parent object itself. Decode also rejects a command where one input binds an object ($.format) and another binds a field beneath it ($.format.width), regardless of declaration order or whether the parent is a flag or a positional.intents.ts,intentcmd.go.stmpl): flags carry their bodyPath. Without a body the value is written into the preset object (flagutil.SetBodyPath); with--body/--body-paramor a body piped on stdin it goes throughPresetMerge.MergeNestedInput, then the preset fill (fix(cli): fill nested keys of a partial object from an intent's object preset #82) restores the discriminator.flags.go.stmpl,preset.go.stmpl):MergeInputIntoBodydelegates to a path-basedMergeInputIntoBodyPath(top-level behaviour and error text unchanged).MergeNestedInputrefuses a caller object that names another union member (--width applies when image_format.kind is "png", but --body sets "svg"), a non-object parent, and a field the caller already set.render --width($.image_format.width) in the primary overlay; README bullet; changeset. Review SDK (zSDKs/sdk-cli) regenerated:internal/flagutil/{flags,preset}.goonly.Testing
go test ./internal/extensions/— newTestCLICommands_NestedPresetBinds,TestCLICommands_NestedPresetBindErrors(10 cases, including parent/nested bind overlap in both declaration orders and with a positional parent),TestCLICommands_NestedBindRejectedOnRouteDispatch; parser path cases updated; operation-declared nested flag rejection inTestCLICommands_OperationDeclarationsAreStrict.TARGET=review make test-cli— 609 tests, 2 skipped, 0 failures.TARGET=primary make test-cli— 1687 tests, 0 failures. NewTestIntentNestedFlagIntoPipedBody(a nested flag merged into a body piped on stdin gets the preset discriminator filled; a field the piped body already sets is a conflict naming stdin) and new cases inTestIntentPresetFromArgsandTestIntentPartialBodyKeepsPresets(written into the preset; merged into a partial body; conflict; other member; non-object parent). Run with agent-mode environment variables unset: with them set, agent mode disables interactive forms and the fourTestConfigure*tests fail independently of this change.make check-template-cli,gofmt,prettier,git diff --check.Public-safety check
.claude/skills/public-repo-communication/SKILL.md).git diff --check.Summary by cubic
An intent command flag can now bind a field one level beneath an object preset (
--widthfor$.format.widthunderpreset: {$.format: {kind: png}}), and the preset fills the missing keys of a partial--body/--body-paramobject instead of replacing it whole and dropping the discriminator.--body,--body-param, or stdin.const/enumvalues, areadOnlyfield, and input binding both an object and a field beneath it; deeper paths stay decode errors.toStringorconstructorno longer counts as preset-covered.Written for commit 694004c. Summary will update on new commits.