// 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 ( "bytes" "encoding/json" "fmt" "reflect" "sort" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" ) // SchemaRegistry is the resolved, typed Agent contract. Source adapters and // precedence resolvers populate this model; wire payloads are rendered only at // the outer boundary by ToPayload. This keeps map[string]any out of the // contract assembly path. type SchemaRegistry struct { Kind string Level string Source string Products []ProductSpec AgentMetadata json.RawMessage } // ProductSpec is one deterministic product grouping in SchemaRegistry. type ProductSpec struct { ID string Name string Description string Runtime bool Tools []ToolSpec Selection contract.SelectionSpec FieldProvenance map[string]contract.FieldProvenance } // ToolSpec is the single assembled representation of a command contract. // Safety, interface and selection are typed sub-models even though ToPayload // preserves the historical flat JSON shape for compatibility. type ToolSpec struct { Identity contract.ToolIdentitySpec Display string Title string Description string MetadataSource string Parameters []ParameterSpec Constraints RuntimeSchemaConstraints Positionals []contract.RuntimeSchemaPositional DryRun *contract.DryRunSpec Result *contract.ResultSpec Pagination *contract.PaginationSpec Safety contract.SafetySpec Interface contract.InterfaceSpec Selection contract.SelectionSpec FieldProvenance map[string]contract.FieldProvenance } // ParameterSpec is one resolved CLI flag projection. Name is the CLI flag key // used in the legacy parameters object. Default and Example are RawMessage // because JSON literals are polymorphic; they are still typed JSON values and // never participate in precedence as map[string]any. type ParameterSpec struct { Name string Type string Description string Property string Required bool CLIRequired bool RequiredWhen string Default json.RawMessage InterfaceDefault json.RawMessage Example json.RawMessage Format string Enum []string InterfaceDescription string InterfaceType string FieldProvenance map[string]contract.FieldProvenance } // Final provenance coverage is an explicit contract, not a truthiness check. // These fields all pass through a precedence resolver and therefore must keep // a winner even when the delivered value is false, "", [], or null. var requiredToolProvenanceFields = [...]string{ "canonical_path", "effect", "risk", "confirmation", "idempotency", "interface_ref", "interface_mode", "availability", "agent_summary", } var conditionalSelectionProvenanceFields = [...]string{ "use_when", "avoid_when", "prerequisites", "tips", "workflow_refs", "examples", } var requiredParameterProvenanceFields = [...]string{ "property", "type", "description", "required", "required_when", } // RuntimeToolSpecInput is the typed hand-off from runtime source resolution to // contract assembly. Adapters resolve candidates first, then call // ToolSpecFromRuntime exactly once; payload rendering does no further merging. type RuntimeToolSpecInput struct { Identity contract.ToolIdentitySpec Display string Title string Description string MetadataSource string Parameters []ParameterSpec Constraints RuntimeSchemaConstraints Positionals []contract.RuntimeSchemaPositional DryRun *contract.DryRunSpec Result *contract.ResultSpec Pagination *contract.PaginationSpec Safety contract.SafetySpec Interface contract.InterfaceSpec Selection contract.SelectionSpec FieldProvenance map[string]contract.FieldProvenance } // SchemaIndex is the immutable navigation view over one normalized registry. // It mirrors Lark's typed catalog split: SchemaRegistry owns data, SchemaIndex // owns deterministic identity/path resolution, and serializers only render a // resolved ToolSpec. type SchemaIndex struct { registry SchemaRegistry byProduct map[string]int byCanonical map[string]schemaToolLocation byToolPath map[string]string byCLIPath map[string]string } type schemaToolLocation struct { product int tool int } // SchemaSnapshotPayload is the final wire boundary for the release snapshot. // Catalog summaries and full Tools are both derived from the same typed // ToolSpec instances, preventing the two snapshot layers from drifting. type SchemaSnapshotPayload struct { Catalog map[string]any Tools map[string]map[string]any } var ( snapshotToolSummary = ToolSpec.ToSummaryPayload snapshotToolPayload = ToolSpec.ToPayload snapshotProductSummary = ProductSpec.ToSummaryPayload ) // SchemaRegistryFromRuntime constructs and validates the one registry shared // by leaf lookup, Catalog summaries and full export. func SchemaRegistryFromRuntime(source string, products []ProductSpec) (SchemaRegistry, error) { registry := SchemaRegistry{ Kind: "schema", Level: "catalog", Source: strings.TrimSpace(source), Products: products, }.Sorted() if _, err := registry.Index(); err != nil { return SchemaRegistry{}, err } return registry, nil } // ToolSpecFromRuntime validates and normalizes the fully resolved runtime // input. It does not choose between sources; that is the resolver's job. // Snapshot loading shares this path and validates a delivered winner exactly // as serialized: it deliberately does not repair provenance, so a snapshot // whose winner differs from the final field must fail closed. func ToolSpecFromRuntime(input RuntimeToolSpecInput) (ToolSpec, error) { spec := ToolSpec(input).normalized() if err := spec.Validate(); err != nil { return ToolSpec{}, err } return spec, nil } func (t ToolSpec) provenanceValue(field string) (any, bool) { switch field { case "canonical_path": return t.Identity.CanonicalPath, true case "title": return t.Title, true case "description": return t.Description, true case "metadata_source": return t.MetadataSource, true case "dry_run": return t.DryRun, true case "effect": return t.Safety.Effect, true case "effect_source": return t.Safety.EffectSource, true case "risk": return t.Safety.Risk, true case "confirmation": return t.Safety.Confirmation, true case "idempotency": return t.Safety.Idempotency, true case "interface_ref": return t.Interface.Ref, true case "interface_mode": return t.Interface.Mode, true case "availability": return t.Interface.Availability, true case "interface_reason": return t.Interface.Reason, true case "agent_summary": return t.Selection.AgentSummary, true case "use_when": return t.Selection.UseWhen, true case "avoid_when": return t.Selection.AvoidWhen, true case "prerequisites": return t.Selection.Prerequisites, true case "tips": return t.Selection.Tips, true case "workflow_refs": return t.Selection.WorkflowRefs, true case "examples": return t.Selection.Examples, true case "reviewed": return t.Selection.Reviewed, true default: return nil, false } } func (p ParameterSpec) provenanceValue(field string) (any, bool) { switch field { case "name": return p.Name, true case "type": return p.Type, true case "description": return p.Description, true case "property": return p.Property, true case "required": return p.Required, true case "cli_required": return p.CLIRequired, true case "required_when": return p.RequiredWhen, true case "default": return p.Default, true case "interface_default": return p.InterfaceDefault, true case "example": return p.Example, true case "format": return p.Format, true case "enum": return p.Enum, true case "interface_description": return p.InterfaceDescription, true case "interface_type": return p.InterfaceType, true default: return nil, false } } // Index validates all registry identities and returns deterministic navigation // indexes. Canonical paths, primary CLI paths and reviewed aliases must each // resolve unambiguously. func (r SchemaRegistry) Index() (SchemaIndex, error) { r = r.Sorted() index := SchemaIndex{ registry: r, byProduct: make(map[string]int, len(r.Products)), byCanonical: make(map[string]schemaToolLocation), byToolPath: make(map[string]string), byCLIPath: make(map[string]string), } for productIndex, product := range r.Products { if product.ID == "" { return SchemaIndex{}, fmt.Errorf("schema product id is empty") } if _, exists := index.byProduct[product.ID]; exists { return SchemaIndex{}, fmt.Errorf("duplicate schema product %q", product.ID) } index.byProduct[product.ID] = productIndex for field, provenance := range product.FieldProvenance { if value, ok := product.provenanceValue(field); ok { if err := validateFinalFieldProvenance("product "+product.ID, field, provenance, value); err != nil { return SchemaIndex{}, err } } } for toolIndex, tool := range product.Tools { if err := tool.Validate(); err != nil { return SchemaIndex{}, err } if err := validateCanonicalToolIdentity(tool); err != nil { return SchemaIndex{}, err } if tool.Identity.ProductID != product.ID { return SchemaIndex{}, fmt.Errorf("tool %s belongs to product %q, not containing product %q", tool.Identity.CanonicalPath, tool.Identity.ProductID, product.ID) } canonical := tool.Identity.CanonicalPath if _, exists := index.byCanonical[canonical]; exists { return SchemaIndex{}, fmt.Errorf("duplicate schema tool %q", canonical) } if existing, exists := index.byToolPath[canonical]; exists && existing != canonical { return SchemaIndex{}, fmt.Errorf("canonical path %q conflicts with contract path for %s", canonical, existing) } index.byCanonical[canonical] = schemaToolLocation{product: productIndex, tool: toolIndex} toolPaths := []string{tool.Identity.Path} if tool.Identity.SourceProductID != "" && tool.Identity.SourceProductID != tool.Identity.ProductID { toolPaths = append(toolPaths, tool.Identity.SourceProductID+"."+tool.Identity.Name) } for _, path := range sortedUniqueStrings(toolPaths) { if path == canonical { continue } if existing, exists := index.byToolPath[path]; exists && existing != canonical { return SchemaIndex{}, fmt.Errorf("schema contract path %q resolves to both %s and %s", path, existing, canonical) } if _, exists := index.byCanonical[path]; exists { return SchemaIndex{}, fmt.Errorf("schema contract path %q conflicts with a canonical path", path) } index.byToolPath[path] = canonical } paths := append([]string{tool.Identity.CLIPath, tool.Identity.PrimaryCLIPath}, tool.Identity.Aliases...) for _, path := range sortedUniqueStrings(paths) { path = normalizeSchemaCLIPath(path) if existing, exists := index.byCLIPath[path]; exists && existing != canonical { return SchemaIndex{}, fmt.Errorf("schema CLI path %q resolves to both %s and %s", path, existing, canonical) } index.byCLIPath[path] = canonical } } } return index, nil } // validateCanonicalToolIdentity applies only to ToolSpecs stored in the // registry. Alias lookup creates a detached query view later, where cli_path // and is_alias intentionally change; that projection is never allowed back // into SchemaRegistry, --all, or the delivered Catalog. func validateCanonicalToolIdentity(tool ToolSpec) error { id := tool.Identity canonical := strings.TrimSpace(id.CanonicalPath) cliPath := normalizeSchemaCLIPath(id.CLIPath) primaryCLIPath := normalizeSchemaCLIPath(id.PrimaryCLIPath) if cliPath != primaryCLIPath { return fmt.Errorf("canonical Schema tool %s cli_path %q must equal primary_cli_path %q", canonical, cliPath, primaryCLIPath) } if id.IsAlias { return fmt.Errorf("canonical Schema tool %s must have is_alias=false", canonical) } return nil } // Registry returns the detached normalized registry owned by this index. func (i SchemaIndex) Registry() SchemaRegistry { return i.registry } // Product resolves one product by exact stable ID. func (i SchemaIndex) Product(id string) (ProductSpec, bool) { position, ok := i.byProduct[strings.TrimSpace(id)] if !ok { return ProductSpec{}, false } return i.registry.Products[position], true } // Resolve resolves either an exact canonical path or a normalized CLI path. // No inference or fuzzy matching occurs in this layer. func (i SchemaIndex) Resolve(path string) (ToolSpec, bool) { path = strings.TrimSpace(path) canonical := path location, ok := i.byCanonical[canonical] if !ok { canonical, ok = i.byToolPath[path] } if !ok { canonical, ok = i.byCLIPath[normalizeSchemaCLIPath(path)] if !ok { return ToolSpec{}, false } } location = i.byCanonical[canonical] return i.registry.Products[location.product].Tools[location.tool], true } // ResolveQuery adds compatibility for dotted and slash-separated CLI paths at // the user-facing query boundary. Resolve remains strict because Registry // validation uses it to detect missing canonical identities without falling // through to a similarly spelled CLI path. func (i SchemaIndex) ResolveQuery(path string) (ToolSpec, bool) { if tool, ok := i.Resolve(path); ok { return tool, true } canonical, ok := i.byCLIPath[normalizeSchemaQueryCLIPath(path)] if !ok { return ToolSpec{}, false } location := i.byCanonical[canonical] return i.registry.Products[location.product].Tools[location.tool], true } // CanonicalPaths returns the complete tool identity set in stable order. func (i SchemaIndex) CanonicalPaths() []string { paths := make([]string, 0, len(i.byCanonical)) for path := range i.byCanonical { paths = append(paths, path) } sort.Strings(paths) return paths } // ToSnapshotPayload derives the progressive Catalog and full tool map from one // normalized registry. This is the direct typed replacement for separately // assembling runtimeToolSummary and runtimeToolPayload. func (r SchemaRegistry) ToSnapshotPayload() (SchemaSnapshotPayload, error) { index, err := r.Index() if err != nil { return SchemaSnapshotPayload{}, err } r = index.Registry() products := make([]map[string]any, 0, len(r.Products)) tools := make(map[string]map[string]any, len(index.byCanonical)) for _, product := range r.Products { summaries := make([]map[string]any, 0, len(product.Tools)) for _, tool := range product.Tools { summary, summaryErr := snapshotToolSummary(tool) if summaryErr != nil { return SchemaSnapshotPayload{}, summaryErr } full, fullErr := snapshotToolPayload(tool) if fullErr != nil { return SchemaSnapshotPayload{}, fullErr } summaries = append(summaries, summary) tools[tool.Identity.CanonicalPath] = full } productPayload, productErr := snapshotProductSummary(product) if productErr != nil { return SchemaSnapshotPayload{}, productErr } productPayload["tools"] = summaries productPayload["tool_count"] = len(summaries) products = append(products, productPayload) } catalog, err := r.registryEnvelopePayload(products, len(tools)) if err != nil { return SchemaSnapshotPayload{}, err } return SchemaSnapshotPayload{Catalog: catalog, Tools: tools}, nil } // registryEnvelopePayload renders the shared Catalog envelope around prebuilt // product payloads. The snapshot catalog and schema --all projection both use // it, so the envelope keys cannot drift between the two views. func (r SchemaRegistry) registryEnvelopePayload(products []map[string]any, toolCount int) (map[string]any, error) { payload := map[string]any{} payload["kind"] = defaultString(r.Kind, "schema") payload["level"] = defaultString(r.Level, "catalog") payload["count"] = len(products) payload["tool_count"] = toolCount payload["products"] = products if r.Source != "" { payload["source"] = r.Source } if err := putRawJSON(payload, "agent_metadata", r.AgentMetadata); err != nil { return nil, fmt.Errorf("agent_metadata: %w", err) } return payload, nil } // Validate checks structural invariants only. It intentionally does not // constrain precedence outcomes such as risk or required values. func (t ToolSpec) Validate() error { id := t.Identity if id.ProductID == "" { return fmt.Errorf("tool product_id is empty") } if id.Name == "" { return fmt.Errorf("tool name is empty") } wantCanonical := id.ProductID + "." + id.Name if id.CanonicalPath != wantCanonical { return fmt.Errorf("tool canonical_path %q does not match %q", id.CanonicalPath, wantCanonical) } if id.CLIPath == "" { return fmt.Errorf("tool %s cli_path is empty", id.CanonicalPath) } seen := make(map[string]bool, len(t.Parameters)) for _, parameter := range t.Parameters { if parameter.Name == "" { return fmt.Errorf("tool %s has parameter with empty name", id.CanonicalPath) } if seen[parameter.Name] { return fmt.Errorf("tool %s has duplicate parameter %q", id.CanonicalPath, parameter.Name) } seen[parameter.Name] = true if err := validateRawJSON(parameter.Default, "default", id.CanonicalPath, parameter.Name); err != nil { return err } if err := validateRawJSON(parameter.InterfaceDefault, "interface_default", id.CanonicalPath, parameter.Name); err != nil { return err } if err := validateRawJSON(parameter.Example, "example", id.CanonicalPath, parameter.Name); err != nil { return err } } if t.Interface.Ref != nil { if strings.TrimSpace(t.Interface.Ref.ProductID) == "" || strings.TrimSpace(t.Interface.Ref.RPCName) == "" { return fmt.Errorf("tool %s has incomplete interface_ref", id.CanonicalPath) } } if t.DryRun != nil { if err := t.DryRun.Validate(id.CanonicalPath); err != nil { return err } } if t.Result != nil { if _, err := contract.NormalizeResultSpec(t.Result, id.CanonicalPath); err != nil { return err } } if t.Pagination != nil { pagination, err := contract.NormalizePaginationSpec(t.Pagination, id.CanonicalPath) if err != nil { return err } if !seen[pagination.CursorParameter] { return fmt.Errorf("tool %s pagination cursor_parameter %q is not a declared parameter", id.CanonicalPath, pagination.CursorParameter) } } if t.Interface.Mode != "" || t.Interface.Availability != "" || t.Interface.Reason != "" || t.Interface.Ref != nil { if err := t.Interface.Validate(id.CanonicalPath); err != nil { return err } } for field, provenance := range t.FieldProvenance { if value, ok := t.provenanceValue(field); ok { if err := validateFinalFieldProvenance(id.CanonicalPath, field, provenance, value); err != nil { return err } } } for _, parameter := range t.Parameters { for field, provenance := range parameter.FieldProvenance { if value, ok := parameter.provenanceValue(field); ok { if err := validateFinalFieldProvenance(id.CanonicalPath+" parameter "+parameter.Name, field, provenance, value); err != nil { return err } } } } return nil } func validateFinalFieldProvenance(owner, field string, provenance contract.FieldProvenance, finalValue any) error { expected, err := json.Marshal(finalValue) if err != nil { return fmt.Errorf("%s field %s cannot encode final provenance value: %w", owner, field, err) } if len(provenance.Value) == 0 || !json.Valid(provenance.Value) || !equalJSONValues(provenance.Value, expected) { return fmt.Errorf("%s field %s provenance winner does not equal final value: winner=%s final=%s", owner, field, string(provenance.Value), string(expected)) } if strings.TrimSpace(provenance.Source) == "" || strings.TrimSpace(provenance.Precedence) == "" || strings.TrimSpace(provenance.Resolution) == "" { return fmt.Errorf("%s field %s provenance winner is incomplete", owner, field) } if len(provenance.Candidates) == 0 { return fmt.Errorf("%s field %s provenance has no candidates", owner, field) } selected := 0 for _, candidate := range provenance.Candidates { if len(candidate.Value) == 0 || !json.Valid(candidate.Value) { return fmt.Errorf("%s field %s has a provenance candidate with invalid value", owner, field) } if candidate.Selected == nil || !*candidate.Selected { continue } selected++ if !json.Valid(candidate.Value) || !equalJSONValues(candidate.Value, expected) { return fmt.Errorf("%s field %s selected provenance candidate does not equal final value", owner, field) } if strings.TrimSpace(candidate.Source) != strings.TrimSpace(provenance.Source) || strings.TrimSpace(candidate.Precedence) != strings.TrimSpace(provenance.Precedence) { return fmt.Errorf("%s field %s selected provenance candidate disagrees with winner", owner, field) } } for _, candidate := range provenance.OverriddenCandidates { if len(candidate.Value) == 0 || !json.Valid(candidate.Value) { return fmt.Errorf("%s field %s has an overridden provenance candidate with invalid value", owner, field) } if candidate.Selected != nil && *candidate.Selected { return fmt.Errorf("%s field %s has a selected overridden provenance candidate", owner, field) } } if selected != 1 { return fmt.Errorf("%s field %s provenance has %d selected candidates, want 1", owner, field, selected) } return nil } // equalJSONValues compares decoded JSON types and values while preserving the // lexical representation of numbers through json.Number. Formatting and // equivalent string escapes are intentionally ignored; 1 and 1.0 remain // distinct provenance values. func equalJSONValues(left, right []byte) bool { // Generated Schema values are already canonical JSON in the common path. // Avoid allocating two decoded object graphs for identical bytes; keep the // semantic decoder fallback for whitespace, escape, and representation // differences. json.Valid preserves the previous behavior for equal but // malformed input. if bytes.Equal(left, right) { return json.Valid(left) } decode := func(raw []byte) (any, bool) { if !json.Valid(raw) { return nil, false } decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var value any _ = decoder.Decode(&value) return value, true } leftValue, leftOK := decode(left) rightValue, rightOK := decode(right) return leftOK && rightOK && reflect.DeepEqual(leftValue, rightValue) } func validateRawJSON(value json.RawMessage, field, canonical, parameter string) error { if len(value) == 0 || json.Valid(value) { return nil } return fmt.Errorf("tool %s parameter %q has invalid JSON %s", canonical, parameter, field) } // Sorted returns a detached, deterministic registry. Products are ordered by // ID, tools by canonical path then CLI path, and parameters by CLI flag name. func (r SchemaRegistry) Sorted() SchemaRegistry { out := r out.Products = append([]ProductSpec(nil), r.Products...) for i := range out.Products { out.Products[i] = out.Products[i].normalized() } sort.Slice(out.Products, func(i, j int) bool { return out.Products[i].ID < out.Products[j].ID }) return out } func (p ProductSpec) normalized() ProductSpec { out := p out.ID = strings.TrimSpace(out.ID) out.Name = strings.TrimSpace(out.Name) out.Tools = append([]ToolSpec(nil), p.Tools...) for i := range out.Tools { out.Tools[i] = out.Tools[i].normalized() } sort.Slice(out.Tools, func(i, j int) bool { left, right := out.Tools[i].Identity, out.Tools[j].Identity if left.CanonicalPath != right.CanonicalPath { return left.CanonicalPath < right.CanonicalPath } return left.CLIPath < right.CLIPath }) out.Selection = out.Selection.Normalized() out.FieldProvenance = cloneFieldProvenance(out.FieldProvenance) return out } func (p ProductSpec) provenanceValue(field string) (any, bool) { switch field { case "agent_summary": return p.Selection.AgentSummary, true case "use_when": return p.Selection.UseWhen, true case "avoid_when": return p.Selection.AvoidWhen, true default: return nil, false } } func (t ToolSpec) normalized() ToolSpec { out := t id := out.Identity id.ProductID = strings.TrimSpace(id.ProductID) id.SourceProductID = strings.TrimSpace(id.SourceProductID) id.Name = strings.TrimSpace(id.Name) id.CLIName = strings.TrimSpace(id.CLIName) id.CanonicalPath = strings.TrimSpace(id.CanonicalPath) if id.CanonicalPath == "" && id.ProductID != "" && id.Name != "" { id.CanonicalPath = id.ProductID + "." + id.Name } id.Path = strings.TrimSpace(id.Path) if id.Path == "" { id.Path = id.CanonicalPath } id.CLIPath = normalizeSchemaCLIPath(id.CLIPath) id.PrimaryCLIPath = normalizeSchemaCLIPath(id.PrimaryCLIPath) if id.PrimaryCLIPath == "" { id.PrimaryCLIPath = id.CLIPath } id.Group = strings.TrimSpace(id.Group) id.Source = strings.TrimSpace(id.Source) id.Aliases = sortedUniqueStrings(id.Aliases) out.Identity = id out.Parameters = append([]ParameterSpec(nil), t.Parameters...) for i := range out.Parameters { out.Parameters[i] = out.Parameters[i].normalized() } sort.Slice(out.Parameters, func(i, j int) bool { return out.Parameters[i].Name < out.Parameters[j].Name }) out.Constraints = normalizeRuntimeSchemaConstraints(t.Constraints) if t.DryRun != nil { dryRun := *t.DryRun dryRun.PreviewKind = strings.TrimSpace(dryRun.PreviewKind) out.DryRun = &dryRun } if t.Result != nil { result, err := contract.NormalizeResultSpec(t.Result, id.CanonicalPath) if err == nil { out.Result = result } } if t.Pagination != nil { pagination, err := contract.NormalizePaginationSpec(t.Pagination, id.CanonicalPath) if err == nil { out.Pagination = pagination } } out.Positionals = append([]contract.RuntimeSchemaPositional(nil), t.Positionals...) sort.Slice(out.Positionals, func(i, j int) bool { if out.Positionals[i].Index != out.Positionals[j].Index { return out.Positionals[i].Index < out.Positionals[j].Index } return out.Positionals[i].Name < out.Positionals[j].Name }) out.Selection = out.Selection.Normalized() return out } func (p ParameterSpec) normalized() ParameterSpec { out := p out.Name = strings.TrimSpace(out.Name) out.Type = strings.TrimSpace(out.Type) out.Property = strings.TrimSpace(out.Property) out.Enum = stableUniqueStrings(out.Enum) return out } func stableUniqueStrings(values []string) []string { if values == nil { return nil } seen := make(map[string]bool, len(values)) out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" || seen[value] { continue } seen[value] = true out = append(out, value) } return out } // cloneOptionalStrings preserves the semantic difference between an omitted // list (nil) and an explicitly authored empty list (non-nil, length zero). // Selection precedence treats [] as a real winner, so that presence must // survive the generated metadata adapter and every typed projection. func cloneOptionalStrings(values []string) []string { if values == nil { return nil } out := make([]string, len(values)) copy(out, values) return out } func sortedUniqueStrings(values []string) []string { seen := make(map[string]bool, len(values)) out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" || seen[value] { continue } seen[value] = true out = append(out, value) } sort.Strings(out) if len(out) == 0 { return nil } return out } // ToPayload renders the compatible flat schema payload after deterministically // sorting the typed model. func (r SchemaRegistry) ToPayload() (map[string]any, error) { index, err := r.Index() if err != nil { return nil, err } r = index.Registry() products := make([]map[string]any, 0, len(r.Products)) toolCount := 0 for _, product := range r.Products { payload, err := product.ToPayload() if err != nil { return nil, err } products = append(products, payload) toolCount += len(product.Tools) } return r.registryEnvelopePayload(products, toolCount) } // ToOverviewPayload renders the small first-hop product index directly from // the resolved registry. It is a presentation projection only: it never reads // Agent metadata or any other source after assembly. func (r SchemaRegistry) ToOverviewPayload() (map[string]any, error) { index, err := r.Index() if err != nil { return nil, err } r = index.Registry() products := make([]map[string]any, 0, len(r.Products)) toolCount := 0 for _, product := range r.Products { entry := map[string]any{ "id": product.ID, "tool_count": len(product.Tools), "schema_path": product.ID, } if summary := strings.TrimSpace(product.Selection.AgentSummary); summary != "" { entry["agent_summary"] = summary } else if len(product.Selection.UseWhen) > 0 { entry["use_when"] = []string{product.Selection.UseWhen[0]} } else if product.Description != "" { entry["description"] = product.Description } products = append(products, entry) toolCount += len(product.Tools) } payload := map[string]any{ "kind": defaultString(r.Kind, "schema"), "level": "products", "count": len(products), "tool_count": toolCount, "products": products, } if r.Source != "" { payload["source"] = r.Source } if err := putRawJSON(payload, "agent_metadata", r.AgentMetadata); err != nil { return nil, fmt.Errorf("agent_metadata: %w", err) } return payload, nil } // ToPayload renders one product and its full tools. func (p ProductSpec) ToPayload() (map[string]any, error) { return p.envelopePayload(ToolSpec.ToPayload) } // ToSummaryPayload renders one product with progressive tool summaries. Both // this view and the full product payload are projections of the same ToolSpec // slice; neither re-resolves annotations or metadata. func (p ProductSpec) ToSummaryPayload() (map[string]any, error) { return p.envelopePayload(ToolSpec.ToSummaryPayload) } // envelopePayload renders the product envelope around tools rendered by // renderTool. The full and summary product payloads differ only in the tool // renderer, which keeps the envelope keys byte-identical across projections. func (p ProductSpec) envelopePayload(renderTool func(ToolSpec) (map[string]any, error)) (map[string]any, error) { p = p.normalized() tools := make([]map[string]any, 0, len(p.Tools)) for _, tool := range p.Tools { payload, err := renderTool(tool) if err != nil { return nil, err } tools = append(tools, payload) } payload := map[string]any{} payload["id"] = p.ID payload["name"] = p.Name payload["description"] = p.Description payload["tool_count"] = len(tools) payload["tools"] = tools if p.Runtime { payload["runtime"] = true } applySelectionPayload(payload, p.Selection, false) if len(p.FieldProvenance) > 0 { value, valueErr := typedJSONValue(p.FieldProvenance) if valueErr != nil { return nil, valueErr } payload["field_provenance"] = value } return payload, nil } // ToPayload renders one full leaf contract in the existing flat JSON shape. func (t ToolSpec) ToPayload() (map[string]any, error) { t = t.normalized() if err := t.Validate(); err != nil { return nil, err } payload := map[string]any{} id := t.Identity payload["name"] = id.Name payload["cli_name"] = id.CLIName payload["canonical_path"] = id.CanonicalPath payload["path"] = id.Path payload["cli_path"] = id.CLIPath payload["primary_cli_path"] = id.PrimaryCLIPath payload["is_alias"] = id.IsAlias payload["source"] = id.Source payload["product_id"] = id.ProductID payload["display"] = t.Display payload["title"] = t.Title payload["description"] = t.Description setOptionalString(payload, "source_product_id", id.SourceProductID) setOptionalString(payload, "group", id.Group) setOptionalString(payload, "metadata_source", t.MetadataSource) if len(id.Aliases) > 0 { payload["aliases"] = append([]string(nil), id.Aliases...) } parameters := make(map[string]any, len(t.Parameters)) for _, parameter := range t.Parameters { entry, parameterErr := parameter.ToPayload() if parameterErr != nil { return nil, fmt.Errorf("render %s parameter %s: %w", id.CanonicalPath, parameter.Name, parameterErr) } parameters[parameter.Name] = entry } payload["parameters"] = parameters payload["has_parameters"] = len(parameters) > 0 payload["parameter_count"] = len(parameters) if !runtimeSchemaConstraintsEmpty(t.Constraints) { value, _ := typedJSONValue(t.Constraints) payload["constraints"] = value } if len(t.Positionals) > 0 { value, _ := typedJSONValue(t.Positionals) payload["positionals"] = value } if t.DryRun != nil { value, _ := typedJSONValue(t.DryRun) payload["dry_run"] = value } if t.Result != nil { value, _ := typedJSONValue(t.Result) payload["result"] = value } if t.Pagination != nil { value, _ := typedJSONValue(t.Pagination) payload["pagination"] = value } applySafetyPayload(payload, t.Safety) applyInterfacePayload(payload, t.Interface) applySelectionPayload(payload, t.Selection, true) if len(t.FieldProvenance) > 0 { value, valueErr := typedJSONValue(t.FieldProvenance) if valueErr != nil { return nil, valueErr } payload["field_provenance"] = value } return payload, nil } // ToSummaryPayload projects a ToolSpec without parameters and leaf-only // execution detail. It is derived from the same typed ToolSpec, never assembled // independently. func (t ToolSpec) ToSummaryPayload() (map[string]any, error) { payload, err := t.ToPayload() if err != nil { return nil, err } for _, key := range []string{ "parameters", "has_parameters", "parameter_count", "constraints", "positionals", "result", "examples", "effect_source", "agent_source_refs", "field_provenance", "path", "source", "product_id", "display", "is_alias", } { delete(payload, key) } return payload, nil } // ToPayload renders one parameter in the existing parameters. shape. func (p ParameterSpec) ToPayload() (map[string]any, error) { p = p.normalized() payload := map[string]any{} payload["type"] = p.Type payload["description"] = p.Description payload["required"] = p.Required setOptionalString(payload, "property", p.Property) if p.CLIRequired { payload["cli_required"] = true } setOptionalString(payload, "required_when", p.RequiredWhen) if err := putRawJSON(payload, "default", p.Default); err != nil { return nil, err } if err := putRawJSON(payload, "interface_default", p.InterfaceDefault); err != nil { return nil, err } if err := putRawJSON(payload, "example", p.Example); err != nil { return nil, err } setOptionalString(payload, "format", p.Format) if len(p.Enum) > 0 { payload["enum"] = append([]string(nil), p.Enum...) } setOptionalString(payload, "interface_description", p.InterfaceDescription) setOptionalString(payload, "interface_type", p.InterfaceType) if len(p.FieldProvenance) > 0 { value, valueErr := typedJSONValue(p.FieldProvenance) if valueErr != nil { return nil, valueErr } payload["field_provenance"] = value } return payload, nil } func applySafetyPayload(payload map[string]any, safety contract.SafetySpec) { setOptionalString(payload, "effect", safety.Effect) setOptionalString(payload, "effect_source", safety.EffectSource) setOptionalString(payload, "risk", safety.Risk) setOptionalString(payload, "confirmation", safety.Confirmation) setOptionalString(payload, "idempotency", safety.Idempotency) } func applyInterfacePayload(payload map[string]any, spec contract.InterfaceSpec) { if spec.Ref != nil { payload["interface_ref"] = map[string]any{ "product_id": spec.Ref.ProductID, "rpc_name": spec.Ref.RPCName, } } setOptionalString(payload, "interface_mode", spec.Mode) setOptionalString(payload, "availability", spec.Availability) setOptionalString(payload, "interface_reason", spec.Reason) } func applySelectionPayload(payload map[string]any, selection contract.SelectionSpec, full bool) { setOptionalString(payload, "agent_summary", selection.AgentSummary) setOptionalString(payload, "agent_summary_source", selection.AgentSummarySource) setOptionalStrings(payload, "use_when", selection.UseWhen) setOptionalStrings(payload, "avoid_when", selection.AvoidWhen) if full { setOptionalStrings(payload, "prerequisites", selection.Prerequisites) setOptionalStrings(payload, "tips", selection.Tips) setOptionalStrings(payload, "workflow_refs", selection.WorkflowRefs) setOptionalStrings(payload, "examples", selection.Examples) if selection.Reviewed != nil { payload["reviewed"] = *selection.Reviewed } } setOptionalStrings(payload, "agent_source_refs", selection.SourceRefs) setOptionalString(payload, "agent_metadata_source", selection.MetadataSource) } func setOptionalString(payload map[string]any, key, value string) { if value = strings.TrimSpace(value); value != "" { payload[key] = value } } func setOptionalStrings(payload map[string]any, key string, values []string) { if values = stableUniqueStrings(values); values != nil { payload[key] = values } } func defaultString(value, fallback string) string { if value = strings.TrimSpace(value); value != "" { return value } return fallback } func putRawJSON(payload map[string]any, key string, raw json.RawMessage) error { if len(raw) == 0 { return nil } value, err := rawJSONValue(raw) if err != nil { return err } payload[key] = value return nil } // rawJSONValue decodes a JSON value that may come from an untrusted source, so // it validates before decoding. Callers holding output that json.Marshal just // produced should use typedJSONValue instead of paying the validation scan. func rawJSONValue(raw json.RawMessage) (any, error) { if !json.Valid(raw) { return nil, fmt.Errorf("invalid JSON value") } return decodeValidJSONValue(raw), nil } // decodeValidJSONValue decodes JSON whose validity the caller has already // established, either by json.Valid or by having just marshaled it. Decode // errors are unreachable under that precondition and are therefore discarded, // exactly as this path behaved when the decode was inlined into rawJSONValue. func decodeValidJSONValue(raw json.RawMessage) any { decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var value any _ = decoder.Decode(&value) return value } // typedJSONValue projects a typed value into the generic JSON shape the payload // renderers consume. json.Marshal output is valid by construction, so this path // decodes it directly: routing through rawJSONValue re-scanned every marshaled // document with json.Valid, which measured ~34% of Schema Catalog assembly time // across the 1121-tool set (26.0s -> 17.2s for the internal/app schema suite). func typedJSONValue(value any) (any, error) { data, err := json.Marshal(value) if err != nil { return nil, err } return decodeValidJSONValue(data), nil }