fix(schema-compat): allow reviewed whiteboard property corrections

ParamDecl already maps whiteboard insert anchors to referenceBlockId;
schema-compat froze the prior flag-name inference. Allowlist the exact
inference→declaration remaps so Interface Integrity can pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
玉澜
2026-08-05 17:05:35 +08:00
co-authored by Cursor
parent b3f493d8b8
commit d3d14eaddb
2 changed files with 110 additions and 1 deletions
+49 -1
View File
@@ -730,7 +730,6 @@ func checkParameterCompatibility(toolPath, name string, oldParameter, newParamet
new string
}{
{name: "type", old: oldParameter.Type, new: newParameter.Type},
{name: "property", old: oldParameter.Property, new: newParameter.Property},
{name: "default", old: oldParameter.Default, new: newParameter.Default},
{name: "interface_default", old: oldParameter.InterfaceDefault, new: newParameter.InterfaceDefault},
{name: "format", old: oldParameter.Format, new: newParameter.Format},
@@ -739,6 +738,13 @@ func checkParameterCompatibility(toolPath, name string, oldParameter, newParamet
failures = append(failures, fmt.Sprintf("schema tool %q parameter %q changed %s", toolPath, name, field.name))
}
}
// Property remaps are incompatible unless a reviewed correction replaces
// flag-name inference with the ParamDecl-declared MCP field. Arbitrary
// non-empty A→B remaps remain a contract break.
if oldParameter.Property != newParameter.Property &&
!compatibleReviewedPropertyCorrection(toolPath, name, oldParameter.Property, newParameter.Property) {
failures = append(failures, fmt.Sprintf("schema tool %q parameter %q changed property", toolPath, name))
}
// Clearing interface_type is accepted as compatible: a deliberate,
// wire-visible policy decision taken with the pinned MCP metadata
// retirement. Production no longer projects MCP-sourced types unless
@@ -783,6 +789,48 @@ func enumNarrowed(oldValues, newValues []string) bool {
return false
}
// reviewedPropertyCorrection is an exact allowlist entry for replacing a
// historical wire property (typically flag-name inference) with the MCP field
// declared on leaf ParamDecl.Property. Do not use this to bless arbitrary
// remaps: each entry must name tool path, parameter, old property, and new
// property exactly.
type reviewedPropertyCorrection struct {
toolPath string
param string
oldProperty string
newProperty string
}
// Keep this list tiny and evidence-backed. Prefer ParamDecl declaration; only
// add an entry when schema-compat would otherwise freeze a wrong inferred
// property that agents bind against.
var reviewedPropertyCorrections = []reviewedPropertyCorrection{
{
toolPath: "doc/doc.whiteboard_insert",
param: "parent-block",
oldProperty: "parentBlock",
newProperty: "referenceBlockId",
},
{
toolPath: "doc/doc.whiteboard_insert",
param: "ref-block",
oldProperty: "refBlock",
newProperty: "referenceBlockId",
},
}
func compatibleReviewedPropertyCorrection(toolPath, param, oldProperty, newProperty string) bool {
for _, entry := range reviewedPropertyCorrections {
if entry.toolPath == toolPath &&
entry.param == param &&
entry.oldProperty == oldProperty &&
entry.newProperty == newProperty {
return true
}
}
return false
}
func mergeContracts(historical, current schemaContract) (schemaContract, []string) {
failures := checkCompatibility(historical, current)
if len(failures) > 0 {
+61
View File
@@ -596,6 +596,67 @@ func TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion(t
}
}
func TestCrossPlatformCoverageSchemaCompatReviewedPropertyCorrection(t *testing.T) {
if !compatibleReviewedPropertyCorrection(
"doc/doc.whiteboard_insert", "ref-block", "refBlock", "referenceBlockId",
) {
t.Fatal("reviewed whiteboard ref-block correction must be accepted")
}
if !compatibleReviewedPropertyCorrection(
"doc/doc.whiteboard_insert", "parent-block", "parentBlock", "referenceBlockId",
) {
t.Fatal("reviewed whiteboard parent-block correction must be accepted")
}
if compatibleReviewedPropertyCorrection(
"doc/doc.whiteboard_insert", "ref-block", "refBlock", "otherProperty",
) {
t.Fatal("non-reviewed new property must remain incompatible")
}
if compatibleReviewedPropertyCorrection(
"doc/doc.create", "title", "title", "subject",
) {
t.Fatal("unlisted tool/param remap must remain incompatible")
}
baseline := schemaContract{
Products: map[string]productSchema{
"doc": {Tools: map[string]toolSchema{
"doc.whiteboard_insert": {
PrimaryCLIPath: "doc whiteboard insert",
InterfaceMode: "composite",
Availability: "available",
Effect: "write",
Risk: "medium",
Confirmation: "user_required",
Idempotency: "unknown",
Parameters: map[string]parameterSchema{
"ref-block": {Type: "string", Property: "refBlock"},
"parent-block": {Type: "string", Property: "parentBlock"},
},
},
}},
},
}
corrected := cloneContract(baseline)
corrected.Products["doc"].Tools["doc.whiteboard_insert"].Parameters["ref-block"] = parameterSchema{
Type: "string", Property: "referenceBlockId",
}
corrected.Products["doc"].Tools["doc.whiteboard_insert"].Parameters["parent-block"] = parameterSchema{
Type: "string", Property: "referenceBlockId",
}
if failures := checkCompatibility(baseline, corrected); len(failures) != 0 {
t.Fatalf("reviewed property corrections should pass: %v", failures)
}
broken := cloneContract(baseline)
broken.Products["doc"].Tools["doc.whiteboard_insert"].Parameters["ref-block"] = parameterSchema{
Type: "string", Property: "subject",
}
if failures := checkCompatibility(baseline, broken); !strings.Contains(strings.Join(failures, "\n"), "changed property") {
t.Fatalf("unreviewed property remap should fail: %v", failures)
}
}
func TestCrossPlatformCoverageSchemaCompatAdditiveConstraintEvolution(t *testing.T) {
oldTool := toolSchema{
Parameters: map[string]parameterSchema{