Skip to content

feat: add IntrospectMethod for DESCRIBE METHOD primitive#89

Merged
jeffreyaven merged 2 commits into
mainfrom
feature/introspection
May 14, 2026
Merged

feat: add IntrospectMethod for DESCRIBE METHOD primitive#89
jeffreyaven merged 2 commits into
mainfrom
feature/introspection

Conversation

@jeffreyaven
Copy link
Copy Markdown
Member

Summary

Adds IntrospectMethod, a new free-function entry point in any-sdk that returns input and output field metadata for a single method on a resource. This is the any-sdk half of the upcoming DESCRIBE METHOD <provider>.<service>.<resource>.<method> SQL primitive in stackql core — designed to give agents enough schema context in one query to construct payloads and interpret responses without follow-up round trips.

The resolver lives in its own file with its own types and does not mutate any existing interface. Existing DESCRIBE paths, SHOW METHODS, SHOW INSERT INTO, and every other code path are byte-identical to main.

What it produces

One row per input parameter (required and optional, including body fields) and one row per top-level response field. Each row carries:

  • name — user-facing name (renamed where stackql has a body translation algorithm configured, e.g. data__name)
  • type — openapi type or object/array
  • param_typeinput_required | input_optional | output
  • shape — JSON Schema subset (text) for object/array fields; empty for scalars
  • description — populated only when the extended flag is set

The shape is intentionally complete (type, format, properties, items, required, enum, default, description, readOnly/writeOnly/deprecated, and surviving oneOf/anyOf/allOf) so an agent can construct a valid payload from one DESCRIBE METHOD call. Whether a parameter goes in the path, query, header, or body is deliberately hidden — stackql treats methods as a uniform input surface.

Key design decisions

  • Free function, not interface method. Zero existing-interface mutations to honour the "own code path" rule. The public surface is a hand-written addition to formulation.go; the generated wrappers.go is untouched.
  • Method-level request.required annotations honoured. Body fields listed in the method's request annotation are promoted to input_required even when the underlying schema's required array does not list them (matches SHOW METHODS behaviour).
  • No depth knob. Depth is purely a cycle/runaway guard, not a UX dial. The walker recurses to natural leaves; a 64-level hard ceiling catches pathological deeply-nested-but-non-cyclic schemas, and an identity+$ref visited-map catches actual cycles. Cycles emit a sentinel node with x-stackql-cycle-ref.
  • Empty-response methods produce zero output rows. A DELETE returning 204 emits inputs only; no synthetic placeholder.
  • shape is text containing JSON. Cross-backend portable; the SQL renderer will not depend on Postgres jsonb.

Files

New:

  • internal/anysdk/introspection.go — resolver, JSON Schema subset renderer, cycle-guarded walker, types
  • internal/anysdk/introspection_test.go — 14 tests against the in-repo google storage fixture

Modified:

  • public/formulation/formulation.goIntrospectMethod free function plus type aliases (ParamType, IntrospectedField, MethodIntrospection)

No other files touched. wrappers.go, interfaces.go, schema.go, operation_store.go, and every other existing file are unchanged.

Tests

Fourteen new tests, all passing locally:

  • Happy path: input + output rows on buckets.get
  • Nested object shape rendering on encryption
  • Three-level nesting on iamConfiguration.bucketPolicyOnly.enabled
  • Array-of-objects items shape on cors
  • Body field merge with request.required annotation on buckets.insert (name promoted to required)
  • Extended flag controls per-row description; in-shape description always present
  • additionalProperties-only object on labels
  • Deterministic field ordering across two invocations
  • Unknown method returns error
  • Nil resource returns error
  • Empty-response method (delete) emits zero output rows
  • Required and optional input sets are disjoint
  • All shape JSON blobs are valid JSON across all five methods on buckets

Backward compatibility

go test ./... runs clean on the entire repo with zero regressions. The full suite passes including internal/anysdk, public/discovery, public/persistence, and public/radix_tree_address_space.

Test plan

  • CI: go build ./... clean
  • CI: go test ./... passes
  • CI: go vet ./... introduces no new warnings (pre-existing vet warnings in pkg/marshalmap, pkg/xmlmap, internal/anysdk/registry.go are unchanged)
  • Local: introspection tests pass against the google storage fixture
  • Manual: IntrospectMethod returns the expected buckets.insert shape with name as input_required

Follow-ups (not in this PR)

  1. stackql-parser PR adds DESCRIBE METHOD grammar and DescribeMethod AST node.
  2. stackql PR pulls both new releases, wires the executor and an MCP tool that calls formulation.IntrospectMethod.

@jeffreyaven
Copy link
Copy Markdown
Member Author

refactor: expose introspection results via interfaces, not structs

Address PR feedback: do not export concrete structures from internal/anysdk.

Convert IntrospectedField and MethodIntrospection from exported structs
to interfaces with package-private implementations. The public surface
in formulation now wraps the inner types behind its own interfaces,
matching the existing wrappedX pattern used elsewhere in the package.

ParamType remains an exported string type — it is a value tag with no
internal structure, so re-exporting it leaks no implementation detail.

Tests updated to use accessor methods. Full suite passes, zero
regressions.

@jeffreyaven jeffreyaven merged commit dbb5c78 into main May 14, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants