Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dcb8dd1
feat(schema): add envelope types and ordered properties container
sang-neo03 May 22, 2026
7ab36c4
feat(schema): build meta_data.json key-order index for property ordering
sang-neo03 May 22, 2026
a7b02a6
feat(schema): implement convertProperty with file/enum/range/nested h…
sang-neo03 May 22, 2026
3020dc8
feat(schema): build inputSchema with x-in / file binary / yes injection
sang-neo03 May 22, 2026
f3f4442
feat(schema): build outputSchema wrapping responseBody
sang-neo03 May 22, 2026
d068624
feat(schema): build _meta with scopes/risk/access_tokens normalization
sang-neo03 May 22, 2026
e650faa
feat(schema): scaffold affordance overlay loader (PR-1 stub)
sang-neo03 May 22, 2026
b7bc039
feat(schema): wire up AssembleEnvelope main entry point
sang-neo03 May 22, 2026
8b27de5
feat(schema): parse dotted and space-separated path arguments
sang-neo03 May 22, 2026
6c74138
feat(schema): batch envelope assembly with optional method filter
sang-neo03 May 22, 2026
fcc1c03
feat(schema): implement L1-L3 envelope lint (structure/type/cross-field)
sang-neo03 May 22, 2026
e9f193e
feat(schema): measure L4 coverage and gate all envelopes through L1-L3
sang-neo03 May 22, 2026
fc72fde
feat(schema): add golden test harness with UPDATE_GOLDEN refresh
sang-neo03 May 22, 2026
38b4683
test(schema): seed 20 golden envelopes covering edge cases
sang-neo03 May 22, 2026
99a84db
feat(schema): output MCP envelope as default JSON, preserve pretty mode
sang-neo03 May 22, 2026
ea3b2d3
test(schema): cover envelope JSON output, space-form path, yes injection
sang-neo03 May 22, 2026
440ad39
feat(schema): assemble envelope from embedded data only for stability
sang-neo03 May 22, 2026
955bfdf
chore(schema): lint cleanup
sang-neo03 May 22, 2026
762dc64
fix(schema): preserve dotted resource segments in envelope name
sang-neo03 May 22, 2026
e7076bf
fix(schema): align MCP envelope output with JSON Schema 2020-12 contract
sang-neo03 May 23, 2026
722e8ac
fix(schema): address CodeRabbit findings and stabilize CI tests
sang-neo03 May 23, 2026
23086ae
chore(schema): refresh golden envelopes after meta_data drift
sang-neo03 May 23, 2026
54389d3
fix(schema): address CodeRabbit round-3 review findings
sang-neo03 May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
356 changes: 273 additions & 83 deletions cmd/schema/schema.go

Large diffs are not rendered by default.

159 changes: 154 additions & 5 deletions cmd/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package schema

import (
"bytes"
"encoding/json"
"strings"
"testing"

Expand Down Expand Up @@ -33,17 +34,165 @@ func TestSchemaCmd_FlagParsing(t *testing.T) {
}
}

