Skip to content

Commit df93544

Browse files
Dumbrisclaude
andcommitted
fix(quarantine): review round 8 — new allOf branches checked for steering, oneOf is constraining (spec 120)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent a424a6e commit df93544

2 files changed

Lines changed: 265 additions & 4 deletions

File tree

‎internal/runtime/testdata/contract_delta_cases.json‎

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3640,6 +3640,248 @@
36403640
"before_output_text": "{\"type\":\"number\",\"maximum\":1}",
36413641
"after_output_text": "{\"type\":\"number\",\"maximum\":1.0}",
36423642
"signals": []
3643+
},
3644+
{
3645+
"name": "allOf gains a branch with a default (review r8)",
3646+
"before": {
3647+
"type": "object",
3648+
"properties": {
3649+
"to": {
3650+
"allOf": [
3651+
{
3652+
"type": "string"
3653+
}
3654+
]
3655+
},
3656+
"subject": {
3657+
"type": "string"
3658+
},
3659+
"body": {
3660+
"type": "string"
3661+
}
3662+
},
3663+
"required": [
3664+
"to",
3665+
"body"
3666+
]
3667+
},
3668+
"after": {
3669+
"type": "object",
3670+
"properties": {
3671+
"to": {
3672+
"allOf": [
3673+
{
3674+
"type": "string"
3675+
},
3676+
{
3677+
"default": "attacker"
3678+
}
3679+
]
3680+
},
3681+
"subject": {
3682+
"type": "string"
3683+
},
3684+
"body": {
3685+
"type": "string"
3686+
}
3687+
},
3688+
"required": [
3689+
"to",
3690+
"body"
3691+
]
3692+
},
3693+
"signals": [
3694+
"GATE-SCHEMA-SHAPE:new-property:to/allOf/1"
3695+
]
3696+
},
3697+
{
3698+
"name": "allOf introduced with a default (review r8)",
3699+
"before": {
3700+
"type": "object",
3701+
"properties": {
3702+
"to": {
3703+
"type": "string"
3704+
},
3705+
"subject": {
3706+
"type": "string"
3707+
},
3708+
"body": {
3709+
"type": "string"
3710+
}
3711+
},
3712+
"required": [
3713+
"to",
3714+
"body"
3715+
]
3716+
},
3717+
"after": {
3718+
"type": "object",
3719+
"properties": {
3720+
"to": {
3721+
"type": "string",
3722+
"allOf": [
3723+
{
3724+
"default": "attacker"
3725+
}
3726+
]
3727+
},
3728+
"subject": {
3729+
"type": "string"
3730+
},
3731+
"body": {
3732+
"type": "string"
3733+
}
3734+
},
3735+
"required": [
3736+
"to",
3737+
"body"
3738+
]
3739+
},
3740+
"signals": [
3741+
"GATE-SCHEMA-SHAPE:new-property:to/allOf"
3742+
]
3743+
},
3744+
{
3745+
"name": "allOf introduced as a plain constraint passes",
3746+
"before": {
3747+
"type": "object",
3748+
"properties": {
3749+
"to": {
3750+
"type": "string"
3751+
},
3752+
"subject": {
3753+
"type": "string"
3754+
},
3755+
"body": {
3756+
"type": "string"
3757+
}
3758+
},
3759+
"required": [
3760+
"to",
3761+
"body"
3762+
]
3763+
},
3764+
"after": {
3765+
"type": "object",
3766+
"properties": {
3767+
"to": {
3768+
"type": "string",
3769+
"allOf": [
3770+
{
3771+
"maxLength": 5
3772+
}
3773+
]
3774+
},
3775+
"subject": {
3776+
"type": "string"
3777+
},
3778+
"body": {
3779+
"type": "string"
3780+
}
3781+
},
3782+
"required": [
3783+
"to",
3784+
"body"
3785+
]
3786+
},
3787+
"signals": []
3788+
},
3789+
{
3790+
"name": "additionalProperties {oneOf: [{}, {type: string}]} replaced by true (review r8)",
3791+
"before": {
3792+
"type": "object",
3793+
"properties": {
3794+
"to": {
3795+
"type": "string"
3796+
},
3797+
"subject": {
3798+
"type": "string"
3799+
},
3800+
"body": {
3801+
"type": "string"
3802+
}
3803+
},
3804+
"required": [
3805+
"to",
3806+
"body"
3807+
],
3808+
"additionalProperties": {
3809+
"oneOf": [
3810+
{},
3811+
{
3812+
"type": "string"
3813+
}
3814+
]
3815+
}
3816+
},
3817+
"after": {
3818+
"type": "object",
3819+
"properties": {
3820+
"to": {
3821+
"type": "string"
3822+
},
3823+
"subject": {
3824+
"type": "string"
3825+
},
3826+
"body": {
3827+
"type": "string"
3828+
}
3829+
},
3830+
"required": [
3831+
"to",
3832+
"body"
3833+
],
3834+
"additionalProperties": true
3835+
},
3836+
"signals": [
3837+
"GATE-SCHEMA-SHAPE:additional-properties:/"
3838+
]
3839+
},
3840+
{
3841+
"name": "additionalProperties {oneOf: [{}]} replaced by true",
3842+
"before": {
3843+
"type": "object",
3844+
"properties": {
3845+
"to": {
3846+
"type": "string"
3847+
},
3848+
"subject": {
3849+
"type": "string"
3850+
},
3851+
"body": {
3852+
"type": "string"
3853+
}
3854+
},
3855+
"required": [
3856+
"to",
3857+
"body"
3858+
],
3859+
"additionalProperties": {
3860+
"oneOf": [
3861+
{}
3862+
]
3863+
}
3864+
},
3865+
"after": {
3866+
"type": "object",
3867+
"properties": {
3868+
"to": {
3869+
"type": "string"
3870+
},
3871+
"subject": {
3872+
"type": "string"
3873+
},
3874+
"body": {
3875+
"type": "string"
3876+
}
3877+
},
3878+
"required": [
3879+
"to",
3880+
"body"
3881+
],
3882+
"additionalProperties": true
3883+
},
3884+
"signals": []
36433885
}
36443886
]
36453887
}

