Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

## 2. Point the sources at the capability

- [ ] 2.1 `osapi` — `docs/docs/sidebar/sdk/guidelines.md` keeps its worked
- [x] 2.1 `osapi` — `docs/docs/sidebar/sdk/guidelines.md` keeps its worked
examples and points at the capability for the rules
- [ ] 2.2 `osapi` — `CLAUDE.md` drops the SDK naming block, which the capability
- [x] 2.2 `osapi` — `CLAUDE.md` drops the SDK naming block, which the capability
now states

## 3. Verification

- [ ] 3.1 Confirm no service method repeats its service name
- [ ] 3.2 Confirm no public signature contains a generated type
- [ ] 3.3 Confirm every exported result field carries a JSON tag
- [ ] 3.4 Confirm no consumer imports the generated package
- [x] 3.1 Confirm no service method repeats its service name
- [x] 3.2 Confirm no public signature contains a generated type
- [x] 3.3 Confirm every exported result field carries a JSON tag
- [x] 3.4 Confirm no consumer imports the generated package
2 changes: 1 addition & 1 deletion openspec/changes/specify-documentation-homes/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
blocks: cross-layer consistency, provider idempotency, broadcast response
shape, HTTP verb separation, OpenAPI validation, SDK method naming, and the Go
code standards (signatures, `types.go`, test-file naming, mocking)
- [ ] 3.5 `osapi` — propose a capability from `docs/sidebar/sdk/guidelines.md`;
- [x] 3.5 `osapi` — propose a capability from `docs/sidebar/sdk/guidelines.md`;
"never expose generated types" and "JSON tags required" bind every consumer of
`pkg/sdk`, including `osapi-orchestrator`
- [ ] 3.6 `osapi` — resolve the three-way duplication of branching, commit
Expand Down
95 changes: 95 additions & 0 deletions openspec/specs/sdk-standards/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# sdk-standards Specification

## Purpose

Records what the SDK at `pkg/sdk` guarantees the repositories built on it, so a
consumer can read the contract it depends on rather than infer it from another
repository's contributor documentation.

## Requirements

### Requirement: A method name does not repeat its service

A service method SHALL be named for the action alone. The service already
supplies the namespace, so repeating it in the method reads twice at every call
site.

Methods SHALL use `List`, `Get`, `Create`, `Update`, and `Delete` for operations
on a resource. An operation with no persistent resource — a one-shot action or a
command execution — MAY use a verb naming what it does.

#### Scenario: A service gains a read method

- **WHEN** a service is added for a domain
- **THEN** its read method is `Get`, not the domain name followed by `Get`

#### Scenario: An operation has no resource

- **WHEN** an operation performs an action rather than acting on a stored
resource
- **THEN** it is named for the action, because none of the five resource verbs
describes it

### Requirement: Generated types stay inside the SDK

A public method signature SHALL NOT contain a type from the generated OpenAPI
package. The SDK exists to hide that package; a signature naming one requires
every consumer to import it, and re-exports each regeneration as a breaking
change.

A consumer needing to import the generated package indicates the SDK is missing
a wrapper, rather than indicating the consumer should import it.

#### Scenario: A request needs a generated body type

- **WHEN** a method sends a request whose body is a generated type
- **THEN** the method accepts an SDK-defined type and builds the generated one
internally

#### Scenario: A consumer reaches for the generated package

- **WHEN** a consumer cannot express a call without importing the generated
package
- **THEN** the SDK adds the missing wrapper, rather than the consumer adding the
import

### Requirement: Every exported result field carries a JSON tag

Every exported field on a result type SHALL carry a `json` tag naming the key in
`snake_case`.

The tags are load-bearing rather than decorative: results are converted to
generic maps by round-tripping through JSON, and an untagged field arrives under
its Go name, which does not match the key the API returned.

A field whose absence is meaningful SHALL use `omitempty`. A field a caller must
always be able to read SHALL NOT, so that a false or empty value is
distinguishable from a field that was never set.

#### Scenario: A result is converted to a map

- **WHEN** a result is converted to a generic map
- **THEN** its keys match the API's, because each field names its key

#### Scenario: A mutation reports whether it changed anything

- **WHEN** a mutation result reports that nothing changed
- **THEN** the field is present and false, rather than omitted

### Requirement: Errors reaching a consumer carry context

An error returned from an SDK method SHALL name the operation that produced it.

A response body SHALL be checked for nil after its status is checked, because a
status alone does not establish that a body was returned.

#### Scenario: A call fails inside a wrapped client

- **WHEN** an underlying call fails
- **THEN** the returned error names the SDK operation, so a consumer's log
identifies the call without a stack trace

#### Scenario: A success status arrives with no body

- **WHEN** a response carries a success status and no body
- **THEN** the SDK returns an error rather than dereferencing it