Schema Catalog now assembles from Contract/ParamDecl/Interface and Cobra only. Keep fetch-mcp-metadata as an optional diagnostic dump and ban the retired pin path. Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
7.5 KiB
Go
211 lines
7.5 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 agentmetadata
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contractfinal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// RegistryProjection is the EffectiveCommandRegistry view required by Generate.
|
|
// Catalog assembly and the optional diagnostic Agent-metadata CLI both build
|
|
// this in-memory; neither path reads schema_agent_metadata/ from disk.
|
|
type RegistryProjection struct {
|
|
ToolPaths map[string]string
|
|
CanonicalToolPaths map[string]string
|
|
ProductIDs map[string]bool
|
|
Hash string
|
|
ToolCount int
|
|
Bound cli.BoundCommandRegistry
|
|
}
|
|
|
|
// ProjectEffectiveRegistry builds the Generate projection from a public
|
|
// Effective registry. Keeping projection below the post-manual boundary
|
|
// prevents a base-registry allowlist from silently dropping reviewed
|
|
// manual-only commands.
|
|
func ProjectEffectiveRegistry(effective cli.EffectiveCommandRegistry) RegistryProjection {
|
|
projection := RegistryProjection{
|
|
ToolPaths: map[string]string{},
|
|
CanonicalToolPaths: map[string]string{},
|
|
ProductIDs: map[string]bool{},
|
|
Hash: effective.SourceHash(),
|
|
}
|
|
for _, command := range effective.Commands {
|
|
if command.Visibility != cli.SchemaVisibilityPublic {
|
|
continue
|
|
}
|
|
projection.ToolCount++
|
|
primary := strings.TrimSpace(command.PrimaryCLIPath)
|
|
projection.ToolPaths[primary] = primary
|
|
projection.ToolPaths[command.CanonicalPath] = primary
|
|
projection.CanonicalToolPaths[command.CanonicalPath] = primary
|
|
if productID, _, ok := strings.Cut(command.CanonicalPath, "."); ok && strings.TrimSpace(productID) != "" {
|
|
projection.ProductIDs[strings.TrimSpace(productID)] = true
|
|
}
|
|
for _, alias := range command.Aliases {
|
|
if alias = strings.TrimSpace(alias); alias != "" {
|
|
projection.ToolPaths[alias] = primary
|
|
}
|
|
}
|
|
}
|
|
return projection
|
|
}
|
|
|
|
// SelectionCoverageTools returns canonical tools that still lack a
|
|
// ContractFinal selection declaration. Declared leaves are exempt.
|
|
func SelectionCoverageTools(projection RegistryProjection) map[string]bool {
|
|
expectedTools := make(map[string]bool, len(projection.CanonicalToolPaths))
|
|
for canonical := range projection.CanonicalToolPaths {
|
|
expectedTools[canonical] = true
|
|
}
|
|
for canonical := range expectedTools {
|
|
bound, ok := projection.Bound.ByCanonical[canonical]
|
|
if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) {
|
|
delete(expectedTools, canonical)
|
|
}
|
|
}
|
|
return expectedTools
|
|
}
|
|
|
|
// SelectionCoverageProducts returns product IDs that still lack a
|
|
// ProductDecl. Declared products are exempt.
|
|
func SelectionCoverageProducts(projection RegistryProjection) map[string]bool {
|
|
if projection.ProductIDs == nil {
|
|
return nil
|
|
}
|
|
expected := make(map[string]bool, len(projection.ProductIDs))
|
|
for productID, include := range projection.ProductIDs {
|
|
if !include {
|
|
continue
|
|
}
|
|
productID = strings.TrimSpace(productID)
|
|
if productID == "" || contract.HasProductDecl(productID) {
|
|
continue
|
|
}
|
|
expected[productID] = true
|
|
}
|
|
return expected
|
|
}
|
|
|
|
// ValidateSelectionCoverage requires every projected product to have
|
|
// ProductDecl and every projected tool to have ContractFinal. Retired
|
|
// schema_hints/ overlays are not accepted as coverage.
|
|
func ValidateSelectionCoverage(projection RegistryProjection) error {
|
|
expectedTools := SelectionCoverageTools(projection)
|
|
expectedProducts := SelectionCoverageProducts(projection)
|
|
if !selectionCoverageRequired(expectedProducts, expectedTools) {
|
|
return nil
|
|
}
|
|
missingProducts := make([]string, 0)
|
|
for productID, include := range expectedProducts {
|
|
if include {
|
|
missingProducts = append(missingProducts, productID)
|
|
}
|
|
}
|
|
missingTools := make([]string, 0)
|
|
for tool, include := range expectedTools {
|
|
if include {
|
|
missingTools = append(missingTools, tool)
|
|
}
|
|
}
|
|
sort.Strings(missingProducts)
|
|
sort.Strings(missingTools)
|
|
return fmt.Errorf("ProductDecl/ContractFinal selection coverage incomplete: missing_products=%v missing_tools=%v", missingProducts, missingTools)
|
|
}
|
|
|
|
var (
|
|
pipelineBuildEffectiveRegistry = cli.BuildEffectiveCommandRegistry
|
|
pipelineBindEffectiveRegistry = cli.BindEffectiveCommandRegistry
|
|
pipelineGenerateMetadata = Generate
|
|
)
|
|
|
|
func selectionCoverageRequired(expectedProducts, expectedTools map[string]bool) bool {
|
|
for _, include := range expectedProducts {
|
|
if include {
|
|
return true
|
|
}
|
|
}
|
|
for _, include := range expectedTools {
|
|
if include {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GenerateFromCommandRoot is the Catalog in-memory Agent-metadata pipeline.
|
|
// It binds the EffectiveCommandRegistry, validates that ProductDecl/ContractFinal
|
|
// cover selection, and Generate()s metadata without reading or writing
|
|
// schema_agent_metadata/ or schema_hints/.
|
|
func GenerateFromCommandRoot(rootPath string, commandRoot *cobra.Command, opts Options) (File, Stats, RegistryProjection, error) {
|
|
if commandRoot == nil {
|
|
return File{}, Stats{}, RegistryProjection{}, fmt.Errorf("schema source root is nil")
|
|
}
|
|
rootPath = strings.TrimSpace(rootPath)
|
|
if rootPath == "" {
|
|
rootPath = "."
|
|
}
|
|
effective, err := pipelineBuildEffectiveRegistry(commandRoot)
|
|
if err != nil {
|
|
return File{}, Stats{}, RegistryProjection{}, fmt.Errorf("build effective CommandRegistry for Agent metadata: %w", err)
|
|
}
|
|
bound, err := pipelineBindEffectiveRegistry(commandRoot, effective)
|
|
if err != nil {
|
|
return File{}, Stats{}, RegistryProjection{}, fmt.Errorf("bind effective CommandRegistry for Agent metadata: %w", err)
|
|
}
|
|
projection := ProjectEffectiveRegistry(effective)
|
|
projection.Bound = bound
|
|
|
|
opts.Root = rootPath
|
|
if strings.TrimSpace(opts.SkillPath) == "" {
|
|
opts.SkillPath = "skills/mono/SKILL.md"
|
|
}
|
|
if strings.TrimSpace(opts.ProductsDir) == "" {
|
|
opts.ProductsDir = "skills/mono/references/products"
|
|
}
|
|
if strings.TrimSpace(opts.IntentGuidePath) == "" {
|
|
opts.IntentGuidePath = "skills/mono/references/intent-guide.md"
|
|
}
|
|
// HintsDir must stay empty: schema_hints/ is retired and non-empty values fail closed.
|
|
// InterfaceMetadataPath is optional diagnostic input only. The committed
|
|
// schema_mcp_metadata.json pin is retired; leave empty to skip fallback.
|
|
if opts.MaxExamples <= 0 {
|
|
opts.MaxExamples = 2
|
|
}
|
|
if opts.MaxInterfaceSummaryRunes <= 0 {
|
|
opts.MaxInterfaceSummaryRunes = 120
|
|
}
|
|
opts.ToolPaths = projection.ToolPaths
|
|
opts.CanonicalToolPaths = projection.CanonicalToolPaths
|
|
opts.BoundCommands = projection.Bound
|
|
opts.ProductIDs = projection.ProductIDs
|
|
opts.SurfaceHash = projection.Hash
|
|
opts.SurfaceToolCount = projection.ToolCount
|
|
|
|
if err := ValidateSelectionCoverage(projection); err != nil {
|
|
return File{}, Stats{}, RegistryProjection{}, err
|
|
}
|
|
metadata, stats, err := pipelineGenerateMetadata(opts)
|
|
if err != nil {
|
|
return File{}, Stats{}, RegistryProjection{}, fmt.Errorf("generate in-memory Agent metadata: %w", err)
|
|
}
|
|
return metadata, stats, projection, nil
|
|
}
|