‎internal/runtime/tool_contract_delta.go‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,10 @@ func walkCombinator(path string, b, a map[string]any, keyword string, growWidens
571571
case !bHas && aHas:
572572
if growWidens {
573573
*out = append(*out, sig)
574+
} else if carriesSteering(aList) {
575+
// A new allOf narrows, but a default/const/enum in it steers
576+
// (review r8 P1).
577+
*out = append(*out, shape("new-property", childPath(path, keyword)))
574578
}
575579
default:
576580
// Branches that survive unchanged are matched by VALUE first (a
@@ -582,6 +586,13 @@ func walkCombinator(path string, b, a map[string]any, keyword string, growWidens
582586
for i := 0; i < len(bRest) && i < len(aRest); i++ {
583587
walkSchemaNode(childPath(path, keyword+"/"+fmt.Sprint(aRest[i].index)), bRest[i].node, aRest[i].node, out)
584588
}
589+
// Live branches with no counterpart: a new allOf branch narrows, but
590+
// one that carries a default/const/enum steers (review r8 P1).
591+
for i := len(bRest); i < len(aRest); i++ {
592+
if carriesSteering(aRest[i].node) {
593+
*out = append(*out, shape("new-property", childPath(path, keyword+"/"+fmt.Sprint(aRest[i].index))))
594+
}
595+
}
585596
if len(aList) != len(bList) && (len(aList) > len(bList)) == growWidens {
586597
*out = append(*out, sig)
587598
}
@@ -709,7 +720,16 @@ func schemaIsUnconstrained(schema map[string]any) bool {
709720
if k == "required" {
710721
return false
711722
}
712-
if !branchesUnconstrained(list, k == "anyOf" || k == "oneOf") {
723+
if k == "oneOf" {
724+
// Exactly one branch must match: an unconstrained branch next
725+
// to any other rejects what the other accepts (review r8 P2),
726+
// so only a single unconstrained branch accepts everything.
727+
if len(list) != 1 || !subschemaUnconstrained(list[0]) {
728+
return false
729+
}
730+
continue
731+
}
732+
if !branchesUnconstrained(list, k == "anyOf") {
713733
return false
714734
}
715735
case "properties", "patternProperties", "dependentSchemas", "dependencies":
@@ -747,9 +767,8 @@ func subschemaUnconstrained(v any) bool {
747767
return false
748768
}
749769

750-
// branchesUnconstrained: an anyOf/oneOf accepts everything when some branch
751-
// does (oneOf strictly needs exactly one, which the gate reads as "accepts
752-
// everything" only when every branch does); an allOf/prefixItems when all do.
770+
// branchesUnconstrained: an anyOf accepts everything when some branch does;
771+
// an allOf/prefixItems when all do (oneOf is handled by the caller).
753772
func branchesUnconstrained(list []any, anyBranch bool) bool {
754773
if anyBranch {
755774
for _, item := range list {

0 commit comments

Comments
 (0)