func TestSchemaCmd_NoArgs(t *testing.T) {
func TestSchemaCmd_NoArgs_Pretty(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{})
err := cmd.Execute()
if err != nil {
cmd.SetArgs([]string{"--format", "pretty"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(stdout.String(), "Available services") {
t.Error("expected service list output")
t.Error("expected service list in pretty mode")
}
}

func TestSchemaCmd_NoArgs_JSON_IsArray(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{}) // default --format json
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := strings.TrimSpace(stdout.String())
if !strings.HasPrefix(out, "[") {
head := out
if len(head) > 80 {
head = head[:80]
}
t.Errorf("expected JSON array root, first 80 chars:\n%s", head)
}
var envs []map[string]interface{}
if err := json.Unmarshal([]byte(out), &envs); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if len(envs) < 193 {
t.Errorf("envelopes count = %d, want >= 193", len(envs))
}
}

func TestSchemaCmd_JSONIsEnvelope(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{"im.images.create", "--format", "json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var env map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("not valid JSON: %v\n%s", err, stdout.String())
}
if env["name"] != "im images create" {
t.Errorf("name = %v, want \"im images create\"", env["name"])
}
for _, key := range []string{"description", "inputSchema", "outputSchema", "_meta"} {
if _, ok := env[key]; !ok {
t.Errorf("missing top-level key: %s", key)
}
}
meta, _ := env["_meta"].(map[string]interface{})
if meta["envelope_version"] != "1.0" {
t.Errorf("envelope_version = %v, want \"1.0\"", meta["envelope_version"])
}
}

func TestSchemaCmd_SpaceSeparatedPath_EqualsDotted(t *testing.T) {
f1, out1, _, _ := cmdutil.TestFactory(t, nil)
cmd1 := NewCmdSchema(f1, nil)
cmd1.SetArgs([]string{"im", "images", "create"})
if err := cmd1.Execute(); err != nil {
t.Fatalf("space form failed: %v", err)
}

f2, out2, _, _ := cmdutil.TestFactory(t, nil)
cmd2 := NewCmdSchema(f2, nil)
cmd2.SetArgs([]string{"im.images.create"})
if err := cmd2.Execute(); err != nil {
t.Fatalf("dotted form failed: %v", err)
}

if out1.String() != out2.String() {
t.Errorf("space and dotted forms produced different output")
}
}

func TestSchemaCmd_ServiceListIsArray(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{"im"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var envs []map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &envs); err != nil {
t.Fatalf("unmarshal failed: %v\n%s", err, stdout.String())
}
if len(envs) == 0 {
t.Fatal("expected non-empty array for service im")
}
for _, e := range envs {
name, _ := e["name"].(string)
if !strings.HasPrefix(name, "im ") {
t.Errorf("envelope name %q does not start with \"im \"", name)
}
}
}

func TestSchemaCmd_HighRiskYesInjection(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{"im.messages.delete"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var env map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
is, _ := env["inputSchema"].(map[string]interface{})
props, _ := is["properties"].(map[string]interface{})
if _, ok := props["yes"]; !ok {
t.Errorf("inputSchema.properties.yes missing for high-risk-write command")
}
}

func TestSchemaCmd_NoYesForReadRisk(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{"im.reactions.list"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var env map[string]interface{}
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
is, _ := env["inputSchema"].(map[string]interface{})
props, _ := is["properties"].(map[string]interface{})
if _, ok := props["yes"]; ok {
t.Errorf("yes property should not appear for risk=read command")
}
}

func TestSchemaCmd_PrettyUnchanged_KeyTextPresent(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, nil)

cmd := NewCmdSchema(f, nil)
cmd.SetArgs([]string{"im.images.create", "--format", "pretty"})
if err := cmd.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := stdout.String()
// Existing pretty rendering surfaces these markers — they must still appear
for _, want := range []string{"Parameters:", "Response:", "Identity:", "Scopes:", "CLI:"} {
if !strings.Contains(out, want) {
t.Errorf("pretty output missing marker %q", want)
}
}
}

Expand Down
58 changes: 58 additions & 0 deletions internal/registry/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,64 @@
// embeddedMetaJSON is set by loader_embedded.go when meta_data.json is compiled in.
var embeddedMetaJSON []byte

// EmbeddedMetaJSON returns the raw embedded meta_data.json bytes for callers
// that need to parse key order or other JSON-level structure not exposed by
// LoadFromMeta (which loses map insertion order).
func EmbeddedMetaJSON() []byte {
return embeddedMetaJSON

Check warning on line 29 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L28-L29

Added lines #L28 - L29 were not covered by tests
}
Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return defensive copies from embedded getters.

These functions currently expose mutable package-level slices. A caller can mutate shared state and affect subsequent assembly determinism.

🔧 Proposed fix
 func EmbeddedMetaJSON() []byte {
-	return embeddedMetaJSON
+	if len(embeddedMetaJSON) == 0 {
+		return nil
+	}
+	out := make([]byte, len(embeddedMetaJSON))
+	copy(out, embeddedMetaJSON)
+	return out
 }
@@
 func EmbeddedServiceNames() []string {
 	parseEmbeddedServices()
-	return embeddedServiceNames
+	if len(embeddedServiceNames) == 0 {
+		return nil
+	}
+	out := make([]string, len(embeddedServiceNames))
+	copy(out, embeddedServiceNames)
+	return out
 }

Also applies to: 75-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/registry/loader.go` around lines 28 - 30, The EmbeddedMetaJSON
function (and the other embedded getter functions referenced around lines 75-77)
currently returns the package-level byte slice directly, allowing callers to
mutate shared state; change each getter (e.g., EmbeddedMetaJSON) to return a
defensive copy of the underlying slice by allocating a new []byte and copying
the contents (for example via append([]byte(nil), embeddedMetaJSON...) or make +
copy) so callers cannot modify the original package-level slice.


var (
embeddedServicesMap map[string]map[string]interface{} // service name -> spec
embeddedServiceNames []string // sorted
embeddedParseOnce sync.Once
)

// parseEmbeddedServices parses embeddedMetaJSON into a service name → spec map
// without touching mergedServices. Safe to call multiple times (sync.Once).
func parseEmbeddedServices() {
embeddedParseOnce.Do(func() {
embeddedServicesMap = make(map[string]map[string]interface{})
if len(embeddedMetaJSON) == 0 {
return

Check warning on line 44 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L40-L44

Added lines #L40 - L44 were not covered by tests
}
var wrapper struct {
Services []map[string]interface{} `json:"services"`

Check warning on line 47 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L46-L47

Added lines #L46 - L47 were not covered by tests
}
if err := json.Unmarshal(embeddedMetaJSON, &wrapper); err != nil {
return

Check warning on line 50 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L49-L50

Added lines #L49 - L50 were not covered by tests
}
for _, svc := range wrapper.Services {
name, _ := svc["name"].(string)
if name == "" {
continue

Check warning on line 55 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L52-L55

Added lines #L52 - L55 were not covered by tests
}
embeddedServicesMap[name] = svc

Check warning on line 57 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L57

Added line #L57 was not covered by tests
}
embeddedServiceNames = make([]string, 0, len(embeddedServicesMap))
for name := range embeddedServicesMap {
embeddedServiceNames = append(embeddedServiceNames, name)

Check warning on line 61 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L59-L61

Added lines #L59 - L61 were not covered by tests
}
sort.Strings(embeddedServiceNames)

Check warning on line 63 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L63

Added line #L63 was not covered by tests
})
}

// EmbeddedSpec returns the embedded spec for one service, or nil if unknown.
// Bypasses remote overlay — used for deterministic envelope output.
func EmbeddedSpec(serviceName string) map[string]interface{} {
parseEmbeddedServices()
return embeddedServicesMap[serviceName]

Check warning on line 71 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L69-L71

Added lines #L69 - L71 were not covered by tests
}

// EmbeddedServiceNames returns sorted embedded service names (no overlay).
// Returns a defensive copy — callers must not mutate the package-level slice.
func EmbeddedServiceNames() []string {
parseEmbeddedServices()
out := make([]string, len(embeddedServiceNames))
copy(out, embeddedServiceNames)
return out

Check warning on line 80 in internal/registry/loader.go

View check run for this annotation

Codecov / codecov/patch

internal/registry/loader.go#L76-L80

Added lines #L76 - L80 were not covered by tests
}

var (
mergedServices = make(map[string]map[string]interface{}) // project name → parsed spec
mergedProjectList []string // sorted project names
Expand Down
1 change: 1 addition & 0 deletions internal/schema/annotations/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Reserved for future _meta.affordance overlays. Empty in PR-1.
Loading
Loading