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:
玉澜andCursor committed 2026-08-01 21:01:45 +08:00
1 parent 3e83ac18d1
commit bb54761e50
16 files changed
+18036 -73

No files matched your search

+14 -8
View File
@@ -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,
+6 -4
View File
@@ -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:
+2 -1
View File
@@ -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 -5
View File
@@ -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)]
+4 -4
View File
@@ -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
View File
@@ -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
+5 -4
View File
@@ -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)
+36 -16
View File
@@ -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()
+2
View File
@@ -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(),
+294
View File
@@ -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
+129
View File
@@ -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)
}
}
+57 -11
View File
@@ -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++ {
+19 -2
View File
@@ -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