Skip to content

Commit 309201a

Browse files
committed
chore: add AIP skills
1 parent 45fa58e commit 309201a

22 files changed

Lines changed: 896 additions & 252 deletions

.agents/skills/api-filters/SKILL.md

Lines changed: 320 additions & 0 deletions
Large diffs are not rendered by default.

.agents/skills/api/AIP.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

.agents/skills/api/SKILL.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ Each operation file uses `httptransport.NewHandlerWithArgs` with 4 arguments:
148148
3. **Response encoder**`commonhttp.JSONResponseEncoderWithStatus[T](http.StatusXxx)`
149149
4. **Options**`httptransport.AppendOptions(h.options, httptransport.WithOperationName("..."), httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()))`
150150

151+
> **List endpoints with filtering:** if the operation supports `?filter[...]` query parameters, use the `/api-filters` skill for the decoder and adapter wiring. It covers `api/v3/filters.Parse`, the typed filter structs, `Convert*` helpers, range splitting, and the Ent `.Select(field)` application — everything this skill does not cover.
152+
151153
Type alias convention at top of file:
152154

153155
```go
@@ -325,7 +327,31 @@ Reference: `api/v3/server/server.go:138-218`, `api/v3/server/routes.go`
325327

326328
## AIP Standards (Kong AIP)
327329

328-
Follow the API design standards in `api/spec/packages/aip/README.md` (symlinked as `AIP.md` next to this file). That document is the single source of truth for all AIP conventions including naming, enums, resources, visibility, CRUD templates, pagination, filtering, labels, errors, time/duration, content-type, bulk operations, versioning, and empty fields.
330+
OpenMeter v3 APIs follow [Kong's AIP](https://kong-aip.netlify.app/list/) conventions. Each rule lives in its own file under `rules/` next to this SKILL — open the rule file you need for the task at hand.
331+
332+
### Rule index
333+
334+
| File | Covers |
335+
| --------------------------------- | -------------------------------------------------------------------------------- |
336+
| `rules/aip-122-naming.md` | Naming conventions + base resource models (`Shared.Resource`) |
337+
| `rules/aip-126-enums.md` | Enum wire values, `Unknown` zero member, prefer-enum-over-bool |
338+
| `rules/aip-visibility.md` | `@visibility` + `Lifecycle.Read/Create/Update` |
339+
| `rules/aip-134-135-crud.md` | Create/Get/Update/Upsert/Delete templates, PATCH rules, DELETE rules |
340+
| `rules/aip-132-list.md` | List endpoints, sort, trailing slash |
341+
| `rules/aip-158-pagination.md` | Page-based and cursor-based pagination |
342+
| `rules/aip-160-filtering.md` | Filter query syntax, `Common.*FieldFilter` types, label dot-notation |
343+
| `rules/aip-129-labels.md` | Label key constraints, PATCH-with-null semantics |
344+
| `rules/aip-193-errors.md` | RFC-7807 error responses, `Common.ErrorResponses`, 403-before-404 rule |
345+
| `rules/aip-composition.md` | Composition-over-inheritance (spread, `model is`, `@discriminator`) |
346+
| `rules/aip-docs.md` | `@doc`/`/** */` requirements, `@operationId`, `@summary` |
347+
| `rules/aip-181-stability.md` | `x-private` / `x-unstable` / `x-internal` stability markers |
348+
| `rules/aip-142-time.md` | RFC-3339 timestamps, ISO-8601 duration deviation |
349+
| `rules/aip-137-content-type.md` | `Content-Type` validation, 415 on unsupported |
350+
| `rules/aip-235-bulk-delete.md` | `POST .../bulk-delete` transactional vs 207 partial |
351+
| `rules/aip-3101-versioning.md` | URL-path versioning, per-resource versioning |
352+
| `rules/aip-3106-empty-fields.md` | Always return all fields, `null` / `[]` / `{}` for empty |
353+
354+
For filtering specifically, `rules/aip-160-filtering.md` covers the **TypeSpec side** (which `Common.*FieldFilter` to pick, `Shared.ResourceFilters`, label dot-notation, `deepObject` exposure). The **Go implementation side** — parsing deepObject query params into typed filters, converting to `pkg/filter`, and applying Ent predicates — is in the `/api-filters` skill.
329355

330356
## Important Reminders
331357

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# AIP-122 — Resource names & URL paths
2+
3+
Reference: https://kong-aip.netlify.app/aip/122/
4+
5+
## What AIP-122 actually covers
6+
7+
AIP-122 is a narrow rule about **resource names, URL paths, and field names** — nothing more:
8+
9+
- **URL paths** use `kebab-case` for multi-word resource names: `/v1/konnect-services/domestic-cats`
10+
- Prefer lowercase resource names like `services`, `users`
11+
- **Field names** (JSON property names) are lowercase and use `snake_case` for multi-word names: `primary_color`, `billing_profile_id`
12+
- Uppercase, title case, and `camelCase` are exceptions and require justification
13+
14+
That's it. AIP-122 says nothing about TypeSpec model names, enum names, path parameter casing, or operation IDs.
15+
16+
## OpenMeter-local naming conventions (not from AIP-122)
17+
18+
These are locally enforced by linter rules in `api/spec/packages/aip/lib/rules/` but are **not** rules from AIP-122 itself. They are collected here for convenience.
19+
20+
| Element | Convention | Example | Source |
21+
| ------------------ | ------------ | --------------------------------------- | --------------------------------------- |
22+
| URL paths | `kebab-case` | `/api/v3/openmeter/llm-cost` | AIP-122 |
23+
| Field/property names | `snake_case` | `created_at`, `billing_profile_id` | AIP-122 |
24+
| Enum wire values | `snake_case` | `"unique_count"` | AIP-126 (see `aip-126-enums.md`) |
25+
| Operation IDs | `kebab-case` | `update-meter`, `list-billing-profiles` | AIP-134 / AIP-135 (see `aip-134-135-crud.md`) |
26+
| Model names (TypeSpec) | `PascalCase` | `BillingProfile` | OpenMeter linter only |
27+
| Enum type names (TypeSpec) | `PascalCase` | `MeterAggregation` | OpenMeter linter only |
28+
| Enum member names (TypeSpec) | `PascalCase` | `UniqueCount` | OpenMeter linter only |
29+
| Path parameters (TypeSpec) | `camelCase` | `meterId`, `customerId` | OpenMeter linter only |
30+
31+
The TypeSpec-facing casing (`PascalCase` model names, `camelCase` path parameters) is an OpenMeter convention because TypeSpec model identifiers and path parameter identifiers are separate from the JSON wire format. The wire format still complies with AIP-122.
32+
33+
## Base resource models (not from AIP-122)
34+
35+
These are OpenMeter `Shared.Resource` conventions defined in `api/spec/packages/aip/src/shared/resource.tsp`, not AIP-122 rules. They use AIP-122-compliant snake_case field names on the wire.
36+
37+
- **`Shared.Resource`**`id`, `name`, `description`, `labels`, `created_at`, `updated_at`, `deleted_at`
38+
- **`Shared.ResourceWithKey`** — same as `Shared.Resource` plus `key: ResourceKey` (`Lifecycle.Read, Lifecycle.Create` only)
39+
- **`Shared.ResourceImmutable`**`Shared.Resource` with `updated_at` and `deleted_at` omitted, for resources that cannot be mutated after creation
40+
41+
`public_labels` is **not** part of `Shared.Resource`; add it explicitly on resources that need publicly visible labels (see `aip-129-labels.md`).
42+
43+
```tsp
44+
model Meter {
45+
...Shared.Resource;
46+
47+
@visibility(Lifecycle.Read, Lifecycle.Create)
48+
event_type: string;
49+
}
50+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# AIP-126 — Enums
2+
3+
Reference: https://kong-aip.netlify.app/aip/126/
4+
5+
- All enum wire values must be `snake_case` (enforced as an error by the `casing-aip-errors` linter rule).
6+
- Every enum must define an `Unknown` member as the zero/default value.
7+
- Prefer enums over booleans for two-state fields — this allows a third state to be added later without a breaking change.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# AIP-129 — Labels
2+
3+
Reference: https://kong-aip.netlify.app/aip/129/
4+
5+
`Common.Labels` stores mutable user-managed metadata. `Common.PublicLabels` is for publicly visible labels. Labels are case-sensitive key/value pairs.
6+
7+
Note: `Shared.Resource` includes `labels` by default but **not** `public_labels` — add `public_labels` explicitly on resources that need it.
8+
9+
## Key constraints
10+
11+
- Maximum **63 characters**
12+
- Must **start and end with an alphanumeric** character
13+
- Permitted internal characters: alphanumerics, dashes (`-`), underscores (`_`), dots (`.`)
14+
- Reserved prefixes (case-insensitive): cannot begin with `kong`, `konnect`, `insomnia`, `mesh`, `kic`, `kuma`, or `_`
15+
16+
## Value constraints
17+
18+
- Maximum **63 characters** (same as keys — **not** 255)
19+
- Same character rules as keys
20+
- **Values must not be empty**
21+
22+
## Resource-level limits
23+
24+
- Maximum **50 user-defined labels** per resource
25+
- Both `labels` and `public_labels` return `{}` (empty object) when unset, never omitted — see `aip-3106-empty-fields.md`
26+
27+
## PATCH semantics
28+
29+
- `PATCH` with a `null` value **deletes** that label key
30+
- `PATCH` with an absent key leaves that entry untouched
31+
- `PATCH` with a new key adds the entry
32+
- Attempting to delete a missing key is not an error
33+
34+
## Filtering
35+
36+
Labels support filtering via dot-notation — see `aip-160-filtering.md`.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# AIP-132 — List endpoints
2+
3+
Reference: https://kong-aip.netlify.app/aip/132/
4+
5+
## Method, URL, and response shape
6+
7+
- List endpoints use `GET` on the entity root (e.g., `GET /meters`).
8+
- Responses must contain a `data` key holding the array of requested entities — this comes from the pagination response templates (`Shared.PagePaginatedResponse<T>`, etc.).
9+
- **Trailing slash returns `404`**`GET /meters/` must 404, `GET /meters` must 200.
10+
- List endpoints must follow the pagination AIP (see `aip-158-pagination.md`) unless the resource will provably never need pagination.
11+
12+
## Default sort order is mandatory and deterministic
13+
14+
AIP-132 requires a default sort order: *"Multiple requests to retrieve the same list must result in the same ordering of items."* Pick a field that is both stable and unique enough to produce a total order.
15+
16+
**OpenMeter convention** (not AIP-132 itself): prefer `name asc`, `created_at asc`, or `updated_at desc` as the default. Avoid UUIDs as the sort key — they are unique but randomly ordered, which makes the default sort feel arbitrary to clients. Always pair a non-unique sort column (like `name`) with a tie-breaker (like `id`) to preserve determinism.
17+
18+
## `sort` query parameter
19+
20+
Expose sorting via `@query(#{ name: "sort" }) sort?: Common.SortQuery`.
21+
22+
Sort values are a **comma-delimited list of attributes with optional `asc`/`desc` suffixes**, with the following syntax rules (from AIP-132):
23+
24+
- Ascending is the default — the `asc` suffix is optional
25+
- Descending requires the `desc` suffix, separated by a space
26+
- Multiple attributes sort left-to-right
27+
- Extra whitespace around delimiters is insignificant
28+
- JSONPath dot notation supports nested attributes (e.g., `foo.bar`)
29+
30+
**AIP example:** `?sort=foo,bar desc,foo.baz asc`
31+
32+
Sortable attributes should include all filterable attributes (so clients can sort on anything they can filter on). If the server cannot execute a particular sort expression, return `400 Bad Request`.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# AIP-134 / AIP-135 — CRUD request & response templates
2+
3+
References:
4+
5+
- https://kong-aip.netlify.app/aip/134/ (update)
6+
- https://kong-aip.netlify.app/aip/135/ (delete)
7+
8+
Use the generic templates from `shared/request.tsp` and `shared/responses.tsp`. Do not define ad-hoc request/response types.
9+
10+
| Purpose | Request template | Response template | HTTP status |
11+
| --------------- | ------------------------- | ----------------------------------- | ----------- |
12+
| Create (POST) | `Shared.CreateRequest<T>` | `Shared.CreateResponse<T>` | 201 |
13+
| Upsert (PUT) | `Shared.UpsertRequest<T>` | `Shared.UpsertResponse<T>` | 200/201 |
14+
| Update (PATCH) | `Shared.UpdateRequest<T>` | `Shared.UpdateResponse<T>` | 200 |
15+
| Get (GET) || `Shared.GetResponse<T>` | 200 |
16+
| Delete (DELETE) || `Shared.DeleteResponse` | 204 |
17+
| Page list || `Shared.PagePaginatedResponse<T>` | 200 |
18+
| Cursor list || `Shared.CursorPaginatedResponse<T>` | 200 |
19+
20+
## AIP-134 update rules
21+
22+
Both `PATCH` and `PUT` are **required** for all entities (mandate introduced 2025-04-08). The sole exception: `PATCH` may be omitted when the full entity representation is needed to validate an update.
23+
24+
### PATCH (partial update)
25+
26+
- Implements JSON Merge Patch (RFC 7396) with **mandatory recursive patching** of nested objects.
27+
- Operation ID: `update-<entity>` (kebab-case).
28+
- Rejects requests with `Content-Type` other than `application/json` with `400 Bad Request`.
29+
- Rejects unknown fields and read-only fields with `400 Bad Request`, naming them in `invalid_parameters`.
30+
- Null-value semantics:
31+
- For non-required properties: removes the property
32+
- For schema-less object properties: removes the property
33+
- For required-nullable properties: sets them to null
34+
- For required non-nullable properties: `400 Bad Request`
35+
36+
### PUT (upsert)
37+
38+
- Returns `201 Created` when creating an entity, `200 OK` when replacing an existing one.
39+
- Operation ID: `upsert-<entity>` or `update-<entity>` (kebab-case).
40+
- **Creation via PUT only works when the entity uses customer-supplied IDs** (unique within the organization or parent scope, not globally).
41+
- For entities with system-generated globally-unique IDs, PUT only supports **replacement**; missing entities must return `404 Not Found`.
42+
43+
## AIP-135 delete rules
44+
45+
- DELETE returns `204 No Content` on success.
46+
- **No request body is accepted**; all parameters go in the URL path, not the query string.
47+
- Return `403 Forbidden` before `404 Not Found` — check permissions before existence to prevent resource enumeration.
48+
- Soft-deleted resources return `404 Not Found` on subsequent DELETE calls.
49+
- Cascading deletes:
50+
- If no protected resources would be affected, proceed as a normal DELETE.
51+
- If protected resources would be affected and `?force=true` is **not** supplied, return `400 Bad Request` listing the affected resource types in the error detail.
52+
- If `?force=true` is supplied, delete all child resources and associations, returning `204`.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# AIP-137 — Content-Type
2+
3+
Reference: https://kong-aip.netlify.app/aip/137/
4+
5+
## Default content type
6+
7+
When the `Content-Type` header is absent, it defaults to `application/json; charset=utf-8` (note: the default **includes** a charset). APIs must not reject valid JSON payloads that omit the header.
8+
9+
## Validation
10+
11+
Validate the request body's `Content-Type` on `POST`, `PUT`, and `PATCH`. Validation fails when:
12+
13+
- The `Content-Type` header specifies an unsupported type, **or**
14+
- The body does not match the declared type
15+
16+
Either failure mode returns `415 Unsupported Media Type`. When responding with 415, include an `Accept-{METHOD}` header listing supported types (e.g., `Accept-POST: application/json`). If the rejection was caused by a charset mismatch, the charset directive must appear in the `Accept-{METHOD}` value.
17+
18+
## Exclusions
19+
20+
**Endpoints without request bodies are exempt** from content-type validation — there is nothing to validate.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# AIP-142 — Time & duration
2+
3+
Reference: https://kong-aip.netlify.app/aip/142/
4+
5+
## Timestamps (AIP-compliant)
6+
7+
- **Field name pattern**: `<past_participle>_at` for fields representing a point in time — e.g., `created_at`, `updated_at`, `deleted_at`, `expires_at`
8+
- **Wire format**: RFC-3339 string in UTC with `Z` suffix, e.g., `"2023-02-27T02:15:00Z"`
9+
- `T` separates date and time; fractional seconds are optional
10+
11+
## Durations (OpenMeter deviates from AIP-142)
12+
13+
AIP-142 specifies durations as **integer values with a unit suffix in the field name**: `ttl_ms`, `flight_duration_mins`, `lifespan_yrs`. Permitted suffixes: `ns`, `ms`, `secs`, `mins`, `hrs`, `days`, `yrs`. Values must be non-negative integers within `0 <= N < 2^53`.
14+
15+
**OpenMeter does not follow AIP-142 for durations.** Instead, OpenMeter uses the **ISO-8601 duration format** as an opaque string:
16+
17+
- `PT1M` — one minute
18+
- `PT1H` — one hour
19+
- `P1D` — one day
20+
- `P1M` — one month
21+
22+
Components: years (`Y`), months (`M`), weeks (`W`), days (`D`), time separator `T`, hours (`H`), minutes (`M` after `T`), seconds (`S`). When authoring a new duration field in OpenMeter, use the ISO-8601 form and do **not** append a unit suffix to the field name. This deviation is intentional and documented here so reviewers know to allow it.

0 commit comments

Comments
 (0)