Skip to content

feat: Kiota SDK generation, the kiotaFluent dialect, and embed/external SDK modes - #105

Merged
ShocOne merged 8 commits into
mainfrom
feat/kiota-sdk
Aug 4, 2026
Merged

feat: Kiota SDK generation, the kiotaFluent dialect, and embed/external SDK modes#105
ShocOne merged 8 commits into
mainfrom
feat/kiota-sdk

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 4, 2026

Copy link
Copy Markdown
Member

Why

The generator could only bind the hand-written resty-style go-sdk-thousandeyes. This PR makes the SDK itself derivable from the pinned OpenAPI snapshot with Microsoft Kiota, implements the long-reserved kiotaFluent dialect end to end, and adds the embed-vs-external choice for where a provider's SDK lives. A provider no longer requires an SDK to exist before onboarding an API.

The new surface

tfpfgen sdk generate — a new noun group. Reads a pinned, checksum-verified snapshot and drives kiota as a gated PATH tool: the committed kiota-lock.json names the version that produced the tree and the command refuses any other, naming both. -mode embed (default) generates under the provider's module — kiota's intended layout, module path derived from the enclosing go.mod; -mode external emits a standalone tree with its own generated go.mod. -check regenerates into scratch and byte-compares (normalising only the lock's output-relative descriptionLocation). Output passes through the same gofumpt form the provider generator uses.

The kiotaFluent dialect, reserved → real, one phase per commit:

  • IR: Operation.chain records a fluent call as data (segments of method + args; the ID rides the typed indexer); ConvertCall.extraArgs carries enum parse companions; NestedAttributeObject.constructorExpr builds setter-SDK elements; provider.sdk gains mode/generator, and modulePath is finally load-bearing (embed ⇒ equals the provider module; every service import must live under it). All additive — existing blueprints byte-identical.
  • Emitter: one access seam (readExpr/writeStmt/expandStmt) behind every SDK field read/write; chainCall renders fluent chains for all five block kinds and the acceptance helper; nil-result guards where kiota's (nil, nil) is meaningful (read ⇒ not-found, create ⇒ refuse to record, list ⇒ empty). Resty goldens proven byte-identical; the refusals lifted together only once the capability was whole.
  • Type checker: MethodChain walks builder chains on real method sets; LookupAccessorPair verifies Get/Set pairs and surfaces mangling misses (GetError vs GetErrorEscaped) with the true spelling; write targets demand setters, read interfaces legitimately carry getters alone. Static facts are dialect-gated with a stated reason. A miniature kiota-shaped SDK in testdata exercises every path.
  • Inference: blueprint draft -sdk-dialect kiotaFluent -sdk-models-package … derives chains from path templates, kiota's exact naming (plain capitalisation + keyword Escaped mangling), literal integer widths (*int32 unless format: int64), and OpenAPI-driven enums wired through parse companions.
  • go.mod assertion: the postcheck battery now proves the hand-owned go.mod declares the blueprint's SDK (external: required at source.sdkVersion; embedded kiota: the runtime module), failure naming the exact go get.

pilot/thousandeyes-kiota — the proof. The tag resource end to end on an embedded, tags-scoped SDK: bindings check verifies all 34 fluent bindings against the real generated code (including both live TypeEscaped cases, *uuid.UUID, *time.Time and four int-backed enums), the module builds and tests green, provider generate -check is stable, and probe verify re-derives identical facts from the shared recordings/thousandeyes — switching dialect is a binding change, not an evidence change. Standing it up caught three real emitter gaps (pointer-to-interface state mappers, a models local shadowing the models alias plus nil-interface element construction, and an unused shared diagnostics var), all fixed with the resty pilot regenerating stably.

CI: a pinned-kiota SDK-drift job (version + recorded sha256 in the workflow env), and the second pilot joins the regenerate and bindings jobs. Docs updated throughout (cli.md drift-tested, architecture, glossary, blueprint.md, onboarding, README, CHANGELOG).

Verification

  • Full test suites green in all three modules at every phase commit; resty pilot byte-stable through the emitter refactors (provider generate -check green before and after).
  • sdk generate -check, provider generate -check (both pilots) and probe verify (shared recordings, kiota blueprint) all pass locally.
  • kiota acquisition verified both ways: the real 1.34.1 binary end to end, and a deterministic stub on PATH in unit tests (version gate, drift catch, embed derivation, external go.mod, dry-run inertness).

Not yet: single-nested attribute wire conversion under kiotaFluent (refused by validation with a message), a live acceptance run against the sandbox, and resource-by-resource expansion beyond tag — deliberately incremental per the plan.

🤖 Generated with Claude Code

ShocOne and others added 8 commits August 4, 2026 14:48
…embedded or external

A new sdk noun group drives Microsoft Kiota as a gated PATH tool: sdk
generate reads a pinned, checksum-verified OpenAPI snapshot and generates a
Go SDK from it, so a provider can bind to an SDK derived from the same
document its blueprints were drafted from. kiota's own kiota-lock.json is
the committed source of truth; the version gate refuses a binary that does
not match it, naming both versions, because a silent kiota bump rewrites
the whole tree as an inexplicable diff. Output passes through the same
gofumpt form the provider generator uses.

-mode embed (the default) generates under the enclosing module -- kiota's
intended layout, with the import path derived from the nearest go.mod plus
the relative output dir -- so bindings check, drift checks and provider
push all see one module. -mode external emits a standalone tree with a
minimal generated go.mod resolved by go mod tidy, ready for its own
repository. -check regenerates into scratch and byte-compares, catching
hand edits, snapshot drift and version drift alike; -dry-run prints the
resolved snapshot and invocation.

Unit tests drive the full surface against a deterministic stub kiota on
PATH (embed module derivation, the version gate naming both versions,
check catching a hand edit and passing after regeneration, external go.mod
emission, dry-run inertness). docs/cli.md documents the group and the
drift test holds it there; the retired-spelling ban narrows from '-mode '
to 'probe -mode' now that a legitimate -mode flag exists again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… assertion

The IR can now express a request-builder chain as data: Operation.Chain is
a list of ChainSegment{method, args}, the final segment the verb, a
mid-chain identifier an ordinary stateField argument -- the same call-is-
data doctrine the flat method style follows, so the emitter will render a
chain generically without knowing which generator produced the SDK.
ConvertCall gains ExtraArgs for helpers needing a companion function (a
generated SDK's enum parse function), and NestedAttributeObject gains
ConstructorExpr for setter-based element construction. All additive; every
existing blueprint is byte-identical and the kiotaFluent/CallStyleFluent/
AccessMethod refusals stay in force -- but the chain's structural rules
(chain required under fluent, no chain under method, verb from the last
segment, no transport arity) already validate, so a malformed chain is
named precisely the moment the refusals lift.

provider.sdk gains mode (embed|external; absent means external, which is
what every existing blueprint always meant) and generator. Validation
makes modulePath finally load-bearing: embed requires it to equal
provider.goModule, an external declaration of the provider's own module is
named as really-embed, and every binding's service import must live under
the declared module. The postcheck battery gains an offline first line --
AssertGoMod holds the hand-owned go.mod to the blueprint's declared SDK:
an external SDK must be required at source.sdkVersion and an embedded
kiota SDK needs its runtime module, with the failure naming the exact
go get. Verified against the live pilot: the committed go.mod passes at
v0.2.0 and the whole-tree drift check stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…view builder

readExpr, writeStmt and expandStmt in access.go are now the only place the
emitter knows how an SDK model's fields are reached: struct assignment for
the resty dialect, Get/Set method pairs for a setter-based one. The eight
sites that previously spelled body.X = / remote.X inline -- construct
expand, state flatten, nested expand and flatten, ephemeral state, list
identity and display -- all route through the seam, with the access style
threaded from each binding's declared AccessStyle via the schema scope into
every nested shape. The zero style means struct fields, so every existing
blueprint and fixture renders byte-identically: proven by the untouched
golden tests and by provider generate -check staying green against the
committed pilot. A setter cannot receive a two-value call, so under method
access every fallible conversion lands in a temp -- the shape table tests
pin all five statement forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… facts

The type checker gains the method-shaped half it needs for a fluent SDK.
MethodChain walks a request-builder chain from the client type -- every hop
resolved on the real method set, argument counts held to the signatures, a
builder hop demanded to return exactly one value -- and hands the final verb
to the same arity and result-type contract a flat call gets, extracted as
verifyMethodContract. LookupAccessorPair verifies a model's Get/Set pair
and is where a mangling miss (GetError vs GetErrorEscaped) surfaces with
the real spelling; LookupFieldAccess dispatches on the binding's declared
AccessStyle, the checker-side twin of the emitter's access seam. Wire-field
checks demand setters on write targets and getters alone on read models --
builders return read-side interfaces whose method sets legitimately carry
no setters. A bare '<receiver>.client' accessor is now the legal fluent
root, and refused with a message when the binding declares a service type
it never reaches.

Static facts are dialect-gated with a stated reason: a method-access SDK is
pointer-typed behind accessors with no encoder-level omitempty, so the
zero-value-unsendable class cannot arise -- an empty document, not an
error, keeping the -check drift gate uniform across dialects. A miniature
kiota-shaped SDK module under testdata (fluent client, typed indexer,
interface-returning verbs, keyword-mangled accessor) exercises every new
path, including the two-result builder refusal and the did-you-mean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…taFluent refusals

The reserved dialect is now real end to end in the emitter. opView renders
a fluent operation through chainCall -- the chain is the call, as data, with
each segment's arguments rendered by the enclosing scope's own rules -- and
the acceptance test helper rebases the same chain onto its bare client via
the argument renderer it already had. Fluent results carry a NilResultGuard
the templates act on where nil is meaningful: a read treats an empty
success body exactly like a not-found, a create refuses to record an object
it cannot identify (errors.ErrEmptyResponse, defined by the kiota errors
scaffold), a data source and an ephemeral report it as the missing object
it is, and a list treats it as an empty collection. Struct-field SDKs never
set the flag, so resty output is untouched -- provider generate -check
stays green against the committed pilot. convertExpr renders
ConvertCall.ExtraArgs, completing the enum-parse companion plumbing.

All the refusals lift together, as one coherent capability: the dialect,
CallStyleFluent, and the three AccessMethod refusals become plain enum
membership checks; a service typeName is required only where a method-style
call actually uses one; and a new coherence pass refuses a fluent operation
under a non-fluent dialect by name -- the refusal tests now assert that rule
and the chainless-fluent emitter guard instead of the old reservations. A
fixture that rebinds the committed tag resource onto a kiota-shaped SDK
proves the whole surface: chain calls, setter construction, getter state
mapping, getter ID assignment, guard placement, and the rebased helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k-dialect

Inference can now draft the fluent dialect. blueprint draft grows
-sdk-dialect restyService|kiotaFluent and -sdk-models-package; under kiota
the resty -sdk-service-root/-sdk-accessor knobs are refused by name if
typed. The drafted bindings are chains derived from each operation's path
template -- /tags/{tagId} becomes Tags().ByTagId(id).Get(ctx, nil), one
builder hop per literal segment, the identifier riding the typed indexer,
nil request configuration until somebody curates one -- with method access,
model constructors, interface response types (<Type>able) and
created.GetId() for the identifier.

Naming is derived from the OpenAPI document, deliberately not by parsing
the generated SDK: kiotaName reproduces kiota's plain word-capitalisation
(accountGroupId -> AccountGroupId, never AccountGroupID) and the keyword
mangling (error -> GetErrorEscaped), preserving the draft-then-curate
order; a wrong guess surfaces in bindings check with the true spelling.
Field gains the schema's format string, and conversionsFor reads it the
way kiota does -- a plain integer is *int32, only format int64 widens,
float narrows -- while the resty rules are untouched. A kiota enum wires
through convert.KiotaEnumToFramework and FrameworkToKiotaEnum with the
ParseX companion in ExtraArgs, so a value the validator missed becomes a
diagnostic rather than a silent nil. Proven end to end by drafting the
committed ThousandEyes snapshot's tag resource into a validating fluent
blueprint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…embedded kiota SDK

The second pilot proves the whole kiota chain against reality. sdk generate
derives an embedded SDK from the same pinned snapshot the blueprints came
from (scoped to /tags, client ThousandEyesClient; kiota-lock.json
committed), blueprints/thousandeyes-kiota rebinds the curated tag schema
onto fluent chains -- Tags().ById(id).Get/Put/Delete, Tags().Post -- with
method access, kiota's enum types behind parse companions, *uuid.UUID and
*time.Time fields and both live TypeEscaped manglings, and bindings check
verifies all 34 bindings against the real generated SDK. probe verify
re-derives identical facts from the shared recordings/thousandeyes tree:
switching dialect is a binding change, not an evidence change. The support
shell (client with a static bearer AccessTokenProvider, errors keyed on
ApiErrorable status codes with ErrEmptyResponse, convert with the
Stringer/time/kiota-enum helpers, the acceptance harness, provider and
main) is hand-written and owned by the pilot, as the boundary doctrine has
it, and the module builds and tests green.

Standing the pilot up surfaced and fixed three real emitter gaps: the
state mapper hard-coded a pointer before the response type (wrong for
interface-typed kiota responses -- the view now carries the full parameter
type), the nested expand helper declared a local slice named 'models' that
shadowed any models package alias (renamed elements, and elements are now
built by ConstructorExpr under a setter SDK instead of calling methods on
a nil interface), and the pre-declared shared diagnostics variable was
unused when every conversion in a loop declares its own (the views now
track shared-d use separately). A single nested attribute's wire
conversion under kiotaFluent is refused by validation until it is
rendered. The resty pilot regenerates byte-stable apart from the local
rename, and both drift checks are green; CI gains a pinned-kiota SDK
drift job (version and sha256 recorded in the workflow) and covers the
second pilot in the regenerate and bindings jobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
acceptance-kiota.yml runs the kiota pilot's generated acceptance tests
against the live tenant, mirroring acceptance.yml rule for rule: hand-
triggered or weekly-scheduled, never pull_request, gated on the same
'acceptance' environment whose THOUSANDEYES_* secrets it reuses -- one
tenant, one credential, two dialects. A separate workflow rather than a
matrix leg, deliberately: the resty run passing while this one fails is
the single most useful signal it can produce, naming the broken dialect
without reading logs. Both workflows share ONE concurrency group, because
both create tfacc-prefixed objects in the same tenant and two live runs
would fight over state that is not Terraform's; the kiota cron sits at
07:30 Monday behind resty's 06:00 only to keep the queue short.
docs/checks.md records the pairing and the local reproduction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 5ef7a9f into main Aug 4, 2026
8 of 12 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.

1 participant