Files
dingtalk-workspace-cli/internal/cli/schema_hint_dirs.go
T
玉澜andCursor 5856a897d1 feat(schema): split human hints into metadata and selection
Own safety/gates/parameters in metadata/ and Agent prose in selection/,
drop the monolithic Manual file, and keep confirmation aligned with
per-tool runtime_gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 13:40:09 +08:00

213 lines
7.2 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 cli
import (
"encoding/json"
"fmt"
"io/fs"
"path/filepath"
"sort"
"strings"
)
const selectionRevisionID = "selection"
type hintDirFile struct {
Version int `json:"version"`
Source hintDirSource `json:"source"`
Products map[string]hintDirProduct `json:"products,omitempty"`
Tools map[string]hintDirTool `json:"tools,omitempty"`
}
type hintDirSource struct {
Kind string `json:"kind"`
Name string `json:"name"`
Reviewed bool `json:"reviewed,omitempty"`
}
type hintDirProduct struct {
AgentSummary string `json:"agent_summary"`
UseWhen []string `json:"use_when"`
AvoidWhen []string `json:"avoid_when"`
Reviewed bool `json:"reviewed"`
ReviewReason string `json:"review_reason"`
SourceRefs []string `json:"source_refs"`
}
type hintDirTool struct {
AgentSummary string `json:"agent_summary,omitempty"`
UseWhen []string `json:"use_when,omitempty"`
AvoidWhen []string `json:"avoid_when,omitempty"`
Examples []string `json:"examples,omitempty"`
Reviewed bool `json:"reviewed,omitempty"`
ReviewReason string `json:"review_reason,omitempty"`
SourceRefs []string `json:"source_refs,omitempty"`
CLIPath string `json:"cli_path,omitempty"`
Parameters map[string]ManualSchemaParameterHint `json:"parameters,omitempty"`
}
func loadManualSchemaHintsFromHintDirs(metadataFS fs.FS, metadataGlob string, selectionFS fs.FS, selectionGlob string) (ManualSchemaHintSnapshot, error) {
commands, err := loadParameterCommandsFromMetadata(metadataFS, metadataGlob)
if err != nil {
return ManualSchemaHintSnapshot{}, err
}
agentHints, err := loadAgentHintsFromSelection(selectionFS, selectionGlob)
if err != nil {
return ManualSchemaHintSnapshot{}, err
}
snapshot := ManualSchemaHintSnapshot{
Schema: manualSchemaHintSchemaRef,
Version: manualSchemaHintVersion,
Commands: commands,
AgentHints: agentHints,
}
if err := ValidateManualAgentHintSet(snapshot.AgentHints, nil, nil); err != nil {
return ManualSchemaHintSnapshot{}, fmt.Errorf("validate selection Agent hints: %w", err)
}
return snapshot, nil
}
func loadParameterCommandsFromMetadata(metadataFS fs.FS, globPattern string) ([]ManualSchemaCommandHint, error) {
files, err := fs.Glob(metadataFS, globPattern)
if err != nil {
return nil, fmt.Errorf("list metadata hints: %w", err)
}
sort.Strings(files)
commands := make([]ManualSchemaCommandHint, 0)
for _, name := range files {
data, err := fs.ReadFile(metadataFS, name)
if err != nil {
return nil, fmt.Errorf("read metadata hint %s: %w", name, err)
}
var file hintDirFile
if err := json.Unmarshal(data, &file); err != nil {
return nil, fmt.Errorf("decode metadata hint %s: %w", name, err)
}
canonicals := make([]string, 0, len(file.Tools))
for canonical := range file.Tools {
canonicals = append(canonicals, canonical)
}
sort.Strings(canonicals)
for _, canonical := range canonicals {
tool := file.Tools[canonical]
if len(tool.Parameters) == 0 {
continue
}
cliPath := strings.TrimSpace(tool.CLIPath)
if cliPath == "" {
return nil, fmt.Errorf("metadata tool %s has parameters but missing cli_path", canonical)
}
reason := strings.TrimSpace(tool.ReviewReason)
if reason == "" {
reason = "reviewed parameter overrides from schema_hints/metadata"
}
commands = append(commands, ManualSchemaCommandHint{
CLIPath: cliPath,
CanonicalPath: canonical,
Reason: reason,
Reviewed: true,
Parameters: tool.Parameters,
})
}
}
return commands, nil
}
// LoadAgentHintsFromSelectionForValidation loads selection HintFiles from an
// FS rooted at schema_hints/selection (so glob *.json).
func LoadAgentHintsFromSelectionForValidation(selectionFS fs.FS) (ManualAgentHintSet, error) {
return loadAgentHintsFromSelection(selectionFS, "*.json")
}
func loadAgentHintsFromSelection(selectionFS fs.FS, globPattern string) (ManualAgentHintSet, error) {
files, err := fs.Glob(selectionFS, globPattern)
if err != nil {
return ManualAgentHintSet{}, fmt.Errorf("list selection hints: %w", err)
}
sort.Strings(files)
hints := ManualAgentHintSet{
Revisions: map[string]ManualAgentHintRevision{
selectionRevisionID: {
GeneratedBy: "human",
Reason: "reviewed Agent selection prose under internal/cli/schema_hints/selection",
},
},
Products: map[string]ManualAgentProductHint{},
Tools: map[string]ManualAgentToolHint{},
}
for _, name := range files {
data, err := fs.ReadFile(selectionFS, name)
if err != nil {
return ManualAgentHintSet{}, fmt.Errorf("read selection hint %s: %w", name, err)
}
var file hintDirFile
if err := json.Unmarshal(data, &file); err != nil {
return ManualAgentHintSet{}, fmt.Errorf("decode selection hint %s: %w", name, err)
}
productIDs := make([]string, 0, len(file.Products))
for productID := range file.Products {
productIDs = append(productIDs, productID)
}
sort.Strings(productIDs)
for _, productID := range productIDs {
product := file.Products[productID]
reason := strings.TrimSpace(product.ReviewReason)
if reason == "" {
reason = "reviewed product selection"
}
evidence := append([]string{}, product.SourceRefs...)
if len(evidence) == 0 {
evidence = []string{"schema_hints/selection/" + filepath.Base(name)}
}
hints.Products[productID] = ManualAgentProductHint{
AgentSummary: product.AgentSummary,
UseWhen: product.UseWhen,
AvoidWhen: product.AvoidWhen,
Reviewed: true,
Revision: selectionRevisionID,
Reason: reason,
Evidence: evidence,
}
}
canonicals := make([]string, 0, len(file.Tools))
for canonical := range file.Tools {
canonicals = append(canonicals, canonical)
}
sort.Strings(canonicals)
for _, canonical := range canonicals {
tool := file.Tools[canonical]
reason := strings.TrimSpace(tool.ReviewReason)
if reason == "" {
reason = "reviewed tool selection"
}
evidence := append([]string{}, tool.SourceRefs...)
if len(evidence) == 0 {
evidence = []string{"schema_hints/selection/" + filepath.Base(name)}
}
hints.Tools[canonical] = ManualAgentToolHint{
AgentSummary: tool.AgentSummary,
UseWhen: tool.UseWhen,
AvoidWhen: tool.AvoidWhen,
Examples: tool.Examples,
Reviewed: true,
Revision: selectionRevisionID,
Reason: reason,
Evidence: evidence,
}
}
}
return hints, nil
}