Files

178 lines
7.0 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package corecmd
import (
"fmt"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
// ContractDecl is the authoring-time leaf contract declaration.
//
// Naming: this is not a Catalog / ToolSpec "Schema" object. Authors declare
// leaf Contract facts (selection / interface / parameters / dry-run /
// identity prose). AttachContract converts once into
// contract.ContractFinalPayload; Catalog assembly pass-throughs that payload.
// Nested fields reuse contract.* types directly so authoring cannot drift from
// the registry model.
//
// Description declare vs delivery (not dual authority, not "declare = wire"):
// - Construction requires Description (declaration evidence / fail-closed).
// - Catalog delivery: Cobra Long wins when present → provenance cobra_help;
// without Long, declared Description is delivered → contract_final.
// - Title: declared Title/ContractFinal first, then Cobra Short, then MCP.
//
// The payload stores the declared text; assembly stamps the real winner.
type ContractDecl struct {
Title string
Description string // required at construction; Catalog may prefer Cobra Long
Positionals []contract.RuntimeSchemaPositional
Parameters []contract.ParamDecl
DryRun *contract.DryRunSpec
Result *contract.ResultSpec
Pagination *contract.PaginationSpec
Interface *contract.InterfaceSpec
Selection contract.SelectionSpec
Identity contract.ToolIdentitySpec
}
// validateContractDecl enforces authoring-time homology for declared commands.
// A declared Contract is the sole final source for its fields: downstream
// catalog/Agent gates hard-require description and the selection prose for
// every effective tool — so a declaration missing any of these fields could
// only fail later, opaquely, in generated artifacts. Failing at construction
// keeps the error next to the authoring mistake and prevents silent drift
// between cobra prose (Short/Long/Example) and the published Contract values.
//
// Identity is required on every declared Contract as the leaf's
// self-description: identity is collected from ContractFinal on the live
// leaves, so an incomplete or inconsistent declared Identity fails collection,
// binding, and policy downstream.
func validateContractDecl(spec Spec) {
if spec.Contract.empty() {
return
}
missing := make([]string, 0, 8)
if strings.TrimSpace(spec.Contract.Description) == "" {
missing = append(missing, "Contract.Description")
}
if strings.TrimSpace(spec.Contract.Selection.AgentSummary) == "" {
missing = append(missing, "Contract.Selection.AgentSummary")
}
if len(spec.Contract.Selection.UseWhen) == 0 {
missing = append(missing, "Contract.Selection.UseWhen")
}
if len(spec.Contract.Selection.AvoidWhen) == 0 {
missing = append(missing, "Contract.Selection.AvoidWhen")
}
if len(spec.Contract.Selection.Examples) == 0 {
missing = append(missing, "Contract.Selection.Examples")
}
// Interface is an unconditional catalog required key for declared tools
// (no hints fallback). Spec.Safety is validated separately and is
// the single source for both runtime confirmation and the Schema block.
if iface := spec.Contract.Interface; iface == nil ||
strings.TrimSpace(iface.Mode) == "" || strings.TrimSpace(iface.Availability) == "" {
missing = append(missing, "Contract.Interface (mode/availability)")
} else if (iface.Mode == contract.InterfaceModeComposite || iface.Availability == contract.InterfaceUnavailable) &&
strings.TrimSpace(iface.Reason) == "" {
missing = append(missing, "Contract.Interface.Reason")
}
id := spec.Contract.Identity
productID := strings.TrimSpace(id.ProductID)
name := strings.TrimSpace(id.Name)
canonical := strings.TrimSpace(id.CanonicalPath)
cliPath := strings.TrimSpace(id.CLIPath)
primary := strings.TrimSpace(id.PrimaryCLIPath)
if productID == "" {
missing = append(missing, "Contract.Identity.ProductID")
}
if name == "" {
missing = append(missing, "Contract.Identity.Name")
}
if canonical == "" {
missing = append(missing, "Contract.Identity.CanonicalPath")
}
if cliPath == "" && primary == "" {
missing = append(missing, "Contract.Identity.CLIPath")
}
if len(missing) > 0 {
panic(fmt.Sprintf(
"command %q declares Contract but is missing %s: a declared Contract is the final source and must carry the full reviewed prose (no hints fallback)",
spec.Use, strings.Join(missing, ", ")))
}
wantCanonical := productID + "." + name
if canonical != wantCanonical {
panic(fmt.Sprintf(
"command %q Contract.Identity.CanonicalPath %q must equal ProductID.Name %q",
spec.Use, canonical, wantCanonical))
}
if cliPath != "" && primary != "" && cliPath != primary {
panic(fmt.Sprintf(
"command %q Contract.Identity.CLIPath %q and PrimaryCLIPath %q must agree on the primary leaf path",
spec.Use, cliPath, primary))
}
}
// Empty reports whether no ContractDecl field was authored.
func (s ContractDecl) Empty() bool {
return s.empty()
}
// empty reports whether no ContractDecl field was authored.
func (s ContractDecl) empty() bool {
if strings.TrimSpace(s.Title) != "" || strings.TrimSpace(s.Description) != "" {
return false
}
if len(s.Positionals) > 0 {
return false
}
if len(s.Parameters) > 0 {
return false
}
if s.DryRun != nil && strings.TrimSpace(s.DryRun.PreviewKind) != "" {
return false
}
if s.Result != nil {
return false
}
if s.Pagination != nil {
return false
}
if s.Interface != nil {
iface := s.Interface
if strings.TrimSpace(iface.Mode) != "" || strings.TrimSpace(iface.Availability) != "" ||
strings.TrimSpace(iface.Reason) != "" || iface.Ref != nil {
return false
}
}
if strings.TrimSpace(s.Selection.AgentSummary) != "" || len(s.Selection.UseWhen) > 0 ||
len(s.Selection.AvoidWhen) > 0 || len(s.Selection.Examples) > 0 ||
len(s.Selection.Prerequisites) > 0 || len(s.Selection.Tips) > 0 ||
len(s.Selection.WorkflowRefs) > 0 {
return false
}
if strings.TrimSpace(s.Identity.ProductID) != "" || strings.TrimSpace(s.Identity.SourceProductID) != "" ||
strings.TrimSpace(s.Identity.Name) != "" || strings.TrimSpace(s.Identity.CLIName) != "" ||
strings.TrimSpace(s.Identity.CanonicalPath) != "" || strings.TrimSpace(s.Identity.Path) != "" ||
strings.TrimSpace(s.Identity.CLIPath) != "" || strings.TrimSpace(s.Identity.PrimaryCLIPath) != "" ||
strings.TrimSpace(s.Identity.Group) != "" || strings.TrimSpace(s.Identity.Source) != "" ||
len(s.Identity.Aliases) > 0 || s.Identity.IsAlias {
return false
}
return true
}