Files
dingtalk-workspace-cli/internal/cli/schema_parameter_bindings.go

338 lines
14 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"
"sort"
"strings"
"sync"
"sync/atomic"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
)
const runtimeSchemaFlagBindingPropertyAnnotation = "dws.schema.binding.property"
// Property delivery has migrated from active bindings to ParamDecl.Property:
//
// - Property delivery is owned by leaf Contract.Parameters (ParamDecl.Property
// → dws.schema.property / native_annotation). There is no committed
// schema_parameter_bindings.json and no empty bindings{} audit table.
// - Mapping exclusions and semantic removals live in
// schema_parameter_mapping_ledger.go (reviewed Go constants).
// - ValidateSchemaParameterBindingDelivery still joins exclusions/removals to
// the final bound SchemaRegistry.
// - Do not record ParamDecl migrations under Removals: that ledger means
// "must not deliver a property anymore". Migration keeps delivery via
// ParamDecl.
// - BuildEffectiveCommandRegistry does not load this ledger; exclusion
// validation runs at BindEffectiveCommandRegistry and catalog assembly.
type schemaParameterBindingSnapshot struct {
// Bindings is retained only so dual-read / delivery audit can still prove
// there are no versioned_parameter_binding winners. It is always empty in
// production; property delivery is ParamDecl.Property.
Bindings map[string]map[string]string
Removals map[string]schemaParameterBindingRemoval
MappingExclusions map[string]string
}
// schemaParameterBindingRemoval records a semantically meaningful deletion
// from a previous reviewed baseline. ReplacedBy, when present, must name an
// exact active binding key (no active bindings remain).
type schemaParameterBindingRemoval struct {
Reason string
ReplacedBy string
Reviewed bool
}
var runtimeSchemaParameterBindingsLazy struct {
once sync.Once
snapshot schemaParameterBindingSnapshot
err error
}
var runtimeSchemaParameterBindingsLazyLoadCount atomic.Uint64
var schemaParameterBindingData = runtimeSchemaParameterBindingData
func loadSchemaParameterBindingSnapshot() (schemaParameterBindingSnapshot, error) {
snapshot := schemaParameterBindingSnapshot{
Bindings: map[string]map[string]string{},
Removals: reviewedSchemaParameterBindingRemovals,
MappingExclusions: reviewedSchemaParameterMappingExclusions,
}
if err := validateSchemaParameterBindingSnapshot(snapshot); err != nil {
return schemaParameterBindingSnapshot{}, err
}
return snapshot, nil
}
func runtimeSchemaParameterBindingData() (schemaParameterBindingSnapshot, error) {
runtimeSchemaParameterBindingsLazy.once.Do(func() {
runtimeSchemaParameterBindingsLazyLoadCount.Add(1)
runtimeSchemaParameterBindingsLazy.snapshot, runtimeSchemaParameterBindingsLazy.err = loadSchemaParameterBindingSnapshot()
})
return runtimeSchemaParameterBindingsLazy.snapshot, runtimeSchemaParameterBindingsLazy.err
}
// ValidateSchemaParameterBindings is the production validation gate for the
// reviewed mapping ledger (exclusions + removals). Build and generator
// entrypoints call this before any candidate resolution.
func ValidateSchemaParameterBindings() error {
_, err := schemaParameterBindingData()
return err
}
func validateSchemaParameterBindingSnapshot(snapshot schemaParameterBindingSnapshot) error {
if len(snapshot.Bindings) != 0 {
return fmt.Errorf("schema parameter active bindings must remain empty; use ParamDecl.Property for property delivery")
}
for key, removal := range snapshot.Removals {
if err := validateSchemaParameterBindingAuditKey(key); err != nil {
return fmt.Errorf("schema parameter binding removal: %w", err)
}
if !removal.Reviewed || strings.TrimSpace(removal.Reason) == "" || removal.Reason != strings.TrimSpace(removal.Reason) {
return fmt.Errorf("schema parameter binding removal %q must be reviewed with an exact non-empty reason", key)
}
if removal.ReplacedBy != strings.TrimSpace(removal.ReplacedBy) {
return fmt.Errorf("schema parameter binding removal %q has non-canonical replaced_by %q", key, removal.ReplacedBy)
}
if removal.ReplacedBy != "" {
return fmt.Errorf("schema parameter binding removal %q replacement %q is not active (active bindings are retired)", key, removal.ReplacedBy)
}
}
for key, reason := range snapshot.MappingExclusions {
if err := validateSchemaParameterBindingAuditKey(key); err != nil {
return fmt.Errorf("schema parameter mapping exclusion: %w", err)
}
if _, removed := snapshot.Removals[key]; removed {
return fmt.Errorf("schema parameter mapping exclusion %q is also recorded as a removal", key)
}
if strings.TrimSpace(reason) == "" || reason != strings.TrimSpace(reason) {
return fmt.Errorf("schema parameter mapping exclusion %q must have an exact non-empty reason", key)
}
}
if len(snapshot.MappingExclusions) == 0 {
return fmt.Errorf("schema parameter mapping exclusions ledger must remain non-empty")
}
return nil
}
// ValidateSchemaParameterBindingDelivery proves that every reviewed mapping
// ledger entry reaches the final public typed registry it was written for.
func ValidateSchemaParameterBindingDelivery(bound BoundCommandRegistry, registry SchemaRegistry) error {
snapshot, err := schemaParameterBindingData()
if err != nil {
return err
}
return validateSchemaParameterBindingDelivery(snapshot, bound, registry)
}
func validateSchemaParameterBindingDelivery(snapshot schemaParameterBindingSnapshot, bound BoundCommandRegistry, registry SchemaRegistry) error {
publicBound := make(map[string]BoundCommandSpec)
for _, command := range bound.Commands {
if command.Visibility == SchemaVisibilityPublic {
publicBound[command.CanonicalPath] = command
}
}
tools := make(map[string]ToolSpec)
problems := make([]string, 0)
for _, product := range registry.Products {
for _, tool := range product.Tools {
canonical := strings.TrimSpace(tool.Identity.CanonicalPath)
if canonical == "" {
problems = append(problems, "final SchemaRegistry contains a tool with empty canonical_path")
continue
}
if _, duplicate := tools[canonical]; duplicate {
problems = append(problems, fmt.Sprintf("final SchemaRegistry contains duplicate canonical tool %s", canonical))
continue
}
tools[canonical] = tool
}
}
active := make(map[string]string)
for canonical, bindings := range snapshot.Bindings {
for flagName, property := range bindings {
active[runtimeSchemaParameterMappingKey(canonical, flagName)] = property
}
}
activeKeys := sortedSchemaParameterBindingKeys(active)
for _, key := range activeKeys {
canonical, flagName, _ := strings.Cut(key, " --")
property := active[key]
command, boundOK := publicBound[canonical]
if !boundOK {
problems = append(problems, fmt.Sprintf("active parameter binding %q does not reference a public bound command", key))
continue
}
if command.PrimaryCommand == nil || runtimeCommandFlag(command.PrimaryCommand, flagName) == nil {
problems = append(problems, fmt.Sprintf("active parameter binding %q does not reference an exact bound Cobra flag", key))
}
parameter, ok := finalSchemaParameterByName(tools[canonical], flagName)
if !ok {
problems = append(problems, fmt.Sprintf("active parameter binding %q does not reference an exact final public Schema parameter", key))
continue
}
if parameter.Property != property {
problems = append(problems, fmt.Sprintf("active parameter binding %q property = %q, final Schema = %q", key, property, parameter.Property))
}
provenance, ok := parameter.FieldProvenance["property"]
if !ok || !schemaParameterProvenanceHasStringCandidate(provenance, "versioned_parameter_binding", property) {
problems = append(problems, fmt.Sprintf("active parameter binding %q final property provenance has no exact versioned_parameter_binding candidate", key))
}
}
exclusionKeys := make([]string, 0, len(snapshot.MappingExclusions))
for key := range snapshot.MappingExclusions {
exclusionKeys = append(exclusionKeys, key)
}
sort.Strings(exclusionKeys)
for _, key := range exclusionKeys {
canonical, flagName, _ := strings.Cut(key, " --")
command, ok := publicBound[canonical]
if !ok {
problems = append(problems, fmt.Sprintf("parameter mapping exclusion %q does not reference a public bound command", key))
continue
}
if command.PrimaryCommand == nil || runtimeCommandFlag(command.PrimaryCommand, flagName) == nil {
problems = append(problems, fmt.Sprintf("parameter mapping exclusion %q does not reference an exact bound Cobra flag", key))
}
parameter, ok := finalSchemaParameterByName(tools[canonical], flagName)
if !ok {
problems = append(problems, fmt.Sprintf("parameter mapping exclusion %q does not reference an exact final public Schema parameter", key))
continue
}
if parameter.Property != "" {
problems = append(problems, fmt.Sprintf("parameter mapping exclusion %q delivered property %q, want omitted", key, parameter.Property))
}
provenance, ok := parameter.FieldProvenance["property"]
if !ok || provenance.Source != "reviewed_mapping_exclusion" || strings.TrimSpace(provenance.ReviewReason) == "" {
problems = append(problems, fmt.Sprintf("parameter mapping exclusion %q final provenance is not the reviewed exclusion", key))
}
}
removalKeys := make([]string, 0, len(snapshot.Removals))
for key := range snapshot.Removals {
removalKeys = append(removalKeys, key)
}
sort.Strings(removalKeys)
for _, key := range removalKeys {
canonical, flagName, _ := strings.Cut(key, " --")
if _, ok := bound.ByCanonical[canonical]; !ok {
problems = append(problems, fmt.Sprintf("parameter binding removal %q has a stale canonical path", key))
continue
}
if parameter, exists := finalSchemaParameterByName(tools[canonical], flagName); exists {
problems = append(problems, fmt.Sprintf("parameter binding removal %q is still delivered with property %q", key, parameter.Property))
}
}
for canonical, tool := range tools {
for _, parameter := range tool.Parameters {
key := runtimeSchemaParameterMappingKey(canonical, parameter.Name)
provenance, ok := parameter.FieldProvenance["property"]
if !ok {
continue
}
switch provenance.Source {
case "versioned_parameter_binding":
if property, exists := active[key]; !exists || property != parameter.Property {
problems = append(problems, fmt.Sprintf("final Schema parameter %q claims versioned binding provenance without an exact active manifest entry", key))
}
case "reviewed_mapping_exclusion":
if _, exists := snapshot.MappingExclusions[key]; !exists || parameter.Property != "" {
problems = append(problems, fmt.Sprintf("final Schema parameter %q claims mapping exclusion provenance without an exact reviewed exclusion", key))
}
}
}
}
if len(problems) == 0 {
return nil
}
sort.Strings(problems)
return fmt.Errorf("schema parameter binding delivery invariant failed with %d problem(s):\n - %s", len(problems), strings.Join(problems, "\n - "))
}
func finalSchemaParameterByName(tool ToolSpec, flagName string) (ParameterSpec, bool) {
for _, parameter := range tool.Parameters {
if parameter.Name == flagName {
return parameter, true
}
}
return ParameterSpec{}, false
}
func schemaParameterProvenanceHasStringCandidate(provenance contract.FieldProvenance, source, value string) bool {
if provenance.Source == source {
var selected string
return json.Unmarshal(provenance.Value, &selected) == nil && selected == value
}
for _, candidate := range provenance.Candidates {
if candidate.Source != source {
continue
}
var candidateValue string
if json.Unmarshal(candidate.Value, &candidateValue) == nil && candidateValue == value {
return true
}
}
return false
}
func sortedSchemaParameterBindingKeys(bindings map[string]string) []string {
keys := make([]string, 0, len(bindings))
for key := range bindings {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func validateSchemaParameterBindingAuditKey(key string) error {
if key == "" || key != strings.TrimSpace(key) {
return fmt.Errorf("invalid exact binding key %q", key)
}
canonical, flagName, ok := strings.Cut(key, " --")
if !ok || strings.Contains(flagName, " --") || !commandRegistryCanonicalPattern.MatchString(canonical) || flagName == "" || strings.ContainsAny(flagName, " \t\r\n") {
return fmt.Errorf("invalid exact binding key %q", key)
}
return nil
}
// LoadSchemaParameterBindings returns a defensive copy of the reviewed
// active public flag-to-interface bindings. Active bindings are empty;
// property delivery comes from ParamDecl.Property. Mapping
// exclusions and removals remain on the Go mapping ledger (not returned here).
func LoadSchemaParameterBindings() (map[string]map[string]string, error) {
snapshot, err := schemaParameterBindingData()
if err != nil {
return nil, err
}
source := snapshot.Bindings
bindings := make(map[string]map[string]string, len(source))
for canonical, parameters := range source {
bindings[canonical] = make(map[string]string, len(parameters))
for flagName, propertyName := range parameters {
bindings[canonical][flagName] = propertyName
}
}
return bindings, nil
}