Merge remote-tracking branch 'origin/main' into pr-922

This commit is contained in:
玉澜
2026-08-10 22:36:07 +08:00
55 changed files with 6520 additions and 2945 deletions
+15 -3
View File
@@ -59,7 +59,7 @@ RUNTIME_CONTRACT_END = "<!-- DWS_RUNTIME_CONTRACT_END -->"
# here only after verifying that the product skill has its own reviewed routing
# section and intent table; compacting a sparse skill without an alternative
# route would make its shortcuts harder to discover.
COMPACT_PRODUCT_SERVICES = {"chat"}
COMPACT_PRODUCT_SERVICES = {"chat", "doc"}
def md_escape(value: Any) -> str:
@@ -121,7 +121,10 @@ def runtime_contract_block() -> str:
def mono_overview(items: list[dict[str, Any]]) -> str:
counts = Counter(item["service"] for item in items)
# The source collector cannot see a small number of commands whose final
# canonical names are normalized at runtime. Overview counts come from the
# reviewed public catalog so they cannot under-report those declarations.
counts = Counter(service for service, _ in load_public_catalog())
rows = []
for service, count in sorted(counts.items()):
path = SERVICE_TO_SKILL.get(service)
@@ -163,10 +166,18 @@ def product_section(service: str, rows: list[dict[str, Any]]) -> str:
def compact_product_section(service: str, rows: list[dict[str, Any]]) -> str:
# Compact skills intentionally do not depend on the source parser's ability
# to recover every runtime-normalized declaration. The reviewed public
# catalog is the count authority for this non-enumerating overview.
public_count = sum(1 for item_service, _ in load_public_catalog() if item_service == service)
if service == "doc":
discovery = """已知意图按下方路由。"""
else:
discovery = """已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。"""
return f"""{PRODUCT_START}
## Shortcut 发现(按需)
`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
`{md_escape(service)}` 当前有 {public_count} 条公开 shortcut,完整清单保留在 Runtime Catalog 与 Schema,不在高频产品根 Skill 中重复展开。{discovery}
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
{PRODUCT_END}"""
@@ -194,6 +205,7 @@ def update_runtime_contract(check: bool) -> list[Path]:
changed = []
targets = [
ROOT / "skills" / "multi" / "dingtalk-chat" / "SKILL.md",
ROOT / "skills" / "multi" / "dingtalk-doc" / "SKILL.md",
SHARED_SKILL,
]
for path in targets:
+85 -1
View File
@@ -7,12 +7,14 @@ cd "$ROOT"
python3 scripts/gen_skill_shortcut_sections.py --check
chat_skill="skills/multi/dingtalk-chat/SKILL.md"
doc_skill="skills/multi/dingtalk-doc/SKILL.md"
event_skill="skills/multi/dingtalk-misc/references/event.md"
mono_skill="skills/mono/SKILL.md"
runtime_contract="skills/multi/dingtalk-shared/references/runtime-contract.md"
chat_target_bytes=10000
chat_max_overage_percent=5
chat_max_bytes=$((chat_target_bytes * (100 + chat_max_overage_percent) / 100))
doc_max_bytes=9000
event_max_bytes=10000
runtime_contract_max_bytes=3000
@@ -23,6 +25,13 @@ if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
exit 1
fi
doc_bytes="$(wc -c < "$doc_skill" | tr -d ' ')"
if [ "$doc_bytes" -gt "$doc_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill is ${doc_bytes} bytes (max ${doc_max_bytes})" >&2
exit 1
fi
event_bytes="$(wc -c < "$event_skill" | tr -d ' ')"
if [ "$event_bytes" -gt "$event_max_bytes" ]; then
printf '%s\n' \
@@ -51,6 +60,20 @@ if [ "$shortcut_rows" -ne 0 ]; then
exit 1
fi
doc_shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\| `dws doc \+/ { count++ }
END { print count + 0 }
' "$doc_skill"
)"
if [ "$doc_shortcut_rows" -ne 0 ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill re-expanded $doc_shortcut_rows shortcut rows" >&2
exit 1
fi
for required_heading in \
"## 最小 DWS 执行契约" \
"## Golden Route" \
@@ -65,6 +88,67 @@ do
fi
done
for required_heading in \
"## 最小 DWS 执行契约" \
"## Golden Route" \
"## 关键结果语义" \
"## 按需加载" \
"## 错误最短路径"
do
if ! grep -Fq "$required_heading" "$doc_skill"; then
printf '%s\n' \
"skill value regression: $doc_skill is missing $required_heading" >&2
exit 1
fi
done
for required_route in \
"+search" \
"+fetch" \
"+create" \
"+update" \
"+checkpoint-update" \
"+export"
do
if ! grep -Fq -- "$required_route" "$doc_skill"; then
printf '%s\n' \
"skill route regression: $doc_skill is missing $required_route" >&2
exit 1
fi
done
for forbidden_route in \
"## 标准 SOP" \
"doc_create_and_write.py" \
"只在任务命中时读取一个精确 reference" \
"MUST 先用 Read" \
"先用 Read 工具完整读取"
do
if grep -Fq "$forbidden_route" "$doc_skill"; then
printf '%s\n' \
"skill route regression: $doc_skill restored legacy route: $forbidden_route" >&2
exit 1
fi
done
for required_reference_rule in \
"Golden Route 已给出命令且参数足够时,禁止读取 reference" \
"才最多读取一个 reference" \
"\`+create\`、\`+fetch\`、\`+update\` append/overwrite、\`+export\`、\`+import\` 禁止读取 reference"
do
if ! grep -Fq "$required_reference_rule" "$doc_skill"; then
printf '%s\n' \
"skill reference budget regression: $doc_skill is missing $required_reference_rule" >&2
exit 1
fi
done
if grep -Fq "../dingtalk-shared/SKILL.md" "$doc_skill"; then
printf '%s\n' \
"skill context regression: $doc_skill requires full dingtalk-shared cold-start loading" >&2
exit 1
fi
for required_route in \
"+dm" \
"+send-to-group" \
@@ -108,4 +192,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
fi
printf '%s\n' \
"skill context budget: ok (chat_bytes=$chat_bytes target=$chat_target_bytes max=$chat_max_bytes allowance=${chat_max_overage_percent}% event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows)"
"skill context budget: ok (chat_bytes=$chat_bytes target=$chat_target_bytes max=$chat_max_bytes allowance=${chat_max_overage_percent}% doc_bytes=$doc_bytes doc_max=$doc_max_bytes event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows doc_shortcut_rows=$doc_shortcut_rows)"
+135 -2
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"os"
"reflect"
"sort"
"strings"
)
@@ -510,7 +511,8 @@ func checkToolCompatibility(toolPath string, oldTool, newTool toolSchema) []stri
}
if oldTool.Constraints != newTool.Constraints &&
!compatibleHiddenSiblingConstraintExpansion(oldTool, newTool) &&
!compatibleAdditiveConstraintEvolution(oldTool, newTool) {
!compatibleAdditiveConstraintEvolution(oldTool, newTool) &&
!compatibleReviewedConstraintTransition(toolPath, oldTool, newTool) {
failures = append(failures, fmt.Sprintf("schema tool %q changed constraints", toolPath))
}
if !compatiblePositionals(oldTool.Positionals, newTool.Positionals) {
@@ -562,6 +564,31 @@ var reviewedInterfaceRefRedirect = map[string]map[string]string{
},
}
// reviewedConstraintTransition enumerates exact constraint changes that were
// explicitly reviewed even though the compatibility gate cannot infer their
// business semantics. Keep this narrow: removing a constraint can expose a new
// runtime route, so arbitrary removals must not pass as harmless drift.
var reviewedConstraintTransition = map[string]map[string]string{
// PR #933 aligned the Shortcut and Schema with the existing doc import
// runtime: omitting both targets imports into the default root. Keeping the
// historical require_one_of made the documented Golden Route unreachable.
"doc/doc.shortcut_import": {
`{"require_one_of":[["folder","workspace"]]}`: "",
},
}
func compatibleReviewedConstraintTransition(toolPath string, oldTool, newTool toolSchema) bool {
transitions, ok := reviewedConstraintTransition[toolPath]
if !ok {
return false
}
want, ok := transitions[oldTool.Constraints]
if !ok {
return false
}
return want == newTool.Constraints
}
// compatibleInterfaceRefRedirect accepts repointing a tool at a different
// backing RPC when the migration is an explicitly reviewed entry in
// reviewedInterfaceRefRedirect **and** the CLI-facing contract is provably
@@ -814,14 +841,120 @@ func compatiblePositionals(oldPositionals, newPositionals []positionalSchema) bo
return true
}
// parameterTypeChange identifies one published parameter type migration
// exactly: which tool, which parameter, and in which direction. Nothing is
// matched by wildcard — an entry differing in any of the four fields does not
// apply.
//
// ToolPath is "<product id>/<tool id>" exactly as the comparison builds it (see
// where toolPath is assembled), e.g.
// "minutes/minutes.apply_minutes_permission". From and To are the canonical
// type values schemaType produces — the JSON encoding of the "type" keyword,
// so `"string"` **with** its quotes, not a bare string. Spelling either one any
// other way silently disables the exemption instead of failing loudly (exactly
// how reviewedInterfaceRefRedirect was broken twice), so
// TestCrossPlatformCoverageReviewedParameterTypeChangeKeysAreCanonical
// recomputes both through schemaType rather than trusting the table.
type parameterTypeChange struct {
ToolPath string
Parameter string
From string
To string
}
// reviewedParameterTypeChanges enumerates the individually reviewed parameter
// type migrations this gate accepts. A published type is part of the Schema
// contract and a swap cannot be proven safe from the type names alone, so a
// migration is accepted only when this exact tool, parameter and old→new pair
// appear here. Adding an entry is a contract decision and belongs in review,
// not in a feature change.
//
// Entries are direction-sensitive by construction: "string" → "integer" is a
// separate key from "integer" → "string", and only the reviewed direction is
// accepted.
//
// The table alone does not admit a change: the migration is rejected unless
// every other published field of the parameter is identical — not merely free
// of compatibility failures. See compatibleReviewedParameterTypeChange.
var reviewedParameterTypeChanges = map[parameterTypeChange]struct{}{
// "dws minutes permission apply --policy" moved from a String flag to a
// native Int flag, so the published type follows it from "string" to
// "integer". A parameter's type is projected from the Cobra flag type
// (provenance source cobra_flag_type), so it describes how the CLI accepts
// the value, not how the backing RPC receives it.
//
// Accepted because no historical caller is invalidated:
//
// - Consumers read this Schema to construct CLI invocations, and a command
// line is text either way: "--policy 4" is the same argv under both
// declarations, and a quoted "--policy \"4\"" still reaches pflag as 4.
// - The flag keeps enforcing the same [2,4] domain in RunE, and pflag's
// base-0 parse accepts every base-10 spelling the previous
// strconv.ParseInt(v, 10, 64) accepted, so the set of successful
// invocations only widens.
// - "integer" also matches what actually goes on the wire: this parameter
// maps to property "policyId", which the command has always sent as a
// number, so the new declaration is closer to the request than the old.
//
// The parameter's default stays absent on both sides here (a zero default is
// not published) and every other published field is identical, which is why
// the equality guard admits this entry. A migration that also moved the
// default — or relaxed required, cleared required_when, widened enum, cleared
// interface_type — would be rejected even though several of those are
// individually compatible, because none of them were reviewed under this
// entry.
{
ToolPath: "minutes/minutes.apply_minutes_permission",
Parameter: "policy",
From: `"string"`,
To: `"integer"`,
}: {},
}
// parameterContractUnchangedExceptType reports whether every published field of
// the parameter other than its type is identical.
//
// This is a full equality check rather than "the gate recorded no other
// failure", because several field changes are individually *compatible* and so
// produce no failure at all: relaxing required or cli_required, clearing
// required_when, widening enum, clearing interface_type, and clearing property
// through a reviewed mapping exclusion. Keying the carve-out on the failure list
// would let any of those ride along with a reviewed type migration without ever
// having been reviewed under that entry — the exemption would be wider than what
// it documents.
//
// Comparing the struct also means a field added to parameterSchema later is
// covered here automatically, instead of silently widening every existing entry.
func parameterContractUnchangedExceptType(oldParameter, newParameter parameterSchema) bool {
oldParameter.Type = ""
newParameter.Type = ""
return reflect.DeepEqual(oldParameter, newParameter)
}
// compatibleReviewedParameterTypeChange accepts a published parameter type
// migration when it is an explicitly reviewed entry **and** the rest of the
// parameter's published contract is unchanged.
func compatibleReviewedParameterTypeChange(toolPath, name string, oldParameter, newParameter parameterSchema) bool {
_, reviewed := reviewedParameterTypeChanges[parameterTypeChange{
ToolPath: toolPath,
Parameter: name,
From: oldParameter.Type,
To: newParameter.Type,
}]
return reviewed && parameterContractUnchangedExceptType(oldParameter, newParameter)
}
func checkParameterCompatibility(toolPath, name string, oldParameter, newParameter parameterSchema) []string {
var failures []string
if oldParameter.Type != newParameter.Type &&
!compatibleReviewedParameterTypeChange(toolPath, name, oldParameter, newParameter) {
failures = append(failures, fmt.Sprintf("schema tool %q parameter %q changed type", toolPath, name))
}
for _, field := range []struct {
name string
old string
new string
}{
{name: "type", old: oldParameter.Type, new: newParameter.Type},
{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},
+264
View File
@@ -430,6 +430,239 @@ func TestMergeContracts(t *testing.T) {
}
}
// registerReviewedParameterTypeFixture puts a fixture entry in the reviewed
// table for the duration of one test, so the behaviour tests exercise the real
// lookup instead of depending on whichever production entries happen to exist.
func registerReviewedParameterTypeFixture(t *testing.T, change parameterTypeChange) {
t.Helper()
if _, exists := reviewedParameterTypeChanges[change]; exists {
t.Fatalf("fixture %+v collides with a production entry", change)
}
reviewedParameterTypeChanges[change] = struct{}{}
t.Cleanup(func() { delete(reviewedParameterTypeChanges, change) })
}
func reviewedFormatTypeFixture() parameterTypeChange {
return parameterTypeChange{
ToolPath: "doc/doc.create",
Parameter: "format",
From: `"string"`,
To: `"integer"`,
}
}
const docCreateFormatTypeFailure = `schema tool "doc/doc.create" parameter "format" changed type`
func assertSchemaFailureContains(t *testing.T, failures []string, want string) {
t.Helper()
for _, failure := range failures {
if strings.Contains(failure, want) {
return
}
}
t.Fatalf("failures %v do not contain %q", failures, want)
}
func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChange(t *testing.T) {
baseline := cloneContract(baselineContract())
current := cloneContract(baseline)
mutateParameter(&current, func(parameter *parameterSchema) { parameter.Type = `"integer"` })
// Without an entry the migration is still a blocking change.
assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure)
registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture())
if failures := checkCompatibility(baseline, current); len(failures) != 0 {
t.Fatalf("a reviewed parameter type migration should pass: %v", failures)
}
}
func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeIsDirectionSensitive(t *testing.T) {
registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture())
// The reverse migration shares tool and parameter but not direction, so the
// string->integer entry must not admit it.
baseline := cloneContract(baselineContract())
mutateParameter(&baseline, func(parameter *parameterSchema) { parameter.Type = `"integer"` })
current := cloneContract(baselineContract())
assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure)
}
func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsOtherToolsAndParameters(t *testing.T) {
baseline := cloneContract(baselineContract())
current := cloneContract(baseline)
mutateParameter(&current, func(parameter *parameterSchema) { parameter.Type = `"integer"` })
// Same parameter and direction, different tool.
other := reviewedFormatTypeFixture()
other.ToolPath = "doc/doc.elsewhere"
registerReviewedParameterTypeFixture(t, other)
assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure)
// Same tool and direction, different parameter.
elsewhere := reviewedFormatTypeFixture()
elsewhere.Parameter = "title"
registerReviewedParameterTypeFixture(t, elsewhere)
assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure)
}
// A reviewed migration is only accepted when the rest of the parameter's
// contract held still. Each case bundles one unrelated regression with the
// reviewed type change and requires the type failure to reappear, so the
// exemption cannot carry a second change in behind it.
func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsBundledRegression(t *testing.T) {
registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture())
tests := []struct {
name string
mutate func(*parameterSchema)
}{
{name: "changed default", mutate: func(p *parameterSchema) { p.Default = `"text"` }},
{name: "changed interface_default", mutate: func(p *parameterSchema) { p.InterfaceDefault = `"text"` }},
{name: "changed format", mutate: func(p *parameterSchema) { p.Format = "uri" }},
{name: "redirected property", mutate: func(p *parameterSchema) { p.Property = "renamed" }},
{name: "redirected interface_type", mutate: func(p *parameterSchema) { p.InterfaceType = "integer" }},
{name: "newly required", mutate: func(p *parameterSchema) { p.Required = true }},
{name: "newly cli_required", mutate: func(p *parameterSchema) { p.CLIRequired = true }},
{name: "changed required_when", mutate: func(p *parameterSchema) { p.RequiredWhen = "always" }},
{name: "narrowed enum", mutate: func(p *parameterSchema) { p.Enum = []string{"markdown"} }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
baseline := cloneContract(baselineContract())
current := cloneContract(baseline)
mutateParameter(&current, func(parameter *parameterSchema) {
parameter.Type = `"integer"`
test.mutate(parameter)
})
assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure)
})
}
}
// Every drift below is individually *compatible* — on its own it produces no
// failure at all — which is precisely why the carve-out cannot be keyed on "the
// gate recorded no other failure for this parameter". Bundled with a reviewed
// type migration they must still be rejected: none of them were reviewed under
// that entry, and admitting them would make the exemption wider than what the
// table documents.
//
// Each case first asserts that the drift alone really is compatible, so a future
// rule change that starts reporting it turns into an explicit failure here
// rather than quietly turning this test into a duplicate of the bundled-
// regression cases above.
func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsIndividuallyCompatibleDrift(t *testing.T) {
registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture())
tests := []struct {
name string
baseline func(*parameterSchema)
current func(*parameterSchema)
}{
{
name: "relaxed required",
baseline: func(p *parameterSchema) { p.Required = true },
current: func(p *parameterSchema) { p.Required = false },
},
{
name: "relaxed cli_required",
baseline: func(p *parameterSchema) { p.CLIRequired = true },
current: func(p *parameterSchema) { p.CLIRequired = false },
},
{
name: "cleared required_when",
baseline: func(p *parameterSchema) { p.RequiredWhen = "always" },
current: func(p *parameterSchema) { p.RequiredWhen = "" },
},
{
name: "widened enum",
current: func(p *parameterSchema) { p.Enum = []string{"html", "markdown", "text"} },
},
{
name: "cleared interface_type",
baseline: func(p *parameterSchema) { p.InterfaceType = "string" },
current: func(p *parameterSchema) { p.InterfaceType = "" },
},
{
name: "cleared property through reviewed mapping exclusion",
current: func(p *parameterSchema) {
p.Property = ""
p.PropertySource = propertySourceReviewedMappingExclusion
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
baseline := cloneContract(baselineContract())
if test.baseline != nil {
mutateParameter(&baseline, test.baseline)
}
driftOnly := cloneContract(baseline)
mutateParameter(&driftOnly, test.current)
if failures := checkCompatibility(baseline, driftOnly); len(failures) != 0 {
t.Fatalf("前提不成立:该变化本身已不兼容,用例测不到相等性守卫: %v", failures)
}
bundled := cloneContract(baseline)
mutateParameter(&bundled, func(parameter *parameterSchema) {
parameter.Type = `"integer"`
test.current(parameter)
})
assertSchemaFailureContains(t, checkCompatibility(baseline, bundled), docCreateFormatTypeFailure)
})
}
}
// The behaviour tests above register their own fixtures, so they cannot catch a
// production entry whose type values are spelled in the wrong form. That mistake
// disables the exemption silently and only a real gate run reports it — exactly
// how reviewedInterfaceRefRedirect was broken twice. Recompute both values
// through schemaType instead of trusting the spelling in the table.
func TestCrossPlatformCoverageReviewedParameterTypeChangeKeysAreCanonical(t *testing.T) {
if len(reviewedParameterTypeChanges) == 0 {
t.Fatal("豁免表为空:若确已清空,请连这条守卫一起删除")
}
for change := range reviewedParameterTypeChanges {
if strings.Count(change.ToolPath, "/") != 1 ||
strings.HasPrefix(change.ToolPath, "/") || strings.HasSuffix(change.ToolPath, "/") {
t.Errorf("tool path %q 不是 \"<product id>/<tool id>\" 形态", change.ToolPath)
}
if strings.TrimSpace(change.Parameter) != change.Parameter || change.Parameter == "" ||
strings.HasPrefix(change.Parameter, "-") {
t.Errorf("%s: 参数名应是 Schema 里的裸名,登记为 %q", change.ToolPath, change.Parameter)
}
if change.From == change.To {
t.Errorf("%s %s: From 与 To 相同,不构成迁移", change.ToolPath, change.Parameter)
}
for _, side := range []struct {
label string
value string
}{{"From", change.From}, {"To", change.To}} {
var name string
if err := json.Unmarshal([]byte(side.value), &name); err != nil {
t.Errorf("%s %s 的 %s=%q 不是合法的 JSON type 值(schemaType 产出带引号的形态,如 `\"string\"`): %v",
change.ToolPath, change.Parameter, side.label, side.value, err)
continue
}
if got := schemaType(map[string]any{"type": name}); got != side.value {
t.Errorf("%s %s 的 %s 不是规范形态\n 登记: %s\n 规范: %s",
change.ToolPath, change.Parameter, side.label, side.value, got)
}
// 复算只能证明引号形态对,证明不了类型名本身没拼错——"integar" 同样
// 能通过复算。JSON Schema 的 type 取值是封闭集合,直接校验。
switch name {
case "string", "integer", "number", "boolean", "array", "object", "null":
default:
t.Errorf("%s %s 的 %s=%q 不是 JSON Schema 的合法 type 名",
change.ToolPath, change.Parameter, side.label, name)
}
}
}
}
func baselineContract() schemaContract {
return schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{
"doc": {Tools: map[string]toolSchema{
@@ -693,6 +926,37 @@ func TestCrossPlatformCoverageSchemaCompatAdditiveConstraintEvolution(t *testing
}
}
func TestCrossPlatformCoverageSchemaCompatReviewedConstraintTransition(t *testing.T) {
const toolPath = "doc/doc.shortcut_import"
oldTool := toolSchema{Constraints: `{"require_one_of":[["folder","workspace"]]}`}
newTool := oldTool
newTool.Constraints = ""
if !compatibleReviewedConstraintTransition(toolPath, oldTool, newTool) {
t.Fatal("reviewed doc import target removal must be accepted")
}
if failures := checkToolCompatibility(toolPath, oldTool, newTool); len(failures) != 0 {
t.Fatalf("reviewed doc import constraint transition failed: %v", failures)
}
for _, test := range []struct {
name string
path string
old string
new string
}{
{name: "unlisted tool", path: "doc/doc.other", old: oldTool.Constraints, new: ""},
{name: "unlisted source", path: toolPath, old: `{"require_one_of":[["folder","workspace","name"]]}`, new: ""},
{name: "unlisted target", path: toolPath, old: oldTool.Constraints, new: `{}`},
} {
t.Run(test.name, func(t *testing.T) {
if compatibleReviewedConstraintTransition(test.path, toolSchema{Constraints: test.old}, toolSchema{Constraints: test.new}) {
t.Fatal("unreviewed constraint transition unexpectedly passed")
}
})
}
}
// Clearing a property through the reviewed mapping exclusion table is the one
// accepted shape, mirroring the interface_type retirement allowance. A leaf
// whose backing RPC moved to a nested payload has no honest flat property to