From 53816b3d33b1b26fee2be81ddef1618d0c80afdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=89=E6=BE=9C?= Date: Sun, 2 Aug 2026 13:21:15 +0800 Subject: [PATCH] fix(schema): drop retired Hint dead code and harden CI coverage shards Remove manual_hints/Hint* leftovers after declare-or-annotate delivery, and fix macOS auth scoping plus Windows/.exe TestMain and race shard packaging so platform coverage gates stay reliable. Co-authored-by: Cursor --- .github/workflows/ci.yml | 20 +- docs/command-framework-architecture.md | 2 +- .../cli/catalogcodegen/probe_bench_test.go | 24 - internal/cli/schema_catalog.go | 27 +- internal/cli/schema_command_registry.go | 2 +- internal/cli/schema_shards_coverage_test.go | 2 +- internal/cli/schema_testmain_test.go | 5 + .../generator/agentmetadata/export_test.go | 10 - internal/generator/agentmetadata/hints.go | 29 - .../generator/agentmetadata/manual_hints.go | 277 -------- .../manual_hints_coverage_external_test.go | 35 - .../manual_hints_coverage_test.go | 488 ------------- internal/generator/agentmetadata/metadata.go | 29 +- .../metadata_helpers_coverage_test.go | 18 +- internal/generator/agentmetadata/pipeline.go | 58 +- .../coverage_more_test.go | 10 +- .../cross_platform_coverage_test.go | 21 +- .../cmd_schema_agent_metadata/main.go | 19 +- .../cmd_schema_agent_metadata/main_test.go | 37 +- scripts/ci/test-packages.sh | 9 +- scripts/dev/migrate_product_params.py | 639 ------------------ scripts/policy/check-schema-binary.sh | 5 +- scripts/policy/check-schema-catalog.sh | 7 +- scripts/policy/run-platform-coverage-gate.sh | 9 +- 24 files changed, 135 insertions(+), 1647 deletions(-) delete mode 100644 internal/generator/agentmetadata/export_test.go delete mode 100644 internal/generator/agentmetadata/hints.go delete mode 100644 internal/generator/agentmetadata/manual_hints.go delete mode 100644 internal/generator/agentmetadata/manual_hints_coverage_external_test.go delete mode 100644 internal/generator/agentmetadata/manual_hints_coverage_test.go delete mode 100644 scripts/dev/migrate_product_params.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e3df6c7..e725f37a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -490,7 +490,8 @@ jobs: needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true' }} runs-on: ubuntu-latest - timeout-minutes: 15 + # cli shard needs headroom beyond go test -timeout for setup + Schema assembly. + timeout-minutes: 20 strategy: fail-fast: false matrix: @@ -498,6 +499,7 @@ jobs: - app - generators - helpers + - cli - remaining steps: - name: Check out repository @@ -523,7 +525,13 @@ jobs: test -n "$package_output" mapfile -t packages <<< "$package_output" test "${#packages[@]}" -gt 0 - go test -v -race -count=1 -timeout=12m "${packages[@]}" + # cli shard owns ResolveSchemaBuild TestMain assembly under -race; give + # it a dedicated budget so remaining is not SIGTERM'd by OOM/timeout. + timeout_budget=12m + if [ "$TEST_SHARD" = "cli" ]; then + timeout_budget=15m + fi + go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}" test-release-scripts: name: Test (workflow and release contracts) @@ -693,8 +701,14 @@ jobs: with: go-version-file: go.mod + # Full ./internal/app (including schema --all assembly) already runs in the + # race:app shard. Keep this job focused on native auth/keychain paths so + # heavy Schema completeness tests cannot exhaust the 10m budget. - name: Test macOS auth and Keychain paths with Race Detection - run: go test -v -race -count=1 -timeout=10m ./internal/keychain ./internal/auth ./internal/app + run: go test -v -race -count=1 -timeout=10m ./internal/keychain ./internal/auth + + - name: Test macOS auth migration and portable auth diagnostics + run: go test -v -race -count=1 -timeout=5m ./internal/app -run '^Test(CrossPlatformCoverage)?Auth(MigrateKeychain|StatusDiagnosticReportsCiphertextKeyMismatch|ExportRejectsWindowsDPAPIBackend|ImportRejectsWindowsDPAPIBackend)' test-windows: name: Test (Windows) diff --git a/docs/command-framework-architecture.md b/docs/command-framework-architecture.md index f0c02ce7..66f57a69 100644 --- a/docs/command-framework-architecture.md +++ b/docs/command-framework-architecture.md @@ -275,7 +275,7 @@ type/default/usage provenance 保持不变,command 统一补充 Required、Enu - **CommandMeta 投影**(装配 Once 同步缓存 `map[cli_path]CommandMeta`;`ResolveMeta` / `SafetyForCLIPath` / leaf `--help` Safety 稳态 O(1) 读缓存,与 SchemaRegistry 同源) - **Dry-run Capabilities**(声明自动索引为 reviewed 能力) -Agent selection / Safety 文案随 Schema 组装时内存 inject,不落盘、不 embed;`schema_agent_metadata/` 与 `schema_hints/` 已退役。不再需要外部 hint 文件维护 selection/metadata/dry-run 信息。`cmd_schema_catalog` 仅生成 CI/local dump,Catalog/meta-index 路径不得提交。 +生产权威是 leaf `ContractFinal` / `ProductDecl`(经 `RegisterSchemaSourceRoot` → `ResolveSchemaBuild` 装配进 Catalog);`InstallBuildTimeAgentMetadataJSON` 仅用于 `cmd_schema_catalog` 的 CI/local dump inject,不是生产交付路径。`schema_agent_metadata/` 与 `schema_hints/` 已退役。不再需要外部 hint 文件维护 selection/metadata/dry-run 信息。Catalog/meta-index 路径不得提交。 ## 设计原则 diff --git a/internal/cli/catalogcodegen/probe_bench_test.go b/internal/cli/catalogcodegen/probe_bench_test.go index a8b58485..5b386d3b 100644 --- a/internal/cli/catalogcodegen/probe_bench_test.go +++ b/internal/cli/catalogcodegen/probe_bench_test.go @@ -4,8 +4,6 @@ package catalogcodegen_test import ( - "encoding/json" - "os" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/catalogcodegen" @@ -26,25 +24,3 @@ func BenchmarkGoLiteralAccess(b *testing.B) { } } } - -// BenchmarkJSONDecodeSameShard measures decoding the same product's committed -// shard into untyped maps, i.e. what the current loader pays for this product. -func BenchmarkJSONDecodeSameShard(b *testing.B) { - data, err := os.ReadFile("../schema_catalog/tools/dev.json") - if err != nil { - b.Skip("shard unavailable") - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - var shard struct { - Tools map[string]map[string]any `json:"tools"` - } - if err := json.Unmarshal(data, &shard); err != nil { - b.Fatal(err) - } - if len(shard.Tools) == 0 { - b.Fatal("empty shard") - } - } -} diff --git a/internal/cli/schema_catalog.go b/internal/cli/schema_catalog.go index fbedc800..cf135dd7 100644 --- a/internal/cli/schema_catalog.go +++ b/internal/cli/schema_catalog.go @@ -164,9 +164,9 @@ func loadTypedSchemaCatalog(envelope schemaCatalogEnvelopeTyped, tools map[strin } // assembleSchemaCatalogSnapshot merges an envelope document and a directory of -// per-product tool shards back into the single-document snapshot shape. Split -// from assembleEmbeddedSchemaCatalog so shard failure modes stay testable -// against fake filesystems. +// per-product tool shards back into the single-document snapshot shape used by +// ephemeral cmd_schema_catalog dumps. Shard failure modes stay testable against +// fake filesystems. func assembleSchemaCatalogSnapshot(envelopeJSON []byte, shards fs.FS, dir string) (SchemaCatalogSnapshot, error) { var envelope schemaCatalogEnvelope if err := decodeStrictSchemaJSON(envelopeJSON, &envelope); err != nil { @@ -214,12 +214,11 @@ var ( buildCatalogValidateDelivery = ValidateSchemaDeliveryInvariants buildCatalogValidateFinalCompleteness = validateResolvedSchemaCatalogDeliveryCompleteness - loadCatalogValidateInterfaces = validateSchemaRegistryInterfaces - loadCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage - loadCatalogValidateAgentMetadata = validateSchemaRegistryAgentMetadata + loadCatalogValidateInterfaces = validateSchemaRegistryInterfaces + loadCatalogValidateProvenance = validateFinalSchemaProvenanceCoverage - renderEmbeddedSchemaAll = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToPayload() } - renderEmbeddedSchemaOverview = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToOverviewPayload() } + renderDeliverySchemaAll = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToPayload() } + renderDeliverySchemaOverview = func(registry SchemaRegistry) (map[string]any, error) { return registry.ToOverviewPayload() } renderSchemaProductSummary = func(product ProductSpec) (map[string]any, error) { return product.ToSummaryPayload() } renderSchemaToolSummary = func(tool ToolSpec) (map[string]any, error) { return tool.ToSummaryPayload() } ) @@ -334,10 +333,10 @@ func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCata if err := loadCatalogValidateProvenance(registry); err != nil { return loadedSchemaCatalog{}, fmt.Errorf("validate final Schema provenance: %w", err) } - // Agent metadata is no longer a shipped embed. Build-time Catalog generation - // still validates the injected Agent metadata set; the production loader - // trusts the committed catalog snapshot and must not reopen that retired - // intermediate artifact. + // Agent metadata is no longer a shipped embed. CI/local cmd_schema_catalog + // dumps may still inject Agent metadata for validation; production delivery + // reassembles via ResolveSchemaBuild and must not reopen retired + // schema_agent_metadata/ artifacts. return loadedSchemaCatalog{Snapshot: snapshot, Registry: registry, Index: index}, nil } @@ -360,7 +359,7 @@ func deliverySchemaOverviewPayload() (map[string]any, error) { } func schemaAllPayloadFromLoaded(loaded loadedSchemaCatalog) (map[string]any, error) { - payload, err := renderEmbeddedSchemaAll(loaded.Registry) + payload, err := renderDeliverySchemaAll(loaded.Registry) if err != nil { return nil, err } @@ -372,7 +371,7 @@ func schemaAllPayloadFromLoaded(loaded loadedSchemaCatalog) (map[string]any, err } func schemaOverviewPayloadFromLoaded(loaded loadedSchemaCatalog) (map[string]any, error) { - payload, err := renderEmbeddedSchemaOverview(loaded.Registry) + payload, err := renderDeliverySchemaOverview(loaded.Registry) if err != nil { return nil, err } diff --git a/internal/cli/schema_command_registry.go b/internal/cli/schema_command_registry.go index 72833a66..1e87fb1e 100644 --- a/internal/cli/schema_command_registry.go +++ b/internal/cli/schema_command_registry.go @@ -106,7 +106,7 @@ func loadEmbeddedCommandRegistry() (CommandRegistry, error) { // assembleCommandRegistry reads the per-product shards embedded at build time // and reassembles them into a single CommandRegistry, identical to the previous -// single-file layout. Mirrors assembleEmbeddedSchemaCatalog for catalog shards. +// single-file layout. Mirrors assembleSchemaCatalogSnapshot's shard merge shape. func assembleCommandRegistry() (CommandRegistry, error) { return assembleCommandRegistryFrom(embeddedSchemaCommandRegistryEnvelopeJSON, embeddedSchemaCommandRegistryProducts, "schema_command_registry/products") } diff --git a/internal/cli/schema_shards_coverage_test.go b/internal/cli/schema_shards_coverage_test.go index 252f2bf5..97d62416 100644 --- a/internal/cli/schema_shards_coverage_test.go +++ b/internal/cli/schema_shards_coverage_test.go @@ -222,7 +222,7 @@ func TestRenderSafetyAnnotation(t *testing.T) { } // TestCrossPlatformCoverageAssembleEmbeddedSchemaCatalogPropagatesAssemblyError -// 通过临时替换嵌入的 envelope 字节,覆盖 assembleEmbeddedSchemaCatalog 的错误透传分支。 +// 覆盖 assembleTypedSchemaCatalog / assembleSchemaCatalogSnapshot 的错误透传分支。 func TestCrossPlatformCoverageAssembleEmbeddedSchemaCatalogPropagatesAssemblyError(t *testing.T) { if _, _, err := assembleTypedSchemaCatalog([]byte("{bad"), nil, "tools"); err == nil || !strings.Contains(err.Error(), "decode schema catalog.json") { t.Fatalf("error = %v", err) diff --git a/internal/cli/schema_testmain_test.go b/internal/cli/schema_testmain_test.go index 87398f02..bc7c2fa0 100644 --- a/internal/cli/schema_testmain_test.go +++ b/internal/cli/schema_testmain_test.go @@ -56,7 +56,12 @@ func installAssembledSchemaDeliveryForPackageCLITests() (func(), error) { } cleanup := func() { _ = os.RemoveAll(tmp) } outDir := filepath.Join(tmp, "schema_catalog") + // On Windows, go build -o without .exe writes foo.exe; exec.Command must + // use the same path or TestMain fails during go test -list / coverage gate. generator := filepath.Join(tmp, "cmd_schema_catalog") + if runtime.GOOS == "windows" { + generator += ".exe" + } build := exec.Command("go", "build", "-o", generator, "./internal/generator/cmd_schema_catalog") build.Dir = repoRoot if out, err := build.CombinedOutput(); err != nil { diff --git a/internal/generator/agentmetadata/export_test.go b/internal/generator/agentmetadata/export_test.go deleted file mode 100644 index dd3722a6..00000000 --- a/internal/generator/agentmetadata/export_test.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2026 Alibaba Group -// Licensed under the Apache License, Version 2.0 (the "License"); - -package agentmetadata - -// ValidateSelectionAuthoringContractsForTest exposes the selection authoring -// gate to external tests that must import app without polluting this package. -func ValidateSelectionAuthoringContractsForTest(opts Options) error { - return validateSelectionAuthoringContracts(opts) -} diff --git a/internal/generator/agentmetadata/hints.go b/internal/generator/agentmetadata/hints.go deleted file mode 100644 index b6f199ad..00000000 --- a/internal/generator/agentmetadata/hints.go +++ /dev/null @@ -1,29 +0,0 @@ -// 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" - "strings" -) - -// parseHintSources formerly loaded schema_hints/ HintFiles. That directory is -// fully retired: ProductDecl / ContractFinal own routing and leaf facts. -// An empty HintsDir is a no-op; any non-empty value fails closed. -func parseHintSources(_ *File, _ []sourceFile, opts Options, _ *Stats, _ sourceTracker) (usedSelection bool, err error) { - if strings.TrimSpace(opts.HintsDir) == "" { - return false, nil - } - return false, fmt.Errorf("schema_hints/ is retired; clear HintsDir and declare ProductDecl/ContractFinal instead (got %q)", opts.HintsDir) -} diff --git a/internal/generator/agentmetadata/manual_hints.go b/internal/generator/agentmetadata/manual_hints.go deleted file mode 100644 index eb53ef36..00000000 --- a/internal/generator/agentmetadata/manual_hints.go +++ /dev/null @@ -1,277 +0,0 @@ -// 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 - -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/cli/contractfinal" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" -) - -var reviewedSelectionFields = map[string]bool{ - "agent_summary": true, - "use_when": true, - "avoid_when": true, - "examples": true, -} - -// retainReviewedSelectionCandidates keeps only reviewed candidates -// (reviewed_explicit or contract_final) for Agent selection fields so -// metadata/skill/MCP prose cannot win after selection files have been -// applied. contract_final candidates come from in-code Contract declarations -// and outrank file sources for declared tools. -func retainReviewedSelectionCandidates(file *File) { - if file == nil { - return - } - for productID, metadata := range file.Products { - if metadata.fieldCandidates == nil { - metadata.fieldCandidates = map[string][]FieldCandidateProvenance{} - } - for _, field := range []string{"agent_summary", "use_when", "avoid_when"} { - metadata.fieldCandidates[field] = onlyReviewedExplicitCandidates(metadata.fieldCandidates[field]) - } - file.Products[productID] = metadata - } - for path, metadata := range file.Tools { - if metadata.fieldCandidates == nil { - metadata.fieldCandidates = map[string][]FieldCandidateProvenance{} - } - for field := range reviewedSelectionFields { - metadata.fieldCandidates[field] = onlyReviewedExplicitCandidates(metadata.fieldCandidates[field]) - } - file.Tools[path] = metadata - } -} - -func onlyReviewedExplicitCandidates(candidates []FieldCandidateProvenance) []FieldCandidateProvenance { - selected := make([]FieldCandidateProvenance, 0, len(candidates)) - for _, candidate := range candidates { - if isReviewedSelectionRank(precedenceRank(candidate.Precedence)) { - selected = append(selected, candidate) - } - } - return selected -} - -// isReviewedSelectionRank reports whether a rank is a reviewed Agent selection -// source: the reviewed selection files (reviewed_explicit) or an in-code -// Contract final declaration (contract_final, the stronger reviewed source). -func isReviewedSelectionRank(rank int) bool { - return rank == selectionRankReviewedExplicit || rank == selectionRankContractFinal -} - -// isReviewedSelectionPrecedence mirrors isReviewedSelectionRank for provenance -// precedence labels. -func isReviewedSelectionPrecedence(precedence string) bool { - switch strings.TrimSpace(precedence) { - case selectionPrecedenceReviewedExplicit, selectionPrecedenceContractFinal: - return true - default: - return false - } -} - -func validateReviewedSelectionDelivery(file File, opts Options) error { - problems := []string{} - productIDs := sortedBoolKeys(opts.ProductIDs) - for _, productID := range productIDs { - metadata, ok := file.Products[productID] - if !ok { - // ProductDecl-registered products may omit selection/ products{} - // rows; their contract_final prose is applied outside hint files. - if contract.HasProductDecl(productID) { - continue - } - problems = append(problems, "missing product "+productID) - continue - } - checks := []struct { - name string - valid bool - }{ - {"agent_summary", strings.TrimSpace(metadata.AgentSummary) != "" && isReviewedSelectionRank(metadata.agentSummaryRank)}, - {"use_when", len(metadata.UseWhen) > 0 && isReviewedSelectionRank(metadata.useWhenRank)}, - {"avoid_when", len(metadata.AvoidWhen) > 0 && isReviewedSelectionRank(metadata.avoidWhenRank)}, - } - for _, check := range checks { - if !check.valid { - problems = append(problems, productID+" "+check.name+" is not reviewed_explicit/contract_final") - } - } - for _, field := range []string{"agent_summary", "use_when", "avoid_when"} { - provenance, ok := metadata.FieldProvenance[field] - if !ok || !isReviewedSelectionPrecedence(provenance.Precedence) || selectedCandidateCount(provenance.Candidates) != 1 { - problems = append(problems, productID+" "+field+" provenance is not one reviewed_explicit/contract_final winner") - } - // File-based winners must come from a selection/ source file; - // contract_final winners are declared in code (no file path). - if ok && provenance.Precedence != selectionPrecedenceContractFinal && !strings.Contains(provenance.Source, "/selection/") { - problems = append(problems, productID+" "+field+" source is not selection/: "+provenance.Source) - } - } - } - - expected := expectedCanonicalToolPaths(opts) - canonicalPaths := make([]string, 0, len(expected)) - for canonical := range expected { - canonicalPaths = append(canonicalPaths, canonical) - } - sort.Strings(canonicalPaths) - for _, canonical := range canonicalPaths { - primary := expected[canonical] - metadata, ok := file.Tools[primary] - if !ok { - problems = append(problems, "missing tool "+canonical) - continue - } - checks := []struct { - name string - valid bool - }{ - {"agent_summary", strings.TrimSpace(metadata.AgentSummary) != "" && isReviewedSelectionRank(metadata.agentSummaryRank)}, - {"use_when", len(metadata.UseWhen) > 0 && isReviewedSelectionRank(metadata.useWhenRank)}, - {"avoid_when", len(metadata.AvoidWhen) > 0 && isReviewedSelectionRank(metadata.avoidWhenRank)}, - {"examples", len(metadata.Examples) > 0 && isReviewedSelectionRank(metadata.examplesRank)}, - {"reviewed", metadata.Reviewed != nil && *metadata.Reviewed && isReviewedSelectionRank(metadata.reviewedRank)}, - } - for _, check := range checks { - if !check.valid { - problems = append(problems, canonical+" "+check.name+" is not reviewed_explicit/contract_final") - } - } - for _, field := range []string{"agent_summary", "use_when", "avoid_when", "examples"} { - provenance, ok := metadata.FieldProvenance[field] - if !ok || !isReviewedSelectionPrecedence(provenance.Precedence) || selectedCandidateCount(provenance.Candidates) != 1 { - problems = append(problems, canonical+" "+field+" provenance is not one reviewed_explicit/contract_final winner") - } - // File-based winners must come from a selection/ source file; - // contract_final winners are declared in code (no file path). - if ok && provenance.Precedence != selectionPrecedenceContractFinal && !strings.Contains(provenance.Source, "/selection/") { - problems = append(problems, canonical+" "+field+" source is not selection/: "+provenance.Source) - } - } - } - if len(problems) == 0 { - return nil - } - sort.Strings(problems) - return fmt.Errorf("selection Agent delivery invariant failed: %s", strings.Join(problems, "; ")) -} - -func expectedCanonicalToolPaths(opts Options) map[string]string { - if len(opts.CanonicalToolPaths) > 0 { - result := make(map[string]string, len(opts.CanonicalToolPaths)) - for canonical, primary := range opts.CanonicalToolPaths { - canonical = strings.TrimSpace(canonical) - primary = normalizeCommandPath(primary) - if canonical != "" && primary != "" { - result[canonical] = primary - } - } - return result - } - result := map[string]string{} - for canonical, primary := range opts.ToolPaths { - canonical = strings.TrimSpace(canonical) - if !strings.ContainsAny(canonical, " \t") && strings.Contains(canonical, ".") { - result[canonical] = normalizeCommandPath(primary) - } - } - return result -} - -func selectedCandidateCount(candidates []FieldCandidateProvenance) int { - count := 0 - for _, candidate := range candidates { - if candidate.Selected { - count++ - } - } - return count -} - -// selectionHintCoverageProducts returns product IDs that still need a -// selection/ products{} row. ProductDecl-registered products are exempt. -func selectionHintCoverageProducts(productIDs map[string]bool) map[string]bool { - if productIDs == nil { - return nil - } - expected := make(map[string]bool, len(productIDs)) - for productID, include := range productIDs { - if !include { - continue - } - productID = strings.TrimSpace(productID) - if productID == "" || contract.HasProductDecl(productID) { - continue - } - expected[productID] = true - } - return expected -} - -func expectedCanonicalToolSet(opts Options) map[string]bool { - if len(opts.CanonicalToolPaths) > 0 { - expected := make(map[string]bool, len(opts.CanonicalToolPaths)) - for canonical := range opts.CanonicalToolPaths { - if canonical = strings.TrimSpace(canonical); canonical != "" { - expected[canonical] = true - } - } - return expected - } - if len(opts.ToolPaths) == 0 { - return nil - } - expected := map[string]bool{} - for path := range opts.ToolPaths { - path = strings.TrimSpace(path) - if !strings.ContainsAny(path, " \t") && strings.Contains(path, ".") { - expected[path] = true - } - } - return expected -} - -func validateSelectionAuthoringContracts(opts Options) error { - expectedTools := expectedCanonicalToolSet(opts) - for canonical := range expectedTools { - bound, ok := opts.BoundCommands.ByCanonical[canonical] - if ok && contractfinal.HasRuntimeContractFinal(bound.PrimaryCommand) { - delete(expectedTools, canonical) - } - } - expectedProducts := selectionHintCoverageProducts(opts.ProductIDs) - if selectionHintCoverageRequired(expectedProducts, expectedTools) { - 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) - } - if len(opts.BoundCommands.Commands) == 0 { - return nil - } - _, err := cli.ValidateAgentSelectionContract(opts.BoundCommands) - return err -} diff --git a/internal/generator/agentmetadata/manual_hints_coverage_external_test.go b/internal/generator/agentmetadata/manual_hints_coverage_external_test.go deleted file mode 100644 index 0b9f555f..00000000 --- a/internal/generator/agentmetadata/manual_hints_coverage_external_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2026 Alibaba Group -// Licensed under the Apache License, Version 2.0 (the "License"); - -package agentmetadata_test - -import ( - "testing" - - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/agentmetadata" -) - -// Kept in package agentmetadata_test so importing app (and its ProductDecl -// registrations) cannot pollute fixture-based generateFromSources tests in -// package agentmetadata. -func TestCrossPlatformCoverageValidateSelectionAuthoringContractsRealBoundRegistry(t *testing.T) { - root := app.NewSchemaSourceRootCommand() - effective, err := cli.BuildEffectiveCommandRegistry(root) - if err != nil { - t.Fatal(err) - } - bound, err := cli.BindEffectiveCommandRegistry(root, effective) - if err != nil { - t.Fatal(err) - } - projection := agentmetadata.ProjectEffectiveRegistry(effective) - if err := agentmetadata.ValidateSelectionAuthoringContractsForTest(agentmetadata.Options{ - CanonicalToolPaths: projection.CanonicalToolPaths, - ProductIDs: projection.ProductIDs, - BoundCommands: bound, - }); err != nil { - t.Fatalf("real bound registry: %v", err) - } -} diff --git a/internal/generator/agentmetadata/manual_hints_coverage_test.go b/internal/generator/agentmetadata/manual_hints_coverage_test.go deleted file mode 100644 index 62114cc2..00000000 --- a/internal/generator/agentmetadata/manual_hints_coverage_test.go +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright 2026 Alibaba Group -// Licensed under the Apache License, Version 2.0 (the "License"); - -package agentmetadata - -import ( - "fmt" - "path/filepath" - "strings" - "testing" - - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" - "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" - "github.com/spf13/cobra" -) - -func reviewedSelectionProvenance(source string) map[string]FieldProvenance { - return map[string]FieldProvenance{ - "agent_summary": { - Precedence: selectionPrecedenceReviewedExplicit, - Source: source, - Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit, Source: source}}, - }, - "use_when": { - Precedence: selectionPrecedenceReviewedExplicit, - Source: source, - Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit, Source: source}}, - }, - "avoid_when": { - Precedence: selectionPrecedenceReviewedExplicit, - Source: source, - Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit, Source: source}}, - }, - } -} - -func TestCrossPlatformCoverageManualHintsRetainAndValidateReviewedSelection(t *testing.T) { - file := &File{ - Products: map[string]ProductMetadata{ - "sample": { - AgentSummary: "summary", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - fieldCandidates: map[string][]FieldCandidateProvenance{ - "agent_summary": { - {Value: "summary", Precedence: selectionPrecedenceReviewedExplicit, Source: "hints/selection/sample.json", Selected: true}, - {Value: "skill", Precedence: selectionPrecedenceSkill, Source: "skill"}, - }, - }, - FieldProvenance: reviewedSelectionProvenance("hints/selection/sample.json"), - }, - }, - Tools: map[string]ToolMetadata{ - "sample run": { - AgentSummary: "tool", agentSummaryRank: selectionRankContractFinal, - UseWhen: []string{"use"}, useWhenRank: selectionRankContractFinal, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankContractFinal, - Examples: []string{"dws sample run"}, examplesRank: selectionRankContractFinal, - Reviewed: boolPtr(true), reviewedRank: selectionRankContractFinal, - fieldCandidates: map[string][]FieldCandidateProvenance{ - "examples": { - {Value: []string{"dws sample run"}, Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin, Selected: true}, - {Value: []string{"other"}, Precedence: selectionPrecedenceSkill, Source: "skill"}, - }, - }, - FieldProvenance: map[string]FieldProvenance{ - "agent_summary": {Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin, Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin}}}, - "use_when": {Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin, Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin}}}, - "avoid_when": {Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin, Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin}}}, - "examples": {Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin, Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceContractFinal, Source: contractFinalOrigin}}}, - }, - }, - }, - } - retainReviewedSelectionCandidates(file) - if len(file.Products["sample"].fieldCandidates["agent_summary"]) != 1 { - t.Fatalf("product candidates = %#v", file.Products["sample"].fieldCandidates["agent_summary"]) - } - if len(file.Tools["sample run"].fieldCandidates["examples"]) != 1 { - t.Fatalf("tool candidates = %#v", file.Tools["sample run"].fieldCandidates["examples"]) - } - if !isReviewedSelectionRank(selectionRankReviewedExplicit) || !isReviewedSelectionRank(selectionRankContractFinal) { - t.Fatal("reviewed rank helpers changed") - } - if got := onlyReviewedExplicitCandidates([]FieldCandidateProvenance{ - {Precedence: selectionPrecedenceSkill}, - {Precedence: selectionPrecedenceContractFinal}, - }); len(got) != 1 { - t.Fatalf("filtered candidates = %#v", got) - } - - opts := Options{ - ProductIDs: map[string]bool{"sample": true}, - CanonicalToolPaths: map[string]string{"sample.run": "sample run"}, - } - if err := validateReviewedSelectionDelivery(*file, opts); err != nil { - t.Fatalf("valid reviewed delivery: %v", err) - } - if err := validateReviewedSelectionDelivery(File{}, Options{ProductIDs: map[string]bool{"missing": true}}); err == nil || - !strings.Contains(err.Error(), "missing product missing") { - t.Fatalf("missing product error = %v", err) - } - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{}}, Options{ - CanonicalToolPaths: map[string]string{"sample.run": "sample run"}, - }); err == nil || !strings.Contains(err.Error(), "missing tool sample.run") { - t.Fatalf("missing tool error = %v", err) - } -} - -func TestCrossPlatformCoverageManualHintsCoverageHelpers(t *testing.T) { - t.Cleanup(func() { contract.ClearProductDeclForTest("declared") }) - contract.RegisterProductDecl(contract.ProductDecl{ - ID: "declared", - Selection: contract.ProductSelectionDecl{ - AgentSummary: "Declared", - UseWhen: []string{"use"}, - AvoidWhen: []string{"avoid"}, - }, - }) - if products := selectionHintCoverageProducts(map[string]bool{"declared": true, "hinted": true}); products["declared"] || !products["hinted"] { - t.Fatalf("selectionHintCoverageProducts = %#v", products) - } - if got := expectedCanonicalToolPaths(Options{CanonicalToolPaths: map[string]string{"sample.run": " sample run "}}); got["sample.run"] != "sample run" { - t.Fatalf("expectedCanonicalToolPaths canonical = %#v", got) - } - if got := expectedCanonicalToolPaths(Options{ToolPaths: map[string]string{"sample run": "sample run", "sample.run": "sample run"}}); got["sample.run"] != "sample run" { - t.Fatalf("expectedCanonicalToolPaths tool paths = %#v", got) - } - if got := expectedCanonicalToolSet(Options{ToolPaths: map[string]string{"sample.run": "sample run", "sample run": "sample run"}}); !got["sample.run"] || got["sample run"] { - t.Fatalf("expectedCanonicalToolSet = %#v", got) - } - if got := expectedCanonicalToolSet(Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); !got["sample.run"] { - t.Fatalf("expectedCanonicalToolSet canonical = %#v", got) - } - if err := validateSelectionAuthoringContracts(Options{BoundCommands: cli.BoundCommandRegistry{Commands: []cli.BoundCommandSpec{}}}); err != nil { - t.Fatalf("empty bound registry: %v", err) - } - selected := true - if selectedCandidateCount([]FieldCandidateProvenance{{Selected: true}, {}}) != 1 { - t.Fatal("selectedCandidateCount changed") - } - if _, err := parseHintSources(nil, nil, Options{HintsDir: "schema_hints"}, nil, sourceTracker{}); err == nil || - !strings.Contains(err.Error(), "schema_hints/ is retired") { - t.Fatalf("parseHintSources error = %v", err) - } - if _, err := parseHintSources(&File{}, nil, Options{}, nil, sourceTracker{}); err != nil { - t.Fatalf("empty hints dir: %v", err) - } - _ = selected -} - -func TestCrossPlatformCoveragePipelineSelectionCoverageAndGenerateFromRoot(t *testing.T) { - if err := ValidateSelectionHints("", "", RegistryProjection{ - CanonicalToolPaths: map[string]string{"sample.run": "sample run"}, - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "selection coverage incomplete") { - t.Fatalf("ValidateSelectionHints error = %v", err) - } - if !selectionHintCoverageRequired(map[string]bool{"sample": true}, nil) { - t.Fatal("product coverage should require hints") - } - if selectionHintCoverageRequired(nil, map[string]bool{"sample.run": false}) { - t.Fatal("all-false tool coverage should not require hints") - } - if got := resolvePipelineRootPath("root", filepath.Join("..", "abs")); !strings.Contains(got, "abs") { - t.Fatalf("resolvePipelineRootPath relative = %q", got) - } - if _, _, _, err := GenerateFromCommandRoot(".", nil, Options{}); err == nil || !strings.Contains(err.Error(), "root is nil") { - t.Fatalf("nil root error = %v", err) - } -} - -func TestCrossPlatformCoverageContractFinalDeclarationsMergeToolMetadata(t *testing.T) { - declared := &cobra.Command{Use: "run"} - cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{ - Selection: &contract.SelectionSpec{ - AgentSummary: "Declared summary", - UseWhen: []string{"use declared"}, - AvoidWhen: []string{"avoid declared"}, - Prerequisites: []string{}, - Tips: []string{"tip"}, - WorkflowRefs: []string{"wf"}, - Examples: []string{"dws sample run"}, - }, - Safety: &contract.SafetySpec{ - Effect: "read", Risk: "low", Confirmation: "not_required", Idempotency: "idempotent", - }, - Interface: &contract.InterfaceSpec{ - Mode: contract.InterfaceModeMCP, Availability: contract.InterfaceAvailable, - Ref: &contract.InterfaceRefSpec{ProductID: "sample", RPCName: "run"}, - }, - }) - t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) }) - - file := &File{Tools: map[string]ToolMetadata{}} - if err := applyContractFinalDeclarations(file, Options{ - BoundCommands: cli.BoundCommandRegistry{ByCanonical: map[string]cli.BoundCommandSpec{ - "sample.run": {PrimaryCommand: declared}, - }}, - CanonicalToolPaths: map[string]string{"sample.run": "sample run"}, - }); err != nil { - t.Fatalf("applyContractFinalDeclarations: %v", err) - } - tool := file.Tools["sample run"] - if tool.AgentSummary != "Declared summary" || tool.Effect != "read" || tool.InterfaceMode != contract.InterfaceModeMCP { - t.Fatalf("merged tool = %#v", tool) - } - if metadata, ok := contractFinalToolMetadata(&cobra.Command{Use: "missing"}); ok || metadata.AgentSummary != "" { - t.Fatalf("missing overlay metadata = %#v, ok=%v", metadata, ok) - } -} - -func boolPtr(v bool) *bool { return &v } - -func TestCrossPlatformCoverageManualHintsReviewedDeliveryProductDeclSkip(t *testing.T) { - t.Cleanup(func() { contract.ClearProductDeclForTest("declared-only") }) - contract.RegisterProductDecl(contract.ProductDecl{ - ID: "declared-only", - Selection: contract.ProductSelectionDecl{ - AgentSummary: "Declared only", - UseWhen: []string{"use"}, - AvoidWhen: []string{"avoid"}, - }, - }) - if err := validateReviewedSelectionDelivery(File{}, Options{ - ProductIDs: map[string]bool{"declared-only": true}, - }); err != nil { - t.Fatalf("ProductDecl skip: %v", err) - } -} - -func TestCrossPlatformCoverageManualHintsReviewedDeliveryProductFailures(t *testing.T) { - badProduct := ProductMetadata{ - AgentSummary: "summary", agentSummaryRank: selectionRankSkill, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - FieldProvenance: reviewedSelectionProvenance("hints/selection/sample.json"), - } - if err := validateReviewedSelectionDelivery(File{Products: map[string]ProductMetadata{"sample": badProduct}}, Options{ - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "agent_summary is not reviewed") { - t.Fatalf("bad agent_summary rank = %v", err) - } - - badProduct = ProductMetadata{ - AgentSummary: "summary", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - FieldProvenance: reviewedSelectionProvenance("hints/selection/sample.json"), - } - if err := validateReviewedSelectionDelivery(File{Products: map[string]ProductMetadata{"sample": badProduct}}, Options{ - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "use_when is not reviewed") { - t.Fatalf("empty use_when = %v", err) - } - - badProduct = ProductMetadata{ - AgentSummary: "summary", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{}, avoidWhenRank: selectionRankReviewedExplicit, - FieldProvenance: reviewedSelectionProvenance("hints/selection/sample.json"), - } - if err := validateReviewedSelectionDelivery(File{Products: map[string]ProductMetadata{"sample": badProduct}}, Options{ - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "avoid_when is not reviewed") { - t.Fatalf("empty avoid_when = %v", err) - } - - badProvenance := ProductMetadata{ - AgentSummary: "summary", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - FieldProvenance: map[string]FieldProvenance{ - "agent_summary": {Precedence: selectionPrecedenceSkill, Candidates: []FieldCandidateProvenance{{Selected: true}}}, - "use_when": reviewedSelectionProvenance("hints/selection/sample.json")["use_when"], - "avoid_when": reviewedSelectionProvenance("hints/selection/sample.json")["avoid_when"], - }, - } - if err := validateReviewedSelectionDelivery(File{Products: map[string]ProductMetadata{"sample": badProvenance}}, Options{ - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "agent_summary provenance") { - t.Fatalf("bad provenance = %v", err) - } - - badSource := ProductMetadata{ - AgentSummary: "summary", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - FieldProvenance: map[string]FieldProvenance{ - "agent_summary": {Precedence: selectionPrecedenceReviewedExplicit, Source: "skill.md", Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit}}}, - "use_when": {Precedence: selectionPrecedenceReviewedExplicit, Source: "hints/selection/sample.json", Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit}}}, - "avoid_when": {Precedence: selectionPrecedenceReviewedExplicit, Source: "hints/selection/sample.json", Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit}}}, - }, - } - if err := validateReviewedSelectionDelivery(File{Products: map[string]ProductMetadata{"sample": badSource}}, Options{ - ProductIDs: map[string]bool{"sample": true}, - }); err == nil || !strings.Contains(err.Error(), "source is not selection/") { - t.Fatalf("bad source = %v", err) - } -} - -func TestCrossPlatformCoverageManualHintsReviewedDeliveryToolFailures(t *testing.T) { - reviewedTool := func(mut func(*ToolMetadata)) ToolMetadata { - tool := ToolMetadata{ - AgentSummary: "tool", agentSummaryRank: selectionRankReviewedExplicit, - UseWhen: []string{"use"}, useWhenRank: selectionRankReviewedExplicit, - AvoidWhen: []string{"avoid"}, avoidWhenRank: selectionRankReviewedExplicit, - Examples: []string{"dws sample run"}, examplesRank: selectionRankReviewedExplicit, - Reviewed: boolPtr(true), reviewedRank: selectionRankReviewedExplicit, - FieldProvenance: reviewedSelectionProvenance("hints/selection/sample.json"), - } - tool.FieldProvenance["examples"] = FieldProvenance{ - Precedence: selectionPrecedenceReviewedExplicit, Source: "hints/selection/sample.json", - Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit}}, - } - if mut != nil { - mut(&tool) - } - return tool - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { t.agentSummaryRank = selectionRankSkill }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run agent_summary is not reviewed") { - t.Fatalf("tool agent_summary = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { t.UseWhen = nil }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run use_when is not reviewed") { - t.Fatalf("tool use_when = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { t.AvoidWhen = nil }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run avoid_when is not reviewed") { - t.Fatalf("tool avoid_when = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { t.Examples = nil }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run examples is not reviewed") { - t.Fatalf("tool examples = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { t.Reviewed = boolPtr(false) }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run reviewed is not reviewed") { - t.Fatalf("tool reviewed = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { - t.FieldProvenance["examples"] = FieldProvenance{Precedence: selectionPrecedenceSkill, Candidates: []FieldCandidateProvenance{{Selected: true}}} - }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run examples provenance") { - t.Fatalf("tool examples provenance = %v", err) - } - - if err := validateReviewedSelectionDelivery(File{Tools: map[string]ToolMetadata{ - "sample run": reviewedTool(func(t *ToolMetadata) { - t.FieldProvenance["agent_summary"] = FieldProvenance{ - Precedence: selectionPrecedenceReviewedExplicit, Source: "skill.md", - Candidates: []FieldCandidateProvenance{{Selected: true, Precedence: selectionPrecedenceReviewedExplicit}}, - } - }), - }}, Options{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}}); err == nil || - !strings.Contains(err.Error(), "sample.run agent_summary source is not selection/") { - t.Fatalf("tool bad source = %v", err) - } -} - -func TestCrossPlatformCoverageSelectionHintCoverageProductsSkips(t *testing.T) { - if got := selectionHintCoverageProducts(map[string]bool{"": true, "skip": false, "hinted": true}); got["hinted"] != true || got[""] || got["skip"] { - t.Fatalf("selectionHintCoverageProducts = %#v", got) - } -} - -func TestCrossPlatformCoverageValidateSelectionAuthoringContractsPaths(t *testing.T) { - if err := validateSelectionAuthoringContracts(Options{ - ProductIDs: map[string]bool{"missing-product": true}, - ToolPaths: map[string]string{"missing.tool": "missing tool"}, - BoundCommands: cli.BoundCommandRegistry{Commands: []cli.BoundCommandSpec{}}, - }); err == nil || !strings.Contains(err.Error(), "missing_products") { - t.Fatalf("missing coverage error = %v", err) - } - - declared := &cobra.Command{Use: "run"} - cli.RegisterRuntimeContractFinal(declared, contract.ContractFinalPayload{ - Selection: &contract.SelectionSpec{ - AgentSummary: "Declared", - UseWhen: []string{"use"}, - AvoidWhen: []string{"avoid"}, - Examples: []string{"dws covered run"}, - }, - }) - t.Cleanup(func() { contractfinal.ClearRuntimeContractFinalForTest(declared) }) - if err := validateSelectionAuthoringContracts(Options{ - CanonicalToolPaths: map[string]string{"covered.run": "covered run"}, - BoundCommands: cli.BoundCommandRegistry{ - ByCanonical: map[string]cli.BoundCommandSpec{"covered.run": {PrimaryCommand: declared}}, - }, - }); err != nil { - t.Fatalf("ContractFinal-covered tool: %v", err) - } -} - -func TestCrossPlatformCoveragePipelineGenerateFromCommandRootErrors(t *testing.T) { - origBuild := pipelineBuildEffectiveRegistry - origBind := pipelineBindEffectiveRegistry - origGenerate := pipelineGenerateMetadata - t.Cleanup(func() { - pipelineBuildEffectiveRegistry = origBuild - pipelineBindEffectiveRegistry = origBind - pipelineGenerateMetadata = origGenerate - }) - - if got := resolvePipelineRootPath("/root", "/abs/meta"); got != "/abs/meta" { - t.Fatalf("absolute path = %q", got) - } - - root := &cobra.Command{Use: "partial"} - _, _, _, err := GenerateFromCommandRoot("", root, Options{}) - if err == nil || !strings.Contains(err.Error(), "bind effective CommandRegistry") { - t.Fatalf("bind failure = %v", err) - } - - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { - return cli.EffectiveCommandRegistry{}, fmt.Errorf("build boom") - } - _, _, _, err = GenerateFromCommandRoot(".", root, Options{}) - if err == nil || !strings.Contains(err.Error(), "build effective CommandRegistry") { - t.Fatalf("build failure = %v", err) - } - pipelineBuildEffectiveRegistry = origBuild - - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { - return cli.BoundCommandRegistry{}, fmt.Errorf("bind boom") - } - _, _, _, err = GenerateFromCommandRoot(".", root, Options{}) - if err == nil || !strings.Contains(err.Error(), "bind effective CommandRegistry") { - t.Fatalf("stub bind failure = %v", err) - } - pipelineBindEffectiveRegistry = origBind - - plain := &cobra.Command{Use: "missing"} - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { - spec := cli.CommandSpec{ - CanonicalPath: "missing.tool", - PrimaryCLIPath: "missing tool", - Visibility: cli.SchemaVisibilityPublic, - } - return cli.EffectiveCommandRegistry{ - Commands: []cli.CommandSpec{spec}, - ByCanonical: map[string]cli.CommandSpec{"missing.tool": spec}, - }, nil - } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { - return cli.BoundCommandRegistry{ - ByCanonical: map[string]cli.BoundCommandSpec{"missing.tool": {PrimaryCommand: plain}}, - }, nil - } - _, _, _, err = GenerateFromCommandRoot(".", root, Options{}) - if err == nil || !strings.Contains(err.Error(), "selection coverage incomplete") { - t.Fatalf("coverage failure = %v", err) - } - - pipelineBuildEffectiveRegistry = func(*cobra.Command) (cli.EffectiveCommandRegistry, error) { - return cli.EffectiveCommandRegistry{}, nil - } - pipelineBindEffectiveRegistry = func(*cobra.Command, cli.EffectiveCommandRegistry) (cli.BoundCommandRegistry, error) { - return cli.BoundCommandRegistry{}, nil - } - pipelineGenerateMetadata = func(Options) (File, Stats, error) { - return File{}, Stats{}, fmt.Errorf("generate boom") - } - _, _, _, err = GenerateFromCommandRoot(".", &cobra.Command{Use: "ok"}, Options{}) - if err == nil || !strings.Contains(err.Error(), "generate in-memory Agent metadata") { - t.Fatalf("generate failure = %v", err) - } -} diff --git a/internal/generator/agentmetadata/metadata.go b/internal/generator/agentmetadata/metadata.go index e3130441..49070d42 100644 --- a/internal/generator/agentmetadata/metadata.go +++ b/internal/generator/agentmetadata/metadata.go @@ -293,9 +293,6 @@ type Stats struct { ToolIntents int Examples int RiskRules int - HintFiles int - HintProducts int - HintTools int InterfaceMetadata *InterfaceMetadataAudit UnmatchedTools int SourceProducts []string @@ -320,8 +317,7 @@ type UnmatchedReference struct { // ReferenceReview is an optional disposition of a Skill command reference that // is not a current public leaf. Production no longer loads reviewed HintFile // reference_review maps; unmatched Skill paths are recorded in the build-time -// audit without requiring a reviewed disposition. When residual HintFiles are -// present in fixtures, alias reviews may still merge into a live target. +// audit without requiring a reviewed disposition. type ReferenceReview struct { Status string `json:"status"` Target string `json:"target,omitempty"` @@ -335,9 +331,6 @@ type Audit struct { SourceHash string `json:"source_hash"` SurfaceHash string `json:"surface_hash,omitempty"` SourceFiles int `json:"source_files"` - HintFiles int `json:"hint_files,omitempty"` - HintProducts int `json:"hint_products,omitempty"` - HintTools int `json:"hint_tools,omitempty"` InterfaceMetadata *InterfaceMetadataAudit `json:"interface_metadata,omitempty"` Coverage Coverage `json:"coverage"` SourceProducts []string `json:"source_products,omitempty"` @@ -448,15 +441,6 @@ func generateFromSources(opts Options) (File, Stats, error) { return File{}, Stats{}, err } } - usedSelection, err := parseHintSources(&out, files, opts, &stats, origins) - if err != nil { - return File{}, Stats{}, err - } - if usedSelection { - if err := validateSelectionAuthoringContracts(opts); err != nil { - return File{}, Stats{}, err - } - } if err := applyInterfaceMetadataFallback(&out, byDisplay, opts, &stats, origins); err != nil { return File{}, Stats{}, err } @@ -488,18 +472,10 @@ func generateFromSources(opts Options) (File, Stats, error) { if err := validateEffectiveToolProjection(out, opts); err != nil { return File{}, Stats{}, err } - if usedSelection { - retainReviewedSelectionCandidates(&out) - } normalizeFile(&out, opts.MaxExamples) if err := validateToolFieldCandidateConflicts(out); err != nil { return File{}, Stats{}, err } - if usedSelection { - if err := validateReviewedSelectionDelivery(out, opts); err != nil { - return File{}, Stats{}, err - } - } finalizeInterfaceDispositions(&out) if err := validateInterfaceDispositions(out); err != nil { return File{}, Stats{}, err @@ -559,9 +535,6 @@ func BuildAudit(file File, stats Stats) Audit { SourceHash: file.SourceHash, SurfaceHash: file.SurfaceHash, SourceFiles: stats.SourceFiles, - HintFiles: stats.HintFiles, - HintProducts: stats.HintProducts, - HintTools: stats.HintTools, InterfaceMetadata: stats.InterfaceMetadata, Coverage: file.Coverage, SourceProducts: append([]string(nil), stats.SourceProducts...), diff --git a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go index 4cbfebe4..b9631059 100644 --- a/internal/generator/agentmetadata/metadata_helpers_coverage_test.go +++ b/internal/generator/agentmetadata/metadata_helpers_coverage_test.go @@ -296,6 +296,12 @@ func TestCrossPlatformCoverageMetadataSourceAndPathEdges(t *testing.T) { if _, err := loadSources(Options{Root: root, ProductsDir: "products", ManualHintsPath: "missing.json"}); err == nil || !strings.Contains(err.Error(), "manual hints are retired") { t.Fatalf("non-empty ManualHintsPath should fail closed: %v", err) } + if got := SelectionCoverageProducts(RegistryProjection{ProductIDs: map[string]bool{"": true, "skip": false}}); len(got) != 0 { + t.Fatalf("SelectionCoverageProducts empty/skip = %#v", got) + } + if err := ValidateSelectionCoverage(RegistryProjection{}); err != nil { + t.Fatalf("ValidateSelectionCoverage empty projection: %v", err) + } if got := sourceProductIDs(sourceFile{path: filepath.Join(root, "products", "sample", "guide.md")}, products, nil, nil); len(got) != 1 || got[0] != "sample" { t.Fatalf("nested product ids = %#v", got) @@ -532,15 +538,3 @@ func TestCrossPlatformCoverageContractFinalDeclarationFailureEdges(t *testing.T) t.Fatalf("generateFromSources contract final error = %v", err) } } - -func TestCrossPlatformCoverageReviewedSelectionPrecedenceLabels(t *testing.T) { - if isReviewedSelectionPrecedence("") || isReviewedSelectionPrecedence("skill_document") { - t.Fatal("non-reviewed precedence labels must report false") - } - if !isReviewedSelectionPrecedence(selectionPrecedenceReviewedExplicit) { - t.Fatal("reviewed_explicit must report true") - } - if !isReviewedSelectionPrecedence(selectionPrecedenceContractFinal) { - t.Fatal("contract_final must report true") - } -} diff --git a/internal/generator/agentmetadata/pipeline.go b/internal/generator/agentmetadata/pipeline.go index 6db65abb..5c273648 100644 --- a/internal/generator/agentmetadata/pipeline.go +++ b/internal/generator/agentmetadata/pipeline.go @@ -15,12 +15,12 @@ package agentmetadata import ( "fmt" - "path/filepath" "sort" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli/contractfinal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract" "github.com/spf13/cobra" ) @@ -68,10 +68,9 @@ func ProjectEffectiveRegistry(effective cli.EffectiveCommandRegistry) RegistryPr return projection } -// SelectionHintCoverageTools returns the canonical tools that still need a -// selection/ hint row. ContractFinal-declared leaves are exempt because their -// selection fields live on the leaf declaration. -func SelectionHintCoverageTools(projection RegistryProjection) map[string]bool { +// 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 @@ -85,20 +84,33 @@ func SelectionHintCoverageTools(projection RegistryProjection) map[string]bool { return expectedTools } -// SelectionHintCoverageProducts returns product IDs that still need a -// selection/ products{} row. ProductDecl-registered products are exempt -// because their routing prose lives on the in-code declaration. -func SelectionHintCoverageProducts(projection RegistryProjection) map[string]bool { - return selectionHintCoverageProducts(projection.ProductIDs) +// 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. Residual -// schema_hints/ HintFiles are not accepted. +// ProductDecl and every projected tool to have ContractFinal. Retired +// schema_hints/ overlays are not accepted as coverage. func ValidateSelectionCoverage(projection RegistryProjection) error { - expectedTools := SelectionHintCoverageTools(projection) - expectedProducts := SelectionHintCoverageProducts(projection) - if !selectionHintCoverageRequired(expectedProducts, expectedTools) { + expectedTools := SelectionCoverageTools(projection) + expectedProducts := SelectionCoverageProducts(projection) + if !selectionCoverageRequired(expectedProducts, expectedTools) { return nil } missingProducts := make([]string, 0) @@ -118,19 +130,13 @@ func ValidateSelectionCoverage(projection RegistryProjection) error { return fmt.Errorf("ProductDecl/ContractFinal selection coverage incomplete: missing_products=%v missing_tools=%v", missingProducts, missingTools) } -// ValidateSelectionHints is a compatibility wrapper that ignores HintsDir and -// enforces ContractFinal/ProductDecl coverage only. -func ValidateSelectionHints(_ string, _ string, projection RegistryProjection) error { - return ValidateSelectionCoverage(projection) -} - var ( pipelineBuildEffectiveRegistry = cli.BuildEffectiveCommandRegistry pipelineBindEffectiveRegistry = cli.BindEffectiveCommandRegistry pipelineGenerateMetadata = Generate ) -func selectionHintCoverageRequired(expectedProducts, expectedTools map[string]bool) bool { +func selectionCoverageRequired(expectedProducts, expectedTools map[string]bool) bool { for _, include := range expectedProducts { if include { return true @@ -203,11 +209,3 @@ func GenerateFromCommandRoot(rootPath string, commandRoot *cobra.Command, opts O } return metadata, stats, projection, nil } - -func resolvePipelineRootPath(root, path string) string { - path = strings.TrimSpace(path) - if filepath.IsAbs(path) { - return path - } - return filepath.Join(root, path) -} diff --git a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go index 16cb0c24..aecac1dd 100644 --- a/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go +++ b/internal/generator/cmd_schema_agent_metadata/coverage_more_test.go @@ -271,7 +271,7 @@ func TestCrossPlatformCoverageMetadataRegistryAndSelectionFailureEdges(t *testin root := t.TempDir() registry := commandRegistryProjection{CanonicalToolPaths: map[string]string{"sample.run": "sample run"}} - if err := validateSelectionHintInput(root, "", registry); err == nil || !strings.Contains(err.Error(), "ProductDecl/ContractFinal selection coverage incomplete") { + if err := validateSelectionCoverage(root, "", registry); err == nil || !strings.Contains(err.Error(), "ProductDecl/ContractFinal selection coverage incomplete") { t.Fatalf("missing ContractFinal coverage error = %v", err) } @@ -295,9 +295,9 @@ func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredTools(t *testing. "sample.run": {PrimaryCommand: declared}, }}, } - expected := agentmetadata.SelectionHintCoverageTools(registry) + expected := agentmetadata.SelectionCoverageTools(registry) if expected["sample.run"] || !expected["sample.get"] { - t.Fatalf("declared tool must be exempt from hint coverage, expected = %#v", expected) + t.Fatalf("declared tool must be exempt from selection coverage, expected = %#v", expected) } } @@ -311,10 +311,10 @@ func TestCrossPlatformCoverageSelectionHintInputExemptsDeclaredProducts(t *testi AvoidWhen: []string{"avoid declared"}, }, }) - expected := agentmetadata.SelectionHintCoverageProducts(commandRegistryProjection{ + expected := agentmetadata.SelectionCoverageProducts(commandRegistryProjection{ ProductIDs: map[string]bool{"declared": true, "hinted": true}, }) if expected["declared"] || !expected["hinted"] { - t.Fatalf("ProductDecl product must be exempt from hint coverage, expected = %#v", expected) + t.Fatalf("ProductDecl product must be exempt from selection coverage, expected = %#v", expected) } } diff --git a/internal/generator/cmd_schema_agent_metadata/cross_platform_coverage_test.go b/internal/generator/cmd_schema_agent_metadata/cross_platform_coverage_test.go index 17bafa82..4ba7ddbd 100644 --- a/internal/generator/cmd_schema_agent_metadata/cross_platform_coverage_test.go +++ b/internal/generator/cmd_schema_agent_metadata/cross_platform_coverage_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/agentmetadata" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/generator/outputguard" ) func TestCrossPlatformCoverageMetadataMainWithHintsProtectedInput(t *testing.T) { @@ -130,22 +131,24 @@ func TestCrossPlatformCoverageValidateCommandRegistryFileDirectory(t *testing.T) } } -func TestCrossPlatformCoverageValidateManualHintsOutputIsolationWrapper(t *testing.T) { +func TestCrossPlatformCoverageValidateAgentMetadataOutputIsolationWrapper(t *testing.T) { root := t.TempDir() hintsDir := filepath.Join(root, "hints") if err := os.MkdirAll(hintsDir, 0o755); err != nil { t.Fatal(err) } - if err := validateManualHintsOutputIsolation(root, "hints", "", filepath.Join(root, "out"), ""); err != nil { - t.Fatalf("validateManualHintsOutputIsolation() error = %v", err) - } -} - -func TestCrossPlatformCoverageValidateAgentMetadataOutputIsolationWrapper(t *testing.T) { - root := t.TempDir() - if err := validateAgentMetadataOutputIsolation(root, nil, "", filepath.Join(root, "out"), ""); err != nil { + if err := validateAgentMetadataOutputIsolation( + root, + []outputguard.Input{{Name: "structured hint input directory", Path: "hints"}}, + "", + filepath.Join(root, "out"), + "", + ); err != nil { t.Fatalf("validateAgentMetadataOutputIsolation() error = %v", err) } + if err := validateAgentMetadataOutputIsolation(root, nil, "", filepath.Join(root, "out"), ""); err != nil { + t.Fatalf("validateAgentMetadataOutputIsolation(nil inputs) error = %v", err) + } } func TestCrossPlatformCoverageValidateAgentMetadataOutputAllowlistWrapper(t *testing.T) { diff --git a/internal/generator/cmd_schema_agent_metadata/main.go b/internal/generator/cmd_schema_agent_metadata/main.go index 28411ea2..99363530 100644 --- a/internal/generator/cmd_schema_agent_metadata/main.go +++ b/internal/generator/cmd_schema_agent_metadata/main.go @@ -33,7 +33,7 @@ var ( validateMetadataAllowlist = validateAgentMetadataOutputAllowlist validateMetadataRegistryFile = validateCommandRegistryFile loadMetadataRegistryProjection = loadEffectiveCommandRegistryProjection - validateMetadataSelection = validateSelectionHintInput + validateMetadataSelection = validateSelectionCoverage generateAgentMetadata = agentmetadata.Generate writeMetadataDirectoryOutput = writeMetadataDirectory writeMetadataFileOutput = writeMetadataFile @@ -168,7 +168,7 @@ func main() { } _, _ = fmt.Fprintf( os.Stderr, - "generated schema Agent metadata: output=%s sources=%d products=%d tools=%d summaries=%d interface_summaries=%d intents=%d examples=%d risk_rules=%d hint_files=%d hint_tools=%d unmatched=%d surface_tools=%d\n", + "generated schema Agent metadata: output=%s sources=%d products=%d tools=%d summaries=%d interface_summaries=%d intents=%d examples=%d risk_rules=%d unmatched=%d surface_tools=%d\n", delivery, stats.SourceFiles, stats.Products, @@ -178,8 +178,6 @@ func main() { stats.ToolIntents, stats.Examples, stats.RiskRules, - stats.HintFiles, - stats.HintTools, stats.UnmatchedTools, registry.ToolCount, ) @@ -408,8 +406,8 @@ func mergeRegistryShards(dir string) ([]byte, error) { return json.Marshal(result) } -func validateSelectionHintInput(rootPath, hintsDir string, registry commandRegistryProjection) error { - return agentmetadata.ValidateSelectionHints(rootPath, hintsDir, registry) +func validateSelectionCoverage(_ string, _ string, registry commandRegistryProjection) error { + return agentmetadata.ValidateSelectionCoverage(registry) } func resolveRootPath(root, path string) string { @@ -420,15 +418,6 @@ func resolveRootPath(root, path string) string { return filepath.Join(root, path) } -// validateManualHintsOutputIsolation protects reviewed human-authored hint -// inputs before the generator writes any output. -func validateManualHintsOutputIsolation(rootPath, hintsDir, outputPath, outputDir, auditOutputPath string) error { - return validateAgentMetadataOutputIsolation(rootPath, - []outputguard.Input{{Name: "structured hint input directory", Path: hintsDir}}, - outputPath, outputDir, auditOutputPath, - ) -} - func validateAgentMetadataOutputIsolation(rootPath string, inputs []outputguard.Input, outputPath, outputDir, auditOutputPath string) error { return outputguard.Validate(rootPath, inputs, diff --git a/internal/generator/cmd_schema_agent_metadata/main_test.go b/internal/generator/cmd_schema_agent_metadata/main_test.go index cff7953d..293f4f80 100644 --- a/internal/generator/cmd_schema_agent_metadata/main_test.go +++ b/internal/generator/cmd_schema_agent_metadata/main_test.go @@ -168,7 +168,7 @@ func TestWriteMetadataDirectorySplitsDomains(t *testing.T) { } } -func TestValidateManualHintsOutputIsolationRejectsOverlaps(t *testing.T) { +func TestValidateAgentMetadataOutputIsolationRejectsHintOverlaps(t *testing.T) { root := t.TempDir() hintsRelative := filepath.Join("internal", "cli", "schema_hints") hintsPath := filepath.Join(root, hintsRelative) @@ -180,32 +180,33 @@ func TestValidateManualHintsOutputIsolationRejectsOverlaps(t *testing.T) { if err := os.WriteFile(marker, []byte(markerContents), 0o644); err != nil { t.Fatal(err) } - err := validateManualHintsOutputIsolation(root, hintsRelative, marker, "", "") + inputs := []outputguard.Input{{Name: "structured hint input directory", Path: hintsRelative}} + err := validateAgentMetadataOutputIsolation(root, inputs, marker, "", "") if err == nil || !strings.Contains(err.Error(), "-output") { - t.Fatalf("validateManualHintsOutputIsolation() error = %v", err) + t.Fatalf("validateAgentMetadataOutputIsolation() error = %v", err) } - err = validateManualHintsOutputIsolation(root, hintsRelative, "", hintsPath, "") + err = validateAgentMetadataOutputIsolation(root, inputs, "", hintsPath, "") if err == nil || !strings.Contains(err.Error(), "structured hint") { - t.Fatalf("validateManualHintsOutputIsolation(dir) error = %v", err) + t.Fatalf("validateAgentMetadataOutputIsolation(dir) error = %v", err) } } -func TestValidateManualHintsOutputIsolationAllowsSeparateTargets(t *testing.T) { +func TestValidateAgentMetadataOutputIsolationAllowsSeparateHintTargets(t *testing.T) { root := t.TempDir() hintsRelative := filepath.Join("internal", "cli", "schema_hints") hintsPath := filepath.Join(root, hintsRelative) if err := os.MkdirAll(filepath.Join(hintsPath, "selection"), 0o755); err != nil { t.Fatal(err) } - err := validateManualHintsOutputIsolation( + err := validateAgentMetadataOutputIsolation( root, - hintsRelative, + []outputguard.Input{{Name: "structured hint input directory", Path: hintsRelative}}, filepath.Join(root, "metadata.json"), filepath.Join(root, "split"), filepath.Join(root, "audit", "metadata-audit.json"), ) if err != nil { - t.Fatalf("validateManualHintsOutputIsolation() error = %v", err) + t.Fatalf("validateAgentMetadataOutputIsolation() error = %v", err) } } @@ -248,32 +249,32 @@ func TestValidateAgentMetadataOutputIsolationProtectsAllSourceKinds(t *testing.T } } -func TestValidateSelectionHintInputAllowsMissingWhenCoverageEmpty(t *testing.T) { +func TestValidateSelectionCoverageAllowsEmptyProjection(t *testing.T) { root := t.TempDir() - if err := validateSelectionHintInput(root, "internal/cli/schema_hints", commandRegistryProjection{}); err != nil { - t.Fatalf("missing selection/ must be optional when coverage is empty: %v", err) + if err := validateSelectionCoverage(root, "internal/cli/schema_hints", commandRegistryProjection{}); err != nil { + t.Fatalf("empty coverage projection must pass: %v", err) } } -func TestValidateSelectionHintInputRequiresContractFinalWhenCoverageRemains(t *testing.T) { +func TestValidateSelectionCoverageRequiresContractFinalWhenCoverageRemains(t *testing.T) { root := t.TempDir() registry := commandRegistryProjection{ CanonicalToolPaths: map[string]string{"sample.run": "sample run"}, ProductIDs: map[string]bool{"sample": true}, } - err := validateSelectionHintInput(root, "", registry) + err := validateSelectionCoverage(root, "", registry) if err == nil || !strings.Contains(err.Error(), "ProductDecl/ContractFinal selection coverage incomplete") { - t.Fatalf("validateSelectionHintInput() error = %v", err) + t.Fatalf("validateSelectionCoverage() error = %v", err) } } -func TestValidateSelectionHintInputAllowsMissingMetadataDir(t *testing.T) { +func TestValidateSelectionCoverageIgnoresHintsDirArg(t *testing.T) { root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "hints", "selection"), 0o755); err != nil { t.Fatal(err) } - if err := validateSelectionHintInput(root, "hints", commandRegistryProjection{}); err != nil { - t.Fatalf("missing metadata/ must be optional: %v", err) + if err := validateSelectionCoverage(root, "hints", commandRegistryProjection{}); err != nil { + t.Fatalf("hintsDir arg is ignored for coverage: %v", err) } } diff --git a/scripts/ci/test-packages.sh b/scripts/ci/test-packages.sh index 041497d1..c8a36627 100755 --- a/scripts/ci/test-packages.sh +++ b/scripts/ci/test-packages.sh @@ -6,7 +6,7 @@ MODULE="$(cd "$ROOT" && go list -m)" usage() { printf '%s\n' \ - "usage: $0 list " \ + "usage: $0 list " \ " $0 verify" >&2 exit 2 } @@ -25,6 +25,10 @@ list_shard() { helpers) go list ./internal/helpers/... ;; + cli) + # Owns package-cli Schema TestMain assembly (cmd_schema_catalog dump). + go list ./internal/cli/... + ;; release-scripts) go list ./test/scripts/... ;; @@ -35,6 +39,7 @@ list_shard() { "$MODULE/internal/app"|"$MODULE/internal/app/"*) ;; "$MODULE/internal/generator"|"$MODULE/internal/generator/"*) ;; "$MODULE/internal/helpers"|"$MODULE/internal/helpers/"*) ;; + "$MODULE/internal/cli"|"$MODULE/internal/cli/"*) ;; "$MODULE/test/scripts"|"$MODULE/test/scripts/"*) ;; *) printf '%s\n' "$package" ;; esac @@ -64,7 +69,7 @@ verify_plan() { LC_ALL=C sort -u "$all_packages" > "$expected" : > "$assigned" - for shard in app generators helpers remaining release-scripts; do + for shard in app generators helpers cli remaining release-scripts; do shard_packages="$workdir/$shard" unsorted_shard_packages="$workdir/$shard.unsorted" list_shard "$shard" > "$unsorted_shard_packages" diff --git a/scripts/dev/migrate_product_params.py b/scripts/dev/migrate_product_params.py deleted file mode 100644 index 61b0ab51..00000000 --- a/scripts/dev/migrate_product_params.py +++ /dev/null @@ -1,639 +0,0 @@ -#!/usr/bin/env python3 -"""Migrate one product's parameter overlays from hints into ParamDecl declarations. - -Usage: python3 scripts/dev/migrate_product_params.py - -For each tool with parameter overlays in schema_hints/metadata/.json: -1. Finds the DeclareLeafMetadata block for the tool's reviewed cli_path. -2. Inserts Parameters: []corecmd.ParamDecl{...} into the Schema block. -3. Moves any overlaid flag registrations before the DeclareLeafMetadata call. -4. Clears the tool's parameters from the hints file. - -Shared-RPC siblings (e.g. drive rename / doc rename both use rename_document, -mail message list / search both call search_emails) MUST be disambiguated by -cli_path. If cli_path cannot uniquely select a leaf, the script refuses to -insert or clear hints — never pick the first RPC hit. - -Run for one product at a time; verify compilation and schema output between runs. -""" -import json -import os -import re -import sys - -HELPER_DIR = "internal/helpers" -HINTS_DIR = "internal/cli/schema_hints/metadata" - -# Primary file per product (used for hints clearing only; RPCName search covers all files) -PRODUCT_FILES = { - "aisearch": "aisearch.go", "aitable": "aitable.go", "attendance": "attendance.go", - "chat": "chat.go", "contact": "contact.go", "devdoc": "devdoc.go", - "doc": "doc.go", "drive": "drive.go", "hrbrain": "hrbrain.go", - "mail": "mail.go", "markdown": "markdown.go", "minutes": "minutes.go", - "report": "report.go", "sheet": "sheet.go", "todo": "todo.go", -} - - -def all_helper_files(): - """Return all non-test .go files in internal/helpers/.""" - files = [] - for f in sorted(os.listdir(HELPER_DIR)): - if f.endswith(".go") and not f.endswith("_test.go"): - files.append(os.path.join(HELPER_DIR, f)) - return files - - -def helper_files_for_product(product): - """Prefer the product primary file, then the rest of helpers.""" - files = all_helper_files() - primary = PRODUCT_FILES.get(product) - if not primary: - return files - preferred = os.path.join(HELPER_DIR, primary) - ordered = [] - if preferred in files: - ordered.append(preferred) - for fp in files: - if fp != preferred: - ordered.append(fp) - return ordered - - -def bool_ptr(v): - return "boolPtr(true)" if v else "boolPtr(false)" - - -def render_param(name, ov): - parts = [f'Name: "{name}"'] - if "property" in ov: - parts.append(f'Property: "{ov["property"]}"') - if "required" in ov: - parts.append(f"Required: {bool_ptr(ov['required'])}") - if "interface_type" in ov: - parts.append(f'InterfaceType: "{ov["interface_type"]}"') - if "description" in ov: - parts.append(f'Description: "{ov["description"]}"') - if "required_when" in ov: - parts.append(f'RequiredWhen: "{ov["required_when"]}"') - if "enum" in ov and ov["enum"]: - vals = ", ".join(f'"{v}"' for v in ov["enum"]) - parts.append(f"Enum: []string{{{vals}}}") - return "\t\t\t\t{" + ", ".join(parts) + "}," - - -def find_block_end(lines, start_idx): - """Find the line index of the closing '})' or '},' for a block starting at start_idx.""" - depth = 0 - for i in range(start_idx, len(lines)): - depth += lines[i].count("{") - lines[i].count("}") - if depth <= 0 and i > start_idx: - return i - return -1 - - -def find_cmd_var_for_call(lines, call_line): - """Find the nearest cmd := &cobra.Command{ whose block contains call_line.""" - candidates = [] - for i in range(call_line, -1, -1): - m = re.search(r'(\w+)\s*:?=\s*&cobra\.Command\s*{', lines[i]) - if not m: - continue - end = find_block_end(lines, i) - if end >= call_line: - return m.group(1), i - # Keep searching further back; an outer command may still enclose us. - candidates.append((m.group(1), i)) - return "", -1 - - -def find_declare_for_var(lines, var_name, after_line=0): - """Find DeclareLeafMetadata(var_name, ...) at or after after_line.""" - if not var_name: - return -1 - pat = re.compile(r'DeclareLeafMetadata\(\s*' + re.escape(var_name) + r'\s*,') - for i in range(max(after_line, 0), len(lines)): - if pat.search(lines[i]): - return i - return -1 - - -# Minimum score to accept a multi-leaf shared-RPC candidate. -# Example/Examples primary invoke = 100; Use+product file = 40. -SCORE_MULTI_LEAF_MIN = 40 - - -def cli_path_tokens(cli_path): - """Normalize a reviewed cli_path into path tokens (no flags).""" - if not cli_path: - return [] - # "mail message list" or "minutes +replace-batch" - parts = [] - for tok in str(cli_path).split(): - if tok.startswith("-"): - break - parts.append(tok) - return parts - - -def example_blobs(block_text): - """Extract Example / Examples string bodies (ignore UseWhen/AvoidWhen prose).""" - blobs = [] - for m in re.finditer(r"Example:\s*`([^`]*)`", block_text, re.S): - blobs.append(m.group(1)) - for m in re.finditer(r"Examples:\s*\[\]string\{([^{}]*)\}", block_text, re.S): - blobs.append(m.group(1)) - return "\n".join(blobs) - - -def file_matches_cli_product(fp, product): - """True when helper/shortcut file clearly belongs to the cli_path product.""" - if not product or not fp: - return False - base = os.path.basename(fp) - norm = fp.replace("\\", "/") - if base == f"{product}.go" or base.startswith(f"{product}_"): - return True - if f"/shortcut/{product}/" in norm or norm.endswith(f"/shortcut/{product}.go"): - return True - return False - - -def score_block_for_cli_path(block_text, cli_path, fp=""): - """Score how strongly a command/Declare block owns reviewed cli_path. - - Sibling AvoidWhen/UseWhen often mention the other leaf's path (e.g. doc rename - says "prefer dws drive rename"). Bare substring matching is therefore unsafe. - Only Example/Examples primary invokes and Use+product-file evidence count. - """ - tokens = cli_path_tokens(cli_path) - if not tokens: - return 0 - path = " ".join(tokens) - product = tokens[0] - leaf = tokens[-1] - score = 0 - - examples = example_blobs(block_text) - # Primary: this leaf's Example documents `dws ...` - if re.search( - r'(?:^|[\s"\'`])dws\s+' + re.escape(path) + r'(?:\s|--|["\'`]|$)', - examples, - re.M, - ): - score += 100 - - if re.search(r'Use:\s*"' + re.escape(leaf) + r'"', block_text): - if file_matches_cli_product(fp, product): - score += 40 - elif len(tokens) >= 2 and tokens[-2] in block_text: - # Same-file siblings (mail list/search): Use leaf differs; parent token - # is weak confirmation only and must not beat Example evidence. - score += 10 - else: - score += 5 - - return score - - -def block_matches_cli_path(block_text, cli_path, fp=""): - """True when score shows clear ownership of cli_path (not bare substring).""" - return score_block_for_cli_path(block_text, cli_path, fp) >= SCORE_MULTI_LEAF_MIN - - -def collect_rpc_sites(file_contents, rpc_name): - """Return [(file, line_idx)] for every RPCName / callMCPTool site.""" - rpc_pattern = re.compile(r'RPCName:\s*"' + re.escape(rpc_name) + '"') - call_patterns = [ - re.compile(r'callMCPTool\("' + re.escape(rpc_name) + '"'), - re.compile(r'callMCPToolOnServer\([^,]+,\s*"' + re.escape(rpc_name) + '"'), - re.compile(r'callAitableTool\("' + re.escape(rpc_name) + '"'), - re.compile(r'rt\.CallMCP\w*\("' + re.escape(rpc_name) + '"'), - ] - sites = [] - for fp, src in file_contents.items(): - lines_src = src.split("\n") - for i, line in enumerate(lines_src): - if rpc_pattern.search(line) or any(cp.search(line) for cp in call_patterns): - sites.append((fp, i)) - return sites - - -def _dedupe_candidates(candidates): - """Keep highest-scoring candidate per DeclareLeafMetadata site.""" - best = {} - for fp, decl_start, cmd_var, score, call_line in candidates: - key = (fp, decl_start) - prev = best.get(key) - if prev is None or score > prev[3]: - best[key] = (fp, decl_start, cmd_var, score, call_line) - return list(best.values()) - - -def _pick_unique_candidate(tool_name, rpc_name, cli_path, candidates, *, multi_leaf): - """Return (fp, decl_start, cmd_var) or (None, -1, "") when unsafe.""" - ranked = sorted(candidates, key=lambda c: (-c[3], c[0], c[1])) - if not ranked: - return None, -1, "" - - if multi_leaf: - if not cli_path: - print(f" WARNING: {tool_name} — RPC '{rpc_name}' has multiple DeclareLeafMetadata " - f"candidates; reviewed cli_path is required for disambiguation") - return None, -1, "" - if ranked[0][3] < SCORE_MULTI_LEAF_MIN: - print(f" WARNING: {tool_name} — RPC '{rpc_name}' has {len(ranked)} leaf candidates; " - f"cli_path={cli_path!r} did not uniquely select a leaf; refusing to insert") - return None, -1, "" - top = [c for c in ranked if c[3] == ranked[0][3]] - if len(top) > 1: - print(f" WARNING: {tool_name} — ambiguous cli_path match for RPC '{rpc_name}' " - f"({len(top)} DeclareLeafMetadata candidates at score {ranked[0][3]}); " - f"refusing to insert") - return None, -1, "" - fp, decl_start, cmd_var, _, _ = top[0] - return fp, decl_start, cmd_var - - # Single leaf: allow RPC-only resolution (no cli_path required). - fp, decl_start, cmd_var, _, _ = ranked[0] - return fp, decl_start, cmd_var - - -def resolve_declare_target(file_contents, tool_name, rpc_name, cli_path): - """Resolve (file, decl_start, cmd_var) for a tool, or (None, -1, ""). - - Shared RPCs (e.g. mail list/search both call search_emails) must be - disambiguated by reviewed cli_path. Never return the first RPC hit blindly. - """ - sites = collect_rpc_sites(file_contents, rpc_name) - candidates = [] # (fp, decl_start, cmd_var, score, call_line) - - for fp, call_line in sites: - lines = file_contents[fp].split("\n") - cmd_var, cmd_line = find_cmd_var_for_call(lines, call_line) - decl_start = find_declare_for_var(lines, cmd_var, after_line=cmd_line) if cmd_var else -1 - if decl_start < 0: - continue - block_end = find_block_end(lines, decl_start) - # Include the command literal block for Use/Example matching. - window_start = cmd_line if cmd_line >= 0 else decl_start - block_text = "\n".join(lines[window_start:block_end + 1]) - score = score_block_for_cli_path(block_text, cli_path, fp) - candidates.append((fp, decl_start, cmd_var, score, call_line)) - - candidates = _dedupe_candidates(candidates) - multi_leaf = len(candidates) > 1 - if candidates: - picked = _pick_unique_candidate( - tool_name, rpc_name, cli_path, candidates, multi_leaf=multi_leaf) - if picked[0] is not None: - return picked - if multi_leaf: - return None, -1, "" - - # Strategy: composite / shortcut helpers keyed by cli_path or Use leaf. - tokens = cli_path_tokens(cli_path) - use_name = tokens[-1] if tokens else tool_name.split(".", 1)[-1].replace("_", "-") - for prefix in ("shortcut-", "+"): - if use_name.startswith(prefix): - use_name = use_name[len(prefix):] - use_pat = re.compile(r'Use:\s*"' + re.escape(use_name) + '"') - use_hits = [] - for fp, src in file_contents.items(): - lines_src = src.split("\n") - for i, line in enumerate(lines_src): - if not use_pat.search(line): - continue - cmd_var, cmd_line = find_cmd_var_for_call(lines_src, i) - decl_start = find_declare_for_var(lines_src, cmd_var, after_line=cmd_line) if cmd_var else -1 - if decl_start < 0: - # shortcut.FromShortcut path: Schema may be on Shortcut literal. - continue - block_end = find_block_end(lines_src, decl_start) - window_start = cmd_line if cmd_line >= 0 else decl_start - block_text = "\n".join(lines_src[window_start:block_end + 1]) - score = score_block_for_cli_path(block_text, cli_path, fp) - use_hits.append((fp, decl_start, cmd_var, score, i)) - use_hits = _dedupe_candidates(use_hits) - if use_hits: - picked = _pick_unique_candidate( - tool_name, rpc_name, cli_path, use_hits, multi_leaf=len(use_hits) > 1) - if picked[0] is not None: - return picked - return None, -1, "" - - print(f" WARNING: {tool_name} — RPCName '{rpc_name}' not found in any helper file") - return None, -1, "" - - -def schema_has_param_decls(schema_text, params): - """True when Schema already declares every overlay flag Name.""" - if "Parameters:" not in schema_text: - return False - for pname in params: - if not re.search(r'Name:\s*"' + re.escape(pname) + r'"', schema_text): - return False - return True - - -def _self_test_fixtures(): - """Synthetic shared-RPC helpers mirroring mail list/search and doc/drive rename.""" - mail_src = r''' - messageSearchCmd := &cobra.Command{ - Use: "search", - Example: ` dws mail message search --email user@company.com --query "subject:周报"`, - RunE: func(cmd *cobra.Command, args []string) error { - return callMCPTool("search_emails", toolArgs) - }, - } - DeclareLeafMetadata(messageSearchCmd, LeafSpec{ - Contract: ContractDecl{ - Interface: &contract.InterfaceSpec{ - Mode: "mcp", - Ref: &contract.InterfaceRefSpec{ProductID: "mail", RPCName: "search_emails"}, - }, - Selection: contract.SelectionSpec{ - AvoidWhen: []string{ - "只列某文件夹邮件且无需复杂条件时优先 mail message list", - }, - Examples: []string{"dws mail message search --email user@company.com --query \"subject:周报\""}, - }, - }, - }) - - messageListCmd := &cobra.Command{ - Use: "list", - Example: ` dws mail message list --email user@company.com`, - RunE: func(cmd *cobra.Command, args []string) error { - return callMCPTool("search_emails", toolArgs) - }, - } - DeclareLeafMetadata(messageListCmd, LeafSpec{ - Contract: ContractDecl{ - Interface: &contract.InterfaceSpec{ - Mode: "composite", - Reason: "wrapper around search_emails", - }, - Selection: contract.SelectionSpec{ - AvoidWhen: []string{"需要组合条件时用 mail message search"}, - Examples: []string{ - "dws mail message list --email user@company.com", - }, - }, - }, - }) -''' - doc_src = r''' - renameCmd := &cobra.Command{ - Use: "rename", - Example: ` dws doc rename --node DOC_ID --name "新名称"`, - RunE: func(cmd *cobra.Command, args []string) error { - return callMCPTool("rename_document", map[string]any{}) - }, - } - DeclareLeafMetadata(renameCmd, LeafSpec{ - Contract: ContractDecl{ - Interface: &contract.InterfaceSpec{ - Mode: "mcp", - Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "rename_document"}, - }, - Selection: contract.SelectionSpec{ - AvoidWhen: []string{ - "文件或文件夹重命名优先用 dws drive rename,由该命令读取真实节点类型", - }, - Examples: []string{"dws doc rename --node --name \"新名称\" --format json"}, - }, - }, - }) -''' - drive_src = r''' - driveRenameCmd := &cobra.Command{ - Use: "rename", - Example: ` dws drive rename --node DOC_ID --name "新名称"`, - RunE: func(cmd *cobra.Command, args []string) error { - return callMCPToolOnServer("doc", "rename_document", map[string]any{}) - }, - } - DeclareLeafMetadata(driveRenameCmd, LeafSpec{ - Contract: ContractDecl{ - Interface: &contract.InterfaceSpec{ - Mode: "mcp", - Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "rename_document"}, - }, - Selection: contract.SelectionSpec{ - AvoidWhen: []string{ - "要改正文里的标题改用 dws doc block update,不要用 rename", - }, - Examples: []string{"dws drive rename --node --name \"新名称\" --format json"}, - }, - Parameters: []contract.ParamDecl{ - {Name: "name", Description: "drive-only extension stripping"}, - }, - }, - }) -''' - return { - "internal/helpers/mail.go": mail_src, - "internal/helpers/doc.go": doc_src, - "internal/helpers/drive.go": drive_src, - } - - -def run_self_test(): - """Verify shared-RPC cli_path disambiguation; exit 0/1.""" - fc = _self_test_fixtures() - cases = [ - ("mail.list_emails", "search_emails", "mail message list", "mail.go", "messageListCmd"), - ("mail.search_emails", "search_emails", "mail message search", "mail.go", "messageSearchCmd"), - ("drive.rename_document", "rename_document", "drive rename", "drive.go", "driveRenameCmd"), - ("doc.rename_document", "rename_document", "doc rename", "doc.go", "renameCmd"), - ] - failed = 0 - for tool, rpc, cli, want_file, want_var in cases: - fp, decl, var = resolve_declare_target(fc, tool, rpc, cli) - base = os.path.basename(fp) if fp else None - ok = base == want_file and var == want_var and decl >= 0 - status = "OK" if ok else "FAIL" - print(f" {status}: {tool} cli_path={cli!r} -> {base}:{var} (want {want_file}:{want_var})") - if not ok: - failed += 1 - - # Bare AvoidWhen cross-links must not make block_matches true alone. - doc_block = fc["internal/helpers/doc.go"] - if block_matches_cli_path(doc_block, "drive rename", "internal/helpers/doc.go"): - print(" FAIL: doc rename AvoidWhen must not own cli_path 'drive rename'") - failed += 1 - else: - print(" OK: doc AvoidWhen cross-link does not claim drive rename") - - # Multi-leaf without cli_path must refuse. - fp, decl, var = resolve_declare_target(fc, "mail.list_emails", "search_emails", "") - if fp is not None: - print(f" FAIL: multi-leaf without cli_path should refuse, got {fp}:{var}") - failed += 1 - else: - print(" OK: multi-leaf without cli_path refused") - - if failed: - print(f"self-test: {failed} failure(s)") - return False - print("self-test: all passed") - return True - - -def migrate_product(product): - hint_path = os.path.join(HINTS_DIR, f"{product}.json") - hints = json.load(open(hint_path, encoding="utf-8")) - tools = hints.get("tools") or {} - overlays = {t: (v or {}).get("parameters") or {} for t, v in tools.items()} - overlays = {t: p for t, p in overlays.items() if p} - if not overlays: - print(f"{product}: no overlays, skipping") - return True - - # Optional canonical-path → actual MCP tool name when they diverge. - RPC_ALIASES = { - "attendance.adjustment_search": "get_adjustment_rule", - "attendance.group_search": "get_simple_groups", - "attendance.overtime_search": "get_overtime_rule", - "mail.list_emails": "search_emails", - "devdoc.search_open_platform_docs_rag": "search_open_platform_docs", - } - - # Also search shortcut packages for composite / no-single-RPC commands. - file_contents = {} - for fp in helper_files_for_product(product): - file_contents[fp] = open(fp, encoding="utf-8").read() - shortcut_root = "internal/shortcut" - if os.path.isdir(shortcut_root): - for root, _, fns in os.walk(shortcut_root): - for fn in fns: - if fn.endswith(".go") and not fn.endswith("_test.go"): - fp = os.path.join(root, fn) - file_contents[fp] = open(fp, encoding="utf-8").read() - - modified_files = set() - flags_to_move = [] - migrated_tools = set() # only clear hints for tools we actually migrated - - for tool_name, params in sorted(overlays.items()): - meta = tools.get(tool_name) or {} - cli_path = (meta.get("cli_path") or "").strip() - rpc_name = RPC_ALIASES.get(tool_name, tool_name.split(".")[-1]) - if not cli_path: - # Shared RPCs cannot be safely resolved without reviewed cli_path. - print(f" WARNING: {tool_name} — parameters overlay missing cli_path; " - f"shared-RPC siblings will refuse multi-leaf resolution") - - found_file, decl_start, cmd_var = resolve_declare_target( - file_contents, tool_name, rpc_name, cli_path) - if not found_file or decl_start < 0: - continue - - lines = file_contents[found_file].split("\n") - - # Find Contract block (legacy Schema: names kept for unmigrated trees) - schema_start = -1 - for i in range(decl_start, min(decl_start + 80, len(lines))): - if ( - "Contract: ContractDecl{" in lines[i] - or "Contract: corecmd.ContractDecl{" in lines[i] - or "Contract: LeafContract{" in lines[i] - or "Schema: LeafSchema{" in lines[i] - or "Schema: corecmd.SchemaDecl{" in lines[i] - ): - schema_start = i - break - if schema_start < 0: - print(f" WARNING: {tool_name} — no Contract block in {os.path.basename(found_file)}") - continue - - schema_end = find_block_end(lines, schema_start) - schema_text = "\n".join(lines[schema_start:schema_end + 1]) - if schema_has_param_decls(schema_text, params): - # Only treat as migrated when THIS leaf already has the overlay names. - print(f" SKIP: {tool_name} — Parameters already declared on matched leaf " - f"(var={cmd_var}, cli_path={cli_path!r})") - migrated_tools.add(tool_name) - continue - if "Parameters:" in schema_text: - print(f" WARNING: {tool_name} — matched leaf already has Parameters but is missing " - f"overlay flags {sorted(params)}; refusing to clear hints (var={cmd_var})") - continue - - # Build and insert Parameters - param_lines = ["\t\t\tParameters: []corecmd.ParamDecl{"] - for pname in sorted(params): - param_lines.append(render_param(pname, params[pname])) - param_lines.append("\t\t\t},") - - insert_at = schema_end - for i in range(schema_end - 1, schema_start, -1): - stripped = lines[i].strip() - if stripped and not stripped.startswith("//"): - insert_at = i + 1 - break - - for j, pl in enumerate(param_lines): - lines.insert(insert_at + j, pl) - modified_files.add(found_file) - file_contents[found_file] = "\n".join(lines) - migrated_tools.add(tool_name) - print(f" OK: {tool_name} — inserted {len(params)} param decls in {os.path.basename(found_file)} " - f"at line {insert_at + 1} (var={cmd_var}, cli_path={cli_path!r})") - - # Check flag registration order - for pname in params: - flag_pattern = re.compile(r'\.Flags\(\)\.\w+\("' + re.escape(pname) + '"') - for i, line in enumerate(lines): - if flag_pattern.search(line): - if i > decl_start: - flags_to_move.append((pname, i, decl_start, os.path.basename(found_file))) - break - - # Clear hints ONLY for tools we migrated onto the matched leaf. - if migrated_tools: - for tool_name in migrated_tools: - if tool_name in hints.get("tools", {}): - hints["tools"][tool_name].pop("parameters", None) - if not hints["tools"][tool_name]: - del hints["tools"][tool_name] - if not hints.get("tools"): - hints["tools"] = {} - json.dump(hints, open(hint_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2) - print(f"{product}: cleared hints for {len(migrated_tools)} tools " - f"({len(overlays) - len(migrated_tools)} remaining)") - - if not modified_files: - if not migrated_tools: - print(f"{product}: no changes needed") - return True - - # Write modified files and ensure corecmd import - for fp in modified_files: - src = file_contents[fp] - if fp.startswith(HELPER_DIR) and "internal/corecmd" not in src: - m = re.search(r'(import \(\n(?:.*\n)*?\))', src) - if m: - block = m.group(1) - new_block = block.rstrip(")\n") + '\n\n\t"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"\n)' - src = src[:m.start()] + new_block + src[m.end():] - print(f" added corecmd import to {os.path.basename(fp)}") - open(fp, "w", encoding="utf-8").write(src) - - if flags_to_move: - print(f" ⚠ {len(flags_to_move)} flags registered AFTER DeclareLeafMetadata — need manual reorder:") - for fname, reg_line, decl_line, fname_file in flags_to_move: - print(f" --{fname} in {fname_file}: reg line {reg_line + 1} > decl line {decl_line + 1}") - - return True - - -if __name__ == "__main__": - if len(sys.argv) == 2 and sys.argv[1] in ("--self-test", "self-test"): - sys.exit(0 if run_self_test() else 1) - if len(sys.argv) != 2: - print("Usage: migrate_product_params.py ") - print(" migrate_product_params.py --self-test") - sys.exit(1) - ok = migrate_product(sys.argv[1]) - sys.exit(0 if ok else 1) diff --git a/scripts/policy/check-schema-binary.sh b/scripts/policy/check-schema-binary.sh index 37f4204c..a3688796 100755 --- a/scripts/policy/check-schema-binary.sh +++ b/scripts/policy/check-schema-binary.sh @@ -9,8 +9,9 @@ BIN="${DWS_BIN:-$ROOT/dws}" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT HUP INT TERM -# The release Catalog is committed as a per-product split; reassemble it into -# the single-document shape this check consumes. +# Assemble a fresh runtime Catalog dump (ResolveSchemaBuild via with-catalog.sh) +# into the single-document shape this check consumes. No committed +# schema_catalog/ split is consulted. catalog_combined="$tmp/catalog-combined.json" scripts/policy/with-catalog.sh >"$catalog_combined" diff --git a/scripts/policy/check-schema-catalog.sh b/scripts/policy/check-schema-catalog.sh index b7f3a33c..b4663b07 100755 --- a/scripts/policy/check-schema-catalog.sh +++ b/scripts/policy/check-schema-catalog.sh @@ -7,9 +7,10 @@ cd "$ROOT" . "$ROOT/scripts/policy/policy-runtime.sh" policy_prepare_runtime "$ROOT" -# The release Catalog is committed as a per-product split; reassemble it into -# the single-document shape (version + surface_hash + source_hash + catalog + -# tools) that the jq queries below consume. +# Assemble a fresh runtime Catalog dump (ResolveSchemaBuild via with-catalog.sh) +# into the single-document shape (version + surface_hash + source_hash + +# catalog + tools) that the jq queries below consume. No committed +# schema_catalog/ split is consulted. catalog="$(mktemp)" trap 'rm -f "$catalog"' EXIT HUP INT TERM scripts/policy/with-catalog.sh >"$catalog" diff --git a/scripts/policy/run-platform-coverage-gate.sh b/scripts/policy/run-platform-coverage-gate.sh index b63ae083..a5f524fb 100755 --- a/scripts/policy/run-platform-coverage-gate.sh +++ b/scripts/policy/run-platform-coverage-gate.sh @@ -96,7 +96,14 @@ else printf ' %s' "$@" printf '\n' for package in "$@"; do - tests="$(go test -list '^(TestAllShortcuts|TestCrossPlatformCoverage)' "$package")" + list_err="$TMP_ROOT/list.err" + if ! tests="$(go test -list '^(TestAllShortcuts|TestCrossPlatformCoverage)' "$package" 2>"$list_err")"; then + printf 'error: go test -list failed for %s\n' "$package" >&2 + if [ -s "$list_err" ]; then + cat "$list_err" >&2 + fi + exit 1 + fi if printf '%s\n' "$tests" | grep -Eq '^(TestAllShortcuts|TestCrossPlatformCoverage)'; then printf '%s\n' "$package" >>"$TEST_PACKAGES" fi