perf(schema): add CommandMeta index so ResolveMeta skips full catalog decode
Publish a compact schema_meta_index.json beside the catalog so help/selection lookups avoid decoding the full ToolSpec wire on the hot path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3e83ac18d1
commit
bb54761e50
16 files changed
+18036
-73
No files matched your search
@@ -12,9 +12,10 @@ unrelated work, and use `gofmt` for every modified Go file.
|
||||
- Check generated drift: `./scripts/policy/check-generated-drift.sh`
|
||||
- Check the Schema contract: `./scripts/policy/check-schema-catalog.sh`
|
||||
|
||||
Generated Schema JSON is committed under `schema_catalog/` only (`go:embed`).
|
||||
Change its source inputs and generators, then regenerate; do not hand-edit
|
||||
generated Catalog files. `schema_agent_metadata/` is retired: if that directory
|
||||
Generated Schema JSON is committed under `schema_catalog/` (ToolSpec wire) and
|
||||
`schema_meta_index.json` (CommandMeta summary for ResolveMeta), both
|
||||
`go:embed`. Change source inputs and generators, then regenerate; do not
|
||||
hand-edit generated Catalog or meta-index files. `schema_agent_metadata/` is retired: if that directory
|
||||
(or `schema_agent_metadata_audit.json`) is present, policy fails.
|
||||
`internal/cli/schema_command_registry.json` is different: it is a reviewed
|
||||
`CommandRegistry` source, not a generated snapshot. It is the single reviewed
|
||||
@@ -81,13 +82,16 @@ The Schema data flow is one way:
|
||||
└─ internal/cli/schema_catalog/
|
||||
(catalog.json + tools/<product>.json; split per product so
|
||||
concurrent feature PRs only rewrite their own shard)
|
||||
└─ dws schema list/product/group/leaf/--all
|
||||
└─ internal/cli/schema_meta_index.json
|
||||
(CommandMeta summary for ResolveMeta / leaf --help Safety)
|
||||
└─ dws schema list/product/group/leaf/--all
|
||||
|
||||
7. Runtime consumption (unified API)
|
||||
ResolveMeta(cliPath) → CommandMeta{Identity, Safety, Selection}
|
||||
└─ internal/cli/command_meta.go
|
||||
└─ all consumers (help, schema, agent, skill-gen) call this one function
|
||||
└─ backed by embedded catalog (sync.Once lazy map, O(1) lookup)
|
||||
└─ help / agent / skill-gen call this one function
|
||||
└─ backed by embedded schema_meta_index.json (sync.Once, O(1))
|
||||
└─ full ToolSpec / dws schema still use embedded schema_catalog/
|
||||
```
|
||||
|
||||
After binding there is no second identity source and no identity precedence
|
||||
@@ -123,6 +127,7 @@ The Schema system has two physically separated processes:
|
||||
metadata + parameter bindings + reviewed parameter concepts + cobra tree).
|
||||
`schema_hints/` is fully retired and must not reappear.
|
||||
- Output (delivery): `schema_catalog/` (per-product shards, `go:embed`) +
|
||||
`schema_meta_index.json` (CommandMeta summary) +
|
||||
`param_aliases_generated.go`. `schema_agent_metadata/` must not reappear.
|
||||
- Refresh MCP metadata: `make fetch-mcp-metadata` (iterates 26 MCP server
|
||||
endpoints, merges with previous data for cross-server interface_ref).
|
||||
@@ -132,9 +137,10 @@ The Schema system has two physically separated processes:
|
||||
**Consumption** (runtime, fast, read-only, unified API):
|
||||
- Entry point: `ResolveMeta(cliPath) → CommandMeta{Identity, Safety, Selection}`
|
||||
in `internal/cli/command_meta.go`.
|
||||
- Backed by embedded catalog (`embeddedSchemaCatalog()`, sync.Once, O(1) map).
|
||||
- Backed by embedded `schema_meta_index.json` (sync.Once, O(1) map). Full
|
||||
`embeddedSchemaCatalog()` loads only for `dws schema` / `--all` / ToolSpec.
|
||||
- Consumers: `--help` (Safety annotation via `RenderSafetyAnnotation`),
|
||||
`dws schema`, agent selection, future skill generation.
|
||||
agent selection, future skill generation; `dws schema` uses the Catalog.
|
||||
- `SafetyForCLIPath` delegates to `ResolveMeta` (backward compatible).
|
||||
|
||||
This split is architecturally isomorphic to Lark's typed metadata registry,
|
||||
|
||||
@@ -134,9 +134,10 @@ mock-mcp-smoke:
|
||||
test-schema-agent-examples:
|
||||
DWS_AGENT_EXAMPLES_DRY_RUN=1 $(GO) test -v -count=1 ./internal/app -run '^TestManualAgentExamplesDryRun$$'
|
||||
|
||||
# generate-schema rebuilds schema_catalog/ (+ param aliases). Agent metadata is
|
||||
# injected in-memory during catalog generation; schema_agent_metadata/ is retired
|
||||
# and must not remain as a delivery artifact (any temp write is removed below).
|
||||
# generate-schema rebuilds schema_catalog/, schema_meta_index.json (+ param
|
||||
# aliases). Agent metadata is injected in-memory during catalog generation;
|
||||
# schema_agent_metadata/ is retired and must not remain as a delivery artifact
|
||||
# (any temp write is removed below).
|
||||
generate-schema:
|
||||
@set -e; \
|
||||
registry_guard=$$(mktemp -d); \
|
||||
@@ -170,7 +171,8 @@ generate-schema:
|
||||
generate-schema-catalog:
|
||||
$(GO) run -a ./internal/generator/cmd_schema_catalog \
|
||||
-root . \
|
||||
-output internal/cli/schema_catalog
|
||||
-output internal/cli/schema_catalog \
|
||||
-meta-index internal/cli/schema_meta_index.json
|
||||
@rm -rf internal/cli/schema_agent_metadata internal/cli/schema_agent_metadata_audit.json
|
||||
|
||||
fetch-mcp-metadata:
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// Package catalogcodegen is a throwaway feasibility probe: it measures what it
|
||||
// costs to store one product's Schema catalog as compiled-in Go literals rather
|
||||
// than embedded JSON, which is how internal/shortcut stores its 376 definitions
|
||||
// (measured at 1.9ms/3MB versus the catalog's 1400ms/740MB).
|
||||
// (measured at 1.9ms/3MB versus the full catalog assemble ~294–360ms/~175MB;
|
||||
// ResolveMeta itself now uses schema_meta_index.json instead).
|
||||
//
|
||||
// The struct shape mirrors the runtime-consumed subset of schemaToolWire. It is
|
||||
// structurally equivalent rather than shared, because the real type is
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
// of the 6 generation layers a field comes from.
|
||||
//
|
||||
// This is the "simple consumption" half of the generation/consumption split:
|
||||
// - Generation (gen.go + internal/generator/): 6 inputs → catalog snapshot.
|
||||
// - Consumption (this file): catalog snapshot → ResolveMeta → CommandMeta.
|
||||
// - Generation (gen.go + internal/generator/): 6 inputs → catalog snapshot
|
||||
// + schema_meta_index.json (CommandMeta summary).
|
||||
// - Consumption (this file): meta index → ResolveMeta → CommandMeta.
|
||||
// Full embeddedSchemaCatalog() is reserved for dws schema / ToolSpec paths.
|
||||
|
||||
package cli
|
||||
|
||||
@@ -59,9 +61,16 @@ var (
|
||||
)
|
||||
|
||||
// initMetaByCLIPath builds the cli_path → CommandMeta lookup from the embedded
|
||||
// catalog. Runs once (sync.Once); the catalog is already decoded at package init.
|
||||
// CommandMeta summary index. It does not decode the full schema_catalog/.
|
||||
func initMetaByCLIPath() {
|
||||
metaByCLIPath = buildMetaByCLIPath(embeddedSchemaCatalog())
|
||||
runtimeEmbeddedSchemaMetaIndexLazyCount.Add(1)
|
||||
lookup, err := decodeSchemaMetaIndexLookup(embeddedSchemaMetaIndexJSON)
|
||||
if err != nil {
|
||||
runtimeEmbeddedSchemaMetaIndexErr = err
|
||||
metaByCLIPath = map[string]CommandMeta{}
|
||||
return
|
||||
}
|
||||
metaByCLIPath = lookup
|
||||
}
|
||||
|
||||
// buildMetaByCLIPath constructs the lookup from a loaded catalog.
|
||||
@@ -173,7 +182,10 @@ func registerCommandMetaAliases(lookup map[string]CommandMeta, metas []CommandMe
|
||||
// ResolveMeta returns the complete metadata for a command identified by its CLI
|
||||
// path (e.g. "dev app delete") or one of its compat aliases (e.g. "report list"
|
||||
// for "report inbox list"). Returns ok=false for commands not in the embedded
|
||||
// catalog (utility commands, hidden commands, shortcuts).
|
||||
// meta index (utility commands, hidden commands, shortcuts).
|
||||
//
|
||||
// ResolveMeta reads schema_meta_index.json only; it never triggers the full
|
||||
// embeddedSchemaCatalog() decode used by dws schema / --all.
|
||||
func ResolveMeta(cliPath string) (CommandMeta, bool) {
|
||||
metaByCLIPathOnce.Do(initMetaByCLIPath)
|
||||
m, ok := metaByCLIPath[strings.TrimSpace(cliPath)]
|
||||
|
||||
@@ -21,10 +21,10 @@ import (
|
||||
)
|
||||
|
||||
// CommandSafety holds the safety metadata for a CLI command, resolved at
|
||||
// runtime from the embedded schema catalog. This is a read-only view over the
|
||||
// catalog — NOT a second safety source. The catalog remains the single
|
||||
// authoritative reviewed source; this struct merely provides typed access for
|
||||
// consumers (help rendering, skill generation).
|
||||
// runtime via ResolveMeta (embedded schema_meta_index.json). This is a
|
||||
// read-only view — NOT a second safety source. The Catalog remains the
|
||||
// authoritative reviewed ToolSpec wire; the meta index is its Identity /
|
||||
// Safety / Selection projection for help and selection consumers.
|
||||
type CommandSafety struct {
|
||||
Effect string // read / write / destructive
|
||||
Risk string // low / medium / high
|
||||
|
||||
+3
-2
@@ -30,10 +30,11 @@
|
||||
// in-memory and does not write or embed that intermediate JSON directory.
|
||||
//
|
||||
// Generation outputs (embedded at build):
|
||||
// - schema_catalog/ per-product catalog shards
|
||||
// - schema_catalog/ per-product catalog shards (ToolSpec wire)
|
||||
// - schema_meta_index.json CommandMeta summary for ResolveMeta
|
||||
// - param_aliases_generated.go per-command parameter normalization
|
||||
|
||||
package cli
|
||||
|
||||
//go:generate go run -a ../generator/cmd_schema_catalog -root ../.. -output schema_catalog
|
||||
//go:generate go run -a ../generator/cmd_schema_catalog -root ../.. -output schema_catalog -meta-index schema_meta_index.json
|
||||
//go:generate go run ../generator/cmd_param_aliases -root ../.. -output param_aliases_generated.go
|
||||
@@ -90,7 +90,8 @@ func embeddedSchemaCatalog() loadedSchemaCatalog {
|
||||
|
||||
// materializeEmbeddedSchemaCatalogMaps fills Snapshot.Catalog/Tools from the
|
||||
// typed Registry for callers that still need the untyped delivery maps.
|
||||
// Production ResolveMeta does not call this; cold start stays on the typed path.
|
||||
// Production ResolveMeta does not call this (or embeddedSchemaCatalog); it
|
||||
// reads schema_meta_index.json instead.
|
||||
func materializeEmbeddedSchemaCatalogMaps() (loadedSchemaCatalog, error) {
|
||||
_ = embeddedSchemaCatalog()
|
||||
if runtimeEmbeddedSchemaCatalogErr != nil {
|
||||
@@ -385,9 +386,9 @@ func loadSchemaCatalogSnapshot(snapshot SchemaCatalogSnapshot) (loadedSchemaCata
|
||||
// check-generated-drift.sh, which regenerates and byte-compares the whole
|
||||
// artifact set — strictly stronger than this hash, since it also catches a
|
||||
// shard set that is self-consistent but stale. Recomputing it here re-marshals
|
||||
// every decoded map (measured: 86.9ms, 65MB, 997k allocs) on each invocation
|
||||
// that touches ResolveMeta, to re-derive a fact about an embedded artifact
|
||||
// that cannot change at runtime.
|
||||
// every decoded map (measured: 86.9ms, 65MB, 997k allocs) on each full
|
||||
// Catalog load, to re-derive a fact about an embedded artifact that cannot
|
||||
// change at runtime. ResolveMeta no longer loads this path.
|
||||
registry, index, err := schemaRegistryFromSnapshot(snapshot)
|
||||
if err != nil {
|
||||
return loadedSchemaCatalog{}, fmt.Errorf("load typed Schema registry: %w", err)
|
||||
|
||||
@@ -20,25 +20,26 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These benchmarks attribute the Schema consumption cost that ResolveMeta pays
|
||||
// on its first hit. Both production entry points are sync.Once memoized, so the
|
||||
// benchmarks call the underlying work directly — measuring the exported wrappers
|
||||
// would time one real run followed by N no-ops.
|
||||
// These benchmarks attribute Schema consumption cost. sync.Once wrappers are
|
||||
// memoized, so cold-path benches call the underlying work directly — measuring
|
||||
// the exported wrappers would time one real run followed by N no-ops.
|
||||
//
|
||||
// Cold-start attribution (Apple M3 Pro, benchtime=3x, 2026-08-01):
|
||||
//
|
||||
// Before (map[string]any + per-tool remarshal + typed round-trip):
|
||||
// Full Catalog (dws schema / --all only — not ResolveMeta):
|
||||
//
|
||||
// BenchmarkAssembleEmbeddedSchemaCatalog ~1091ms 732MB 9.6M allocs
|
||||
// BenchmarkCatalogStageDecodeShards ~154ms 145MB 1.4M allocs
|
||||
// BenchmarkCatalogStageTypedRegistry ~892ms 582MB 8.1M allocs
|
||||
// BenchmarkAssembleEmbeddedSchemaCatalog ~294–360ms ~175MB ~1.2M allocs
|
||||
// BenchmarkCatalogStageDecodeTyped ~160–173ms ~117MB
|
||||
// BenchmarkCatalogStageTypedRegistryFromWire ~80–120ms ~40MB
|
||||
//
|
||||
// After (JSON → schemaToolWire → ToolSpec; round-trip off in production):
|
||||
// CommandMeta summary index (ResolveMeta / leaf --help Safety):
|
||||
//
|
||||
// BenchmarkAssembleEmbeddedSchemaCatalog ~294ms 174MB 1.2M allocs (~3.7× / ~4.2× / ~7.8×)
|
||||
// BenchmarkCatalogStageDecodeTyped ~173ms 117MB 0.55M allocs
|
||||
// BenchmarkCatalogStageTypedRegistryFromWire ~120ms 40MB 0.45M allocs
|
||||
// BenchmarkResolveMetaSteadyState ~222ns 0B 0 allocs
|
||||
// BenchmarkResolveMetaFirstHit ~5.8ms ~3.5MB ~18k allocs
|
||||
// BenchmarkResolveMetaSteadyState ~24–167ns 0B 0 allocs
|
||||
//
|
||||
// Historical (pre typed-wire / pre meta index): AssembleEmbedded ~1091ms/732MB;
|
||||
// first ResolveMeta paid that full Catalog cost. loadcost_bench_test.go's old
|
||||
// ~1.4s/740MB figure is obsolete.
|
||||
//
|
||||
// Package tests still enable validateSnapshotTypedRoundTrip via
|
||||
// schema_snapshot_roundtrip_test.go; benches force it off so they measure the
|
||||
@@ -92,8 +93,8 @@ func BenchmarkSchemaRegistryFromSnapshot(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkBuildMetaByCLIPath measures turning the decoded snapshot into the
|
||||
// ResolveMeta lookup map, isolated from the decode above.
|
||||
// BenchmarkBuildMetaByCLIPath measures projecting CommandMeta from a decoded
|
||||
// Catalog registry (consistency-gate path), isolated from Catalog decode.
|
||||
func BenchmarkBuildMetaByCLIPath(b *testing.B) {
|
||||
loaded := embeddedSchemaCatalog()
|
||||
if len(loaded.Registry.Products) == 0 {
|
||||
@@ -108,11 +109,30 @@ func BenchmarkBuildMetaByCLIPath(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkResolveMetaFirstHit measures decoding the committed CommandMeta
|
||||
// summary index into the ResolveMeta lookup. This is the cold cost leaf
|
||||
// --help Safety / ResolveMeta pay; it must not assemble the full Catalog.
|
||||
func BenchmarkResolveMetaFirstHit(b *testing.B) {
|
||||
if len(embeddedSchemaMetaIndexJSON) == 0 {
|
||||
b.Skip("embedded meta index unavailable")
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
lookup, err := decodeSchemaMetaIndexLookup(embeddedSchemaMetaIndexJSON)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if _, ok := lookup["dev app delete"]; !ok {
|
||||
b.Fatal("first-hit lookup missed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkResolveMetaSteadyState measures a warm lookup, i.e. what every
|
||||
// --help Safety annotation costs once the map exists.
|
||||
func BenchmarkResolveMetaSteadyState(b *testing.B) {
|
||||
if _, ok := ResolveMeta("dev app delete"); !ok {
|
||||
b.Skip("embedded catalog unavailable")
|
||||
b.Skip("embedded meta index unavailable")
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
@@ -8,6 +8,7 @@ package cli
|
||||
// parsed for the first time.
|
||||
type SchemaMetadataLoadCounts struct {
|
||||
Catalog uint64
|
||||
MetaIndex uint64
|
||||
AgentMetadata uint64
|
||||
MCPMetadata uint64
|
||||
ParameterBinding uint64
|
||||
@@ -18,6 +19,7 @@ type SchemaMetadataLoadCounts struct {
|
||||
func RuntimeSchemaMetadataLoadCounts() SchemaMetadataLoadCounts {
|
||||
return SchemaMetadataLoadCounts{
|
||||
Catalog: runtimeEmbeddedSchemaCatalogLazyLoadCount.Load(),
|
||||
MetaIndex: runtimeEmbeddedSchemaMetaIndexLazyCount.Load(),
|
||||
AgentMetadata: runtimeEmbeddedAgentMetadataLazyLoadCount.Load(),
|
||||
MCPMetadata: runtimeEmbeddedMCPMetadataLazyLoadCount.Load(),
|
||||
ParameterBinding: runtimeSchemaParameterBindingsLazyLoadCount.Load(),
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// SchemaMetaIndexVersion is the committed CommandMeta summary index format.
|
||||
const SchemaMetaIndexVersion = 1
|
||||
|
||||
//go:embed schema_meta_index.json
|
||||
var embeddedSchemaMetaIndexJSON []byte
|
||||
|
||||
// SchemaMetaIndexSnapshot is the generated ResolveMeta delivery artifact. It
|
||||
// carries only Identity / Safety / Selection — not parameters, provenance, or
|
||||
// interface disposition. schema_catalog/ remains the full ToolSpec wire.
|
||||
type SchemaMetaIndexSnapshot struct {
|
||||
Version int `json:"version"`
|
||||
SourceHash string `json:"source_hash"`
|
||||
SurfaceHash string `json:"surface_hash,omitempty"`
|
||||
Entries []SchemaMetaIndexEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// SchemaMetaIndexEntry is one primary-path CommandMeta record. Aliases are
|
||||
// expanded into the ResolveMeta lookup at decode time.
|
||||
type SchemaMetaIndexEntry struct {
|
||||
CLIPath string `json:"cli_path"`
|
||||
Canonical string `json:"canonical_path"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
ProductID string `json:"product_id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Effect string `json:"effect,omitempty"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
Confirmation string `json:"confirmation,omitempty"`
|
||||
Idempotency string `json:"idempotency,omitempty"`
|
||||
AgentSummary string `json:"agent_summary,omitempty"`
|
||||
UseWhen []string `json:"use_when,omitempty"`
|
||||
AvoidWhen []string `json:"avoid_when,omitempty"`
|
||||
Examples []string `json:"examples,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
runtimeEmbeddedSchemaMetaIndexErr error
|
||||
runtimeEmbeddedSchemaMetaIndexLazyCount atomic.Uint64
|
||||
)
|
||||
|
||||
// BuildSchemaMetaIndex extracts the ResolveMeta summary from a full Catalog
|
||||
// snapshot. Entries are sorted by cli_path for deterministic generation.
|
||||
func BuildSchemaMetaIndex(snapshot SchemaCatalogSnapshot) (SchemaMetaIndexSnapshot, error) {
|
||||
if snapshot.Version != SchemaCatalogSnapshotVersion {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("unsupported Schema Catalog snapshot version %d", snapshot.Version)
|
||||
}
|
||||
if strings.TrimSpace(snapshot.SourceHash) == "" {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("schema Catalog snapshot is missing source_hash")
|
||||
}
|
||||
entries := make([]SchemaMetaIndexEntry, 0, len(snapshot.Tools))
|
||||
seenCLI := make(map[string]string, len(snapshot.Tools))
|
||||
for canonical, tool := range snapshot.Tools {
|
||||
cliPath := strings.TrimSpace(schemaString(tool["cli_path"]))
|
||||
if cliPath == "" {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("tool %q is missing cli_path", canonical)
|
||||
}
|
||||
if prev, exists := seenCLI[cliPath]; exists {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("duplicate cli_path %q for %q and %q", cliPath, prev, canonical)
|
||||
}
|
||||
seenCLI[cliPath] = canonical
|
||||
toolCanonical := strings.TrimSpace(schemaString(tool["canonical_path"]))
|
||||
if toolCanonical == "" {
|
||||
toolCanonical = canonical
|
||||
}
|
||||
entries = append(entries, SchemaMetaIndexEntry{
|
||||
CLIPath: cliPath,
|
||||
Canonical: toolCanonical,
|
||||
Aliases: schemaStringSlice(tool["aliases"]),
|
||||
ProductID: schemaString(tool["product_id"]),
|
||||
Title: schemaString(tool["title"]),
|
||||
Effect: schemaString(tool["effect"]),
|
||||
Risk: schemaString(tool["risk"]),
|
||||
Confirmation: schemaString(tool["confirmation"]),
|
||||
Idempotency: schemaString(tool["idempotency"]),
|
||||
AgentSummary: schemaString(tool["agent_summary"]),
|
||||
UseWhen: schemaStringSlice(tool["use_when"]),
|
||||
AvoidWhen: schemaStringSlice(tool["avoid_when"]),
|
||||
Examples: schemaStringSlice(tool["examples"]),
|
||||
})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].CLIPath < entries[j].CLIPath
|
||||
})
|
||||
return SchemaMetaIndexSnapshot{
|
||||
Version: SchemaMetaIndexVersion,
|
||||
SourceHash: snapshot.SourceHash,
|
||||
SurfaceHash: snapshot.SurfaceHash,
|
||||
Entries: entries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncodeSchemaMetaIndex marshals the index with stable indentation matching
|
||||
// other generated CLI JSON artifacts.
|
||||
func EncodeSchemaMetaIndex(index SchemaMetaIndexSnapshot) ([]byte, error) {
|
||||
encoded, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode schema meta index: %w", err)
|
||||
}
|
||||
return append(encoded, '\n'), nil
|
||||
}
|
||||
|
||||
// DecodeSchemaMetaIndexJSON parses a committed/generated meta index document.
|
||||
func DecodeSchemaMetaIndexJSON(data []byte) (SchemaMetaIndexSnapshot, error) {
|
||||
var index SchemaMetaIndexSnapshot
|
||||
if err := decodeStrictSchemaJSON(data, &index); err != nil {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("decode schema meta index: %w", err)
|
||||
}
|
||||
if index.Version != SchemaMetaIndexVersion {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("unsupported schema meta index version %d", index.Version)
|
||||
}
|
||||
if strings.TrimSpace(index.SourceHash) == "" {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("schema meta index is missing source_hash")
|
||||
}
|
||||
if len(index.Entries) == 0 {
|
||||
return SchemaMetaIndexSnapshot{}, fmt.Errorf("schema meta index has no entries")
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// commandMetaLookupFromIndex expands primary paths and aliases into the
|
||||
// ResolveMeta O(1) map.
|
||||
func commandMetaLookupFromIndex(index SchemaMetaIndexSnapshot) (map[string]CommandMeta, error) {
|
||||
lookup := make(map[string]CommandMeta, len(index.Entries)*2)
|
||||
metas := make([]CommandMeta, 0, len(index.Entries))
|
||||
for _, entry := range index.Entries {
|
||||
cliPath := strings.TrimSpace(entry.CLIPath)
|
||||
if cliPath == "" {
|
||||
return nil, fmt.Errorf("schema meta index entry is missing cli_path")
|
||||
}
|
||||
if _, exists := lookup[cliPath]; exists {
|
||||
return nil, fmt.Errorf("schema meta index duplicate cli_path %q", cliPath)
|
||||
}
|
||||
meta := CommandMeta{
|
||||
Identity: CommandIdentity{
|
||||
CLIPath: cliPath,
|
||||
Canonical: strings.TrimSpace(entry.Canonical),
|
||||
Aliases: append([]string(nil), entry.Aliases...),
|
||||
ProductID: entry.ProductID,
|
||||
Title: entry.Title,
|
||||
},
|
||||
Safety: CommandSafety{
|
||||
Effect: entry.Effect,
|
||||
Risk: entry.Risk,
|
||||
Confirmation: entry.Confirmation,
|
||||
Idempotency: entry.Idempotency,
|
||||
},
|
||||
Selection: CommandSelection{
|
||||
AgentSummary: entry.AgentSummary,
|
||||
UseWhen: append([]string(nil), entry.UseWhen...),
|
||||
AvoidWhen: append([]string(nil), entry.AvoidWhen...),
|
||||
Examples: append([]string(nil), entry.Examples...),
|
||||
},
|
||||
}
|
||||
lookup[cliPath] = meta
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
return registerCommandMetaAliases(lookup, metas), nil
|
||||
}
|
||||
|
||||
func decodeSchemaMetaIndexLookup(data []byte) (map[string]CommandMeta, error) {
|
||||
index, err := DecodeSchemaMetaIndexJSON(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return commandMetaLookupFromIndex(index)
|
||||
}
|
||||
|
||||
func embeddedSchemaMetaIndexError() error {
|
||||
metaByCLIPathOnce.Do(initMetaByCLIPath)
|
||||
return runtimeEmbeddedSchemaMetaIndexErr
|
||||
}
|
||||
|
||||
// ValidateSchemaMetaIndexAgainstSnapshot proves the summary index matches the
|
||||
// Identity / Safety / Selection projection of every ToolSpec in a Catalog
|
||||
// snapshot (generation-time gate before writing the index).
|
||||
func ValidateSchemaMetaIndexAgainstSnapshot(index SchemaMetaIndexSnapshot, snapshot SchemaCatalogSnapshot) error {
|
||||
if index.SourceHash != snapshot.SourceHash {
|
||||
return fmt.Errorf("schema meta index source_hash %q disagrees with catalog %q", index.SourceHash, snapshot.SourceHash)
|
||||
}
|
||||
if index.SurfaceHash != snapshot.SurfaceHash {
|
||||
return fmt.Errorf("schema meta index surface_hash %q disagrees with catalog %q", index.SurfaceHash, snapshot.SurfaceHash)
|
||||
}
|
||||
want := buildMetaByCLIPathFromSnapshotTools(snapshot.Tools)
|
||||
got, err := commandMetaLookupFromIndex(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareCommandMetaLookups(got, want); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(index.Entries) != len(snapshot.Tools) {
|
||||
return fmt.Errorf("schema meta index entry count %d disagrees with catalog tools %d", len(index.Entries), len(snapshot.Tools))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateSchemaMetaIndexAgainstCatalog proves the summary index matches the
|
||||
// Identity / Safety / Selection projection of every delivered ToolSpec.
|
||||
func ValidateSchemaMetaIndexAgainstCatalog(index SchemaMetaIndexSnapshot, registry SchemaRegistry) error {
|
||||
want := buildMetaByCLIPathFromRegistry(registry)
|
||||
got, err := commandMetaLookupFromIndex(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareCommandMetaLookups(got, want); err != nil {
|
||||
return err
|
||||
}
|
||||
primaryCount := 0
|
||||
for _, product := range registry.Products {
|
||||
primaryCount += len(product.Tools)
|
||||
}
|
||||
if len(index.Entries) != primaryCount {
|
||||
return fmt.Errorf("schema meta index entry count %d disagrees with catalog tools %d", len(index.Entries), primaryCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareCommandMetaLookups(got, want map[string]CommandMeta) error {
|
||||
if len(got) != len(want) {
|
||||
return fmt.Errorf("schema meta index lookup size %d disagrees with catalog projection %d", len(got), len(want))
|
||||
}
|
||||
paths := make([]string, 0, len(want))
|
||||
for path := range want {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
for _, path := range paths {
|
||||
wantMeta := want[path]
|
||||
gotMeta, ok := got[path]
|
||||
if !ok {
|
||||
return fmt.Errorf("schema meta index missing path %q", path)
|
||||
}
|
||||
if err := commandMetaEqual(gotMeta, wantMeta); err != nil {
|
||||
return fmt.Errorf("schema meta index path %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
for path := range got {
|
||||
if _, ok := want[path]; !ok {
|
||||
return fmt.Errorf("schema meta index has unexpected path %q", path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandMetaEqual(got, want CommandMeta) error {
|
||||
if got.Identity.CLIPath != want.Identity.CLIPath ||
|
||||
got.Identity.Canonical != want.Identity.Canonical ||
|
||||
got.Identity.ProductID != want.Identity.ProductID ||
|
||||
got.Identity.Title != want.Identity.Title ||
|
||||
!metaStringSlicesEqual(got.Identity.Aliases, want.Identity.Aliases) {
|
||||
return fmt.Errorf("identity mismatch: got %+v want %+v", got.Identity, want.Identity)
|
||||
}
|
||||
if got.Safety != want.Safety {
|
||||
return fmt.Errorf("safety mismatch: got %+v want %+v", got.Safety, want.Safety)
|
||||
}
|
||||
if got.Selection.AgentSummary != want.Selection.AgentSummary ||
|
||||
!metaStringSlicesEqual(got.Selection.UseWhen, want.Selection.UseWhen) ||
|
||||
!metaStringSlicesEqual(got.Selection.AvoidWhen, want.Selection.AvoidWhen) ||
|
||||
!metaStringSlicesEqual(got.Selection.Examples, want.Selection.Examples) {
|
||||
return fmt.Errorf("selection mismatch: got %+v want %+v", got.Selection, want.Selection)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func metaStringSlicesEqual(got, want []string) bool {
|
||||
if len(got) == 0 && len(want) == 0 {
|
||||
return true
|
||||
}
|
||||
return reflect.DeepEqual(got, want)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,129 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const schemaLazyMetaIndexChildEnv = "DWS_SCHEMA_LAZY_META_INDEX_CHILD"
|
||||
|
||||
func TestEmbeddedSchemaMetaIndexMatchesCatalog(t *testing.T) {
|
||||
index, err := DecodeSchemaMetaIndexJSON(embeddedSchemaMetaIndexJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeSchemaMetaIndexJSON() error = %v", err)
|
||||
}
|
||||
loaded := embeddedSchemaCatalog()
|
||||
if len(loaded.Registry.Products) == 0 {
|
||||
t.Fatal("embedded catalog unavailable")
|
||||
}
|
||||
if err := ValidateSchemaMetaIndexAgainstCatalog(index, loaded.Registry); err != nil {
|
||||
t.Fatalf("ValidateSchemaMetaIndexAgainstCatalog() error = %v", err)
|
||||
}
|
||||
if index.SourceHash != loaded.Snapshot.SourceHash {
|
||||
t.Fatalf("source_hash index=%q catalog=%q", index.SourceHash, loaded.Snapshot.SourceHash)
|
||||
}
|
||||
if got := len(index.Entries); got != len(loaded.Index.CanonicalPaths()) {
|
||||
t.Fatalf("meta index entries = %d, catalog tools = %d", got, len(loaded.Index.CanonicalPaths()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMetaDoesNotLoadFullCatalog(t *testing.T) {
|
||||
if os.Getenv(schemaLazyMetaIndexChildEnv) == "1" {
|
||||
if counts := RuntimeSchemaMetadataLoadCounts(); counts != (SchemaMetadataLoadCounts{}) {
|
||||
t.Fatalf("package init loaded Schema metadata: %#v", counts)
|
||||
}
|
||||
if _, ok := ResolveMeta("dev app delete"); !ok {
|
||||
t.Fatal(`ResolveMeta("dev app delete") ok=false`)
|
||||
}
|
||||
counts := RuntimeSchemaMetadataLoadCounts()
|
||||
if counts.MetaIndex != 1 {
|
||||
t.Fatalf("MetaIndex load count = %d, want 1", counts.MetaIndex)
|
||||
}
|
||||
if counts.Catalog != 0 {
|
||||
t.Fatalf("Catalog load count = %d, want 0 after ResolveMeta", counts.Catalog)
|
||||
}
|
||||
if err := embeddedSchemaMetaIndexError(); err != nil {
|
||||
t.Fatalf("embeddedSchemaMetaIndexError() = %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[0], "-test.run=^TestResolveMetaDoesNotLoadFullCatalog$", "-test.count=1")
|
||||
command.Env = append(os.Environ(), schemaLazyMetaIndexChildEnv+"=1")
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("meta index lazy child failed: %v\n%s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedSchemaMetaIndexLoadsOnlyOnce(t *testing.T) {
|
||||
const childEnv = "DWS_SCHEMA_LAZY_META_INDEX_ONCE_CHILD"
|
||||
if os.Getenv(childEnv) == "1" {
|
||||
var wait sync.WaitGroup
|
||||
for range 32 {
|
||||
wait.Add(1)
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
_, _ = ResolveMeta("dev app delete")
|
||||
}()
|
||||
}
|
||||
wait.Wait()
|
||||
if got := RuntimeSchemaMetadataLoadCounts().MetaIndex; got != 1 {
|
||||
t.Fatalf("MetaIndex lazy load count = %d, want 1", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[0], "-test.run=^TestEmbeddedSchemaMetaIndexLoadsOnlyOnce$", "-test.count=1")
|
||||
command.Env = append(os.Environ(), childEnv+"=1")
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("meta index once child failed: %v\n%s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSchemaMetaIndexDeterministic(t *testing.T) {
|
||||
snapshot, err := assembleSchemaCatalogSnapshot(
|
||||
embeddedSchemaCatalogEnvelopeJSON, embeddedSchemaCatalogTools, "schema_catalog/tools")
|
||||
if err != nil {
|
||||
t.Skip("embedded catalog unavailable")
|
||||
}
|
||||
first, err := BuildSchemaMetaIndex(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildSchemaMetaIndex() error = %v", err)
|
||||
}
|
||||
second, err := BuildSchemaMetaIndex(snapshot)
|
||||
if err != nil {
|
||||
t.Fatalf("second BuildSchemaMetaIndex() error = %v", err)
|
||||
}
|
||||
left, err := EncodeSchemaMetaIndex(first)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
right, err := EncodeSchemaMetaIndex(second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(left) != string(right) {
|
||||
t.Fatal("BuildSchemaMetaIndex is not deterministic")
|
||||
}
|
||||
if err := ValidateSchemaMetaIndexAgainstSnapshot(first, snapshot); err != nil {
|
||||
t.Fatalf("ValidateSchemaMetaIndexAgainstSnapshot() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -40,17 +40,20 @@ func main() {
|
||||
var rootPath string
|
||||
var surfacePath string
|
||||
var outputPath string
|
||||
var metaIndexPath string
|
||||
flag.StringVar(&rootPath, "root", ".", "Repository root used to protect Schema generator inputs")
|
||||
flag.StringVar(&surfacePath, "surface", "", "Deprecated compatibility input relative to --root; when set it must equal the embedded reviewed CommandRegistry")
|
||||
flag.StringVar(&outputPath, "output", "internal/cli/schema_catalog", "Output directory for the split embedded schema catalog (catalog.json + tools/<product>.json)")
|
||||
flag.StringVar(&metaIndexPath, "meta-index", "", "Output path for CommandMeta summary index JSON (default: sibling schema_meta_index.json next to --output)")
|
||||
flag.Parse()
|
||||
resolvedSurfacePath := resolveCatalogRootPath(rootPath, surfacePath)
|
||||
if err := validateCatalogOutputIsolation(rootPath, outputPath, resolvedSurfacePath); err != nil {
|
||||
resolvedMetaIndexPath := resolveSchemaMetaIndexPath(outputPath, metaIndexPath)
|
||||
if err := validateCatalogOutputIsolation(rootPath, outputPath, resolvedMetaIndexPath, resolvedSurfacePath); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
|
||||
root := app.NewSchemaSourceRootCommand()
|
||||
if err := generateSchemaCatalog(rootPath, root, resolvedSurfacePath, outputPath); err != nil {
|
||||
if err := generateSchemaCatalog(rootPath, root, resolvedSurfacePath, outputPath, resolvedMetaIndexPath); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +66,15 @@ func resolveCatalogRootPath(rootPath, path string) string {
|
||||
return filepath.Join(rootPath, path)
|
||||
}
|
||||
|
||||
func validateCatalogOutputIsolation(rootPath, outputPath, surfacePath string) error {
|
||||
func resolveSchemaMetaIndexPath(outputPath, metaIndexPath string) string {
|
||||
metaIndexPath = strings.TrimSpace(metaIndexPath)
|
||||
if metaIndexPath != "" {
|
||||
return metaIndexPath
|
||||
}
|
||||
return filepath.Join(filepath.Dir(outputPath), "schema_meta_index.json")
|
||||
}
|
||||
|
||||
func validateCatalogOutputIsolation(rootPath, outputPath, metaIndexPath, surfacePath string) error {
|
||||
inputs := []outputguard.Input{
|
||||
{Name: "main Skill metadata source", Path: "skills/mono/SKILL.md"},
|
||||
{Name: "product Skill metadata source directory", Path: "skills/mono/references/products"},
|
||||
@@ -77,12 +88,22 @@ func validateCatalogOutputIsolation(rootPath, outputPath, surfacePath string) er
|
||||
if strings.TrimSpace(surfacePath) != "" {
|
||||
inputs = append(inputs, outputguard.Input{Name: "deprecated Registry compatibility input", Path: surfacePath})
|
||||
}
|
||||
if err := outputguard.Validate(rootPath, inputs, []outputguard.Target{{Name: "--output", Path: outputPath, Directory: true}}); err != nil {
|
||||
targets := []outputguard.Target{
|
||||
{Name: "--output", Path: outputPath, Directory: true},
|
||||
{Name: "--meta-index", Path: metaIndexPath},
|
||||
}
|
||||
if err := outputguard.Validate(rootPath, inputs, targets); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := outputguard.ValidateRepoTargetAllowlist(rootPath,
|
||||
outputguard.Target{Name: "--output", Path: outputPath, Directory: true},
|
||||
"internal/cli/schema_catalog",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return outputguard.ValidateRepoTargetAllowlist(rootPath,
|
||||
outputguard.Target{Name: "--output", Path: outputPath, Directory: true},
|
||||
"internal/cli/schema_catalog",
|
||||
outputguard.Target{Name: "--meta-index", Path: metaIndexPath},
|
||||
"internal/cli/schema_meta_index.json",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,8 +112,8 @@ func validateCatalogOutputIsolation(rootPath, outputPath, surfacePath string) er
|
||||
// --surface flag is validated against the embedded registry and can never
|
||||
// replace it as an input source. Agent metadata is generated in-memory and
|
||||
// injected for assembly; schema_agent_metadata/ is not a delivery artifact.
|
||||
func generateSchemaCatalog(rootPath string, root *cobra.Command, surfacePath, outputPath string) error {
|
||||
return generateSchemaCatalogWithResolver(rootPath, root, surfacePath, outputPath, cli.ResolveSchemaBuild)
|
||||
func generateSchemaCatalog(rootPath string, root *cobra.Command, surfacePath, outputPath, metaIndexPath string) error {
|
||||
return generateSchemaCatalogWithResolver(rootPath, root, surfacePath, outputPath, metaIndexPath, cli.ResolveSchemaBuild)
|
||||
}
|
||||
|
||||
type schemaBuildResolver func(*cobra.Command) (cli.ResolvedSchemaBuild, error)
|
||||
@@ -101,13 +122,14 @@ type schemaBuildResolver func(*cobra.Command) (cli.ResolvedSchemaBuild, error)
|
||||
// contract observable in tests. Production passes cli.ResolveSchemaBuild; the
|
||||
// returned Effective/Bound/SchemaRegistry views then travel together through
|
||||
// every gate and the final serializer.
|
||||
func generateSchemaCatalogWithResolver(rootPath string, root *cobra.Command, surfacePath, outputPath string, resolve schemaBuildResolver) error {
|
||||
func generateSchemaCatalogWithResolver(rootPath string, root *cobra.Command, surfacePath, outputPath, metaIndexPath string, resolve schemaBuildResolver) error {
|
||||
if root == nil {
|
||||
return fmt.Errorf("schema source root is nil")
|
||||
}
|
||||
if resolve == nil {
|
||||
return fmt.Errorf("schema build resolver is nil")
|
||||
}
|
||||
metaIndexPath = resolveSchemaMetaIndexPath(outputPath, metaIndexPath)
|
||||
if err := validateDeprecatedSurface(surfacePath); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -132,8 +154,32 @@ func generateSchemaCatalogWithResolver(rootPath string, root *cobra.Command, sur
|
||||
if err := writeSchemaCatalogShards(snapshot, outputPath); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stderr, "generated schema catalog: output=%s registry_commands=%d tools=%d products=%d registry_hash=%s source_hash=%s\n",
|
||||
outputPath, resolved.CommandCount(), len(snapshot.Tools), countSchemaCatalogProducts(snapshot), snapshot.SurfaceHash, snapshot.SourceHash)
|
||||
if err := writeSchemaMetaIndex(snapshot, metaIndexPath); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(os.Stderr, "generated schema catalog: output=%s meta_index=%s registry_commands=%d tools=%d products=%d registry_hash=%s source_hash=%s\n",
|
||||
outputPath, metaIndexPath, resolved.CommandCount(), len(snapshot.Tools), countSchemaCatalogProducts(snapshot), snapshot.SurfaceHash, snapshot.SourceHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeSchemaMetaIndex(snapshot cli.SchemaCatalogSnapshot, outputPath string) error {
|
||||
index, err := cli.BuildSchemaMetaIndex(snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build schema meta index: %w", err)
|
||||
}
|
||||
if err := cli.ValidateSchemaMetaIndexAgainstSnapshot(index, snapshot); err != nil {
|
||||
return fmt.Errorf("validate schema meta index against catalog: %w", err)
|
||||
}
|
||||
encoded, err := cli.EncodeSchemaMetaIndex(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := makeCatalogDirectory(filepath.Dir(outputPath), 0o755); err != nil {
|
||||
return fmt.Errorf("create schema meta index directory: %w", err)
|
||||
}
|
||||
if err := writeCatalogFile(outputPath, encoded, 0o644); err != nil {
|
||||
return fmt.Errorf("write schema meta index: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -80,21 +80,21 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogFailureEdges(t *testing.T) {
|
||||
resolver := func(*cobra.Command) (cli.ResolvedSchemaBuild, error) { return cli.ResolvedSchemaBuild{}, nil }
|
||||
output := filepath.Join(t.TempDir(), "catalog.json")
|
||||
|
||||
if err := generateSchemaCatalogWithResolver(".", nil, "", output, resolver); err == nil || !strings.Contains(err.Error(), "root is nil") {
|
||||
if err := generateSchemaCatalogWithResolver(".", nil, "", output, "", resolver); err == nil || !strings.Contains(err.Error(), "root is nil") {
|
||||
t.Fatalf("nil root error = %v", err)
|
||||
}
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, nil); err == nil || !strings.Contains(err.Error(), "resolver is nil") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", nil); err == nil || !strings.Contains(err.Error(), "resolver is nil") {
|
||||
t.Fatalf("nil resolver error = %v", err)
|
||||
}
|
||||
if err := generateSchemaCatalogWithResolver(".", root, filepath.Join(t.TempDir(), "missing.json"), output, resolver); err == nil || !strings.Contains(err.Error(), "read deprecated") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, filepath.Join(t.TempDir(), "missing.json"), output, "", resolver); err == nil || !strings.Contains(err.Error(), "read deprecated") {
|
||||
t.Fatalf("surface read error = %v", err)
|
||||
}
|
||||
validateCatalogParameterBindings = func() error { return errors.New("bindings") }
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "parameter binding") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", resolver); err == nil || !strings.Contains(err.Error(), "parameter binding") {
|
||||
t.Fatalf("binding error = %v", err)
|
||||
}
|
||||
validateCatalogParameterBindings = func() error { return nil }
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, func(*cobra.Command) (cli.ResolvedSchemaBuild, error) {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", func(*cobra.Command) (cli.ResolvedSchemaBuild, error) {
|
||||
return cli.ResolvedSchemaBuild{}, errors.New("resolve")
|
||||
}); err == nil || !strings.Contains(err.Error(), "resolve final") {
|
||||
t.Fatalf("resolver error = %v", err)
|
||||
@@ -102,19 +102,19 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogFailureEdges(t *testing.T) {
|
||||
buildCatalogSnapshot = func(cli.ResolvedSchemaBuild, cli.SchemaCatalogBuildOptions) (cli.SchemaCatalogSnapshot, error) {
|
||||
return cli.SchemaCatalogSnapshot{}, errors.New("snapshot")
|
||||
}
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "snapshot") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", resolver); err == nil || !strings.Contains(err.Error(), "snapshot") {
|
||||
t.Fatalf("snapshot error = %v", err)
|
||||
}
|
||||
buildCatalogSnapshot = func(cli.ResolvedSchemaBuild, cli.SchemaCatalogBuildOptions) (cli.SchemaCatalogSnapshot, error) {
|
||||
return cli.SchemaCatalogSnapshot{}, nil
|
||||
}
|
||||
makeCatalogDirectory = func(string, os.FileMode) error { return errors.New("mkdir") }
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "create schema catalog") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", resolver); err == nil || !strings.Contains(err.Error(), "create schema catalog") {
|
||||
t.Fatalf("mkdir error = %v", err)
|
||||
}
|
||||
makeCatalogDirectory = func(string, os.FileMode) error { return nil }
|
||||
writeCatalogFile = func(string, []byte, os.FileMode) error { return errors.New("write") }
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, resolver); err == nil || !strings.Contains(err.Error(), "write schema catalog") {
|
||||
if err := generateSchemaCatalogWithResolver(".", root, "", output, "", resolver); err == nil || !strings.Contains(err.Error(), "write schema catalog") {
|
||||
t.Fatalf("write error = %v", err)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogFailureEdges(t *testing.T) {
|
||||
if err := os.WriteFile(surface, []byte("{}"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(repositoryRoot, filepath.Join(t.TempDir(), "catalog.json"), surface); err != nil {
|
||||
if err := validateCatalogOutputIsolation(repositoryRoot, filepath.Join(t.TempDir(), "catalog.json"), filepath.Join(t.TempDir(), "schema_meta_index.json"), surface); err != nil {
|
||||
t.Fatalf("surface isolation input rejected safe output: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ func TestCrossPlatformCoverageGenerateSchemaCatalogResolvesBuildExactlyOnce(t *t
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "schema_catalog")
|
||||
t.Cleanup(cli.ClearBuildTimeAgentMetadata)
|
||||
if err := generateSchemaCatalogWithResolver(repositoryRoot, root, "", outputPath, resolver); err != nil {
|
||||
if err := generateSchemaCatalogWithResolver(repositoryRoot, root, "", outputPath, "", resolver); err != nil {
|
||||
t.Fatalf("generateSchemaCatalogWithResolver() error = %v", err)
|
||||
}
|
||||
if resolveCalls != 1 {
|
||||
@@ -305,19 +305,19 @@ func TestValidateCatalogOutputIsolationProtectsEveryInputLayer(t *testing.T) {
|
||||
{name: "mcp metadata", output: metadataDir, want: "pinned MCP metadata"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := validateCatalogOutputIsolation(root, test.output, "")
|
||||
err := validateCatalogOutputIsolation(root, test.output, filepath.Join(t.TempDir(), "schema_meta_index.json"), "")
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("validateCatalogOutputIsolation() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "internal/cli/schema_catalog"), ""); err != nil {
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "internal/cli/schema_catalog"), filepath.Join(root, "internal/cli/schema_meta_index.json"), ""); err != nil {
|
||||
t.Fatalf("safe output rejected: %v", err)
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(t.TempDir(), "schema_catalog"), ""); err != nil {
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(t.TempDir(), "schema_catalog"), filepath.Join(t.TempDir(), "schema_meta_index.json"), ""); err != nil {
|
||||
t.Fatalf("external temporary output rejected: %v", err)
|
||||
}
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "skills/mono/overwrite.json"), ""); err == nil || !strings.Contains(err.Error(), "not a canonical generated delivery target") {
|
||||
if err := validateCatalogOutputIsolation(root, filepath.Join(root, "skills/mono/overwrite.json"), filepath.Join(t.TempDir(), "schema_meta_index.json"), ""); err == nil || !strings.Contains(err.Error(), "not a canonical generated delivery target") {
|
||||
t.Fatalf("non-canonical repository output error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
// BenchmarkShortcutCommands measures the Go-literal storage model: the 376
|
||||
// shortcut definitions are compiled-in struct literals registered from init(),
|
||||
// so there is no parse step at all — this times only turning them into the Cobra
|
||||
// tree. It is the comparison point for the embedded JSON catalog, whose first
|
||||
// ResolveMeta hit spends ~1.4s and ~740MB decoding.
|
||||
// tree. ResolveMeta no longer pays full Catalog decode (~294–360ms / ~175MB);
|
||||
// it reads schema_meta_index.json (BenchmarkResolveMetaFirstHit). Full Catalog
|
||||
// assemble remains the dws schema / --all path.
|
||||
func BenchmarkShortcutCommands(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -30,6 +30,8 @@ cp internal/cli/param_concepts.json "$concepts_guard"
|
||||
cp internal/cli/param_concepts.schema.json "$concepts_schema_guard"
|
||||
catalog_tmp="$tmp/schema_catalog"
|
||||
catalog_tmp_second="$tmp/schema_catalog-second"
|
||||
meta_index_tmp="$tmp/schema_meta_index.json"
|
||||
meta_index_tmp_second="$tmp/schema_meta_index-second.json"
|
||||
param_aliases_tmp="$tmp/param_aliases_generated.go"
|
||||
param_aliases_tmp_second="$tmp/param_aliases_generated-second.go"
|
||||
|
||||
@@ -43,11 +45,13 @@ fi
|
||||
|
||||
"$catalog_generator" \
|
||||
-root . \
|
||||
-output "$catalog_tmp"
|
||||
-output "$catalog_tmp" \
|
||||
-meta-index "$meta_index_tmp"
|
||||
|
||||
"$catalog_generator" \
|
||||
-root . \
|
||||
-output "$catalog_tmp_second"
|
||||
-output "$catalog_tmp_second" \
|
||||
-meta-index "$meta_index_tmp_second"
|
||||
|
||||
"$param_aliases_generator" \
|
||||
-root . \
|
||||
@@ -84,6 +88,12 @@ if ! diff -qr "$catalog_tmp" "$catalog_tmp_second" >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! cmp -s "$meta_index_tmp" "$meta_index_tmp_second"; then
|
||||
printf '%s\n' 'generated drift: consecutive CommandMeta index generations are not byte-identical' >&2
|
||||
diff -u "$meta_index_tmp" "$meta_index_tmp_second" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! diff -qr internal/cli/schema_catalog "$catalog_tmp" >/dev/null; then
|
||||
printf '%s\n' 'generated drift: internal/cli/schema_catalog is stale' >&2
|
||||
printf '%s\n' 'run: make generate-schema' >&2
|
||||
@@ -91,6 +101,13 @@ if ! diff -qr internal/cli/schema_catalog "$catalog_tmp" >/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! cmp -s internal/cli/schema_meta_index.json "$meta_index_tmp"; then
|
||||
printf '%s\n' 'generated drift: internal/cli/schema_meta_index.json is stale' >&2
|
||||
printf '%s\n' 'run: make generate-schema' >&2
|
||||
diff -u internal/cli/schema_meta_index.json "$meta_index_tmp" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! cmp -s "$param_aliases_tmp" "$param_aliases_tmp_second"; then
|
||||
printf '%s\n' 'generated drift: consecutive parameter-alias generations are not byte-identical' >&2
|
||||
diff -u "$param_aliases_tmp" "$param_aliases_tmp_second" || true
|
||||
|
||||
Reference in new issue
Block a user