|
| 1 | +# FR-021 — `api` metadata type + contract projections (design) |
| 2 | + |
| 3 | +_Status: PROPOSED (design sketch — needs brainstorming + plan before implementation)._ |
| 4 | +_Date: 2026-06-11._ |
| 5 | + |
| 6 | +## Problem |
| 7 | + |
| 8 | +The generated REST surface is **convention-derived**: FR-008/FR-009 define a cross-port |
| 9 | +CRUD contract (routes, filter grammar, pagination) computed from each entity. That covers |
| 10 | +CRUD well, but three needs have no declared home: |
| 11 | + |
| 12 | +1. **Non-CRUD operations** (RPC-shaped actions, batch ops, domain verbs) — today they are |
| 13 | + hand-written outside the metadata, invisible to `verify` and to doc/contract emitters. |
| 14 | +2. **Versioned wire contracts** — an API surface that must stay stable while the domain |
| 15 | + entities evolve (v1 and v2 served simultaneously over the same entities). |
| 16 | +3. **Contract emission** (FR-022: JSON Schema / OpenAPI / protobuf) — emitters need a |
| 17 | + declared operation surface and, for protobuf, **wire-stable field numbering**, which |
| 18 | + must not live on domain entities (it would couple domain evolution to wire compat). |
| 19 | + |
| 20 | +## Design direction |
| 21 | + |
| 22 | +### 1. API payloads are projections (reuse, don't invent) |
| 23 | + |
| 24 | +An operation's input/output references an `object.value` VO whose fields carry the |
| 25 | +existing `origin.*` provenance (`passthrough` / `aggregate` / `collection`) from entities |
| 26 | +— the **same projection machinery** that already drives prompt payloads (FR-004) and DB |
| 27 | +views (FR-003). The entity stays domain truth; the API projection is the contract surface. |
| 28 | +Renaming an entity field re-points (or breaks) the projection's `origin.*`, which `meta |
| 29 | +verify` catches at build time — the wire shape never silently changes. |
| 30 | + |
| 31 | +### 2. New metamodel vocabulary (provider-registered, ADR-0023) |
| 32 | + |
| 33 | +Slim sketch (names to be settled in brainstorming): |
| 34 | + |
| 35 | +```yaml |
| 36 | +api: # new top-level type; protocol-neutral surface |
| 37 | + name: ProgramApi |
| 38 | + version: v1 # bare attr; versioned surfaces = sibling api nodes |
| 39 | + children: |
| 40 | + - operation: # subtypes: query | command (read vs write semantics) |
| 41 | + name: getProgram |
| 42 | + inputRef: ProgramIdParam # object.value projection (or none) |
| 43 | + outputRef: ProgramSummary # object.value projection |
| 44 | + children: |
| 45 | + - binding.rest: { method: GET, path: "/programs/{id}" } |
| 46 | + # future: binding.grpc, binding.messaging — per-protocol bindings |
| 47 | +``` |
| 48 | + |
| 49 | +- **Derived CRUD stays the zero-config default.** With no `api` declared, FR-008/009 |
| 50 | + behavior is unchanged. A declared `api` *extends* (or, per entity opt-in, replaces) |
| 51 | + the derived surface. Emitters consume the union. |
| 52 | +- `binding.*` keeps the surface protocol-neutral with per-protocol attrs where they |
| 53 | + belong (REST method/path now; gRPC service/method naming later). |
| 54 | + |
| 55 | +### 3. Wire-stable numbering lives on the contract projection |
| 56 | + |
| 57 | +protobuf field numbers identify fields **on the wire**; renumbering silently misdecodes |
| 58 | +(it is equivalent to delete+add). Numbers therefore must be **authored, append-only |
| 59 | +metadata** — they genuinely cannot be computed from the model across schema evolution, |
| 60 | +which is the ADR-0023 "cannot be computed" justification recorded here. |
| 61 | + |
| 62 | +- `wireId: <n>` on fields of an `object.value` used as a contract projection (and |
| 63 | + `wireId` on `field.enum` members where the enum crosses the wire). Protocol-neutral |
| 64 | + name on purpose: protobuf consumes it now; Avro/Thrift-style emitters could later. |
| 65 | +- `reservedWireIds: [..]` + `reservedWireNames: [..]` on the VO — emitted as proto |
| 66 | + `reserved` so deleted fields can never be reused. |
| 67 | +- Validation (loader, own-only): duplicate `wireId` in one VO → `ERR_BAD_ATTR_VALUE`; |
| 68 | + `wireId` outside 1..536870911 or in 19000..19999 → `ERR_BAD_ATTR_VALUE`; a VO |
| 69 | + referenced by a proto-bound operation with any field missing `wireId` → emit-time |
| 70 | + error in FR-022 (not a load error — VOs without proto bindings don't need numbers). |
| 71 | +- Precedent: TypeSpec's protobuf emitter requires `@field(n)`; entproto requires |
| 72 | + `entproto.Field(n)`. Auto-numbering from declaration order is explicitly rejected |
| 73 | + (insert/rename/delete renumbers the tail = wire break). |
| 74 | + |
| 75 | +### 4. Relationship to the enterprise tier |
| 76 | + |
| 77 | +Application/ecosystem/dependency-level modeling (which org unit owns which API, what |
| 78 | +consumes what) is intentionally **out of scope** for this library — `api` here is the |
| 79 | +slim contract vocabulary only. An organization-level metadata tier can layer on top via |
| 80 | +the provider SPI without changes here. |
| 81 | + |
| 82 | +## Conformance + verify |
| 83 | + |
| 84 | +- New `fixtures/conformance/` fixtures: api/operation/binding loading, wireId validation |
| 85 | + errors, projection-ref resolution errors (`format: "resolved"` envelopes). |
| 86 | +- `meta verify --templates`-style gate extended: operation input/output refs must |
| 87 | + resolve; projections' `origin.*` must resolve against entities (existing machinery). |
| 88 | +- Registry-conformance manifest gains the new vocabulary in all 5 ports (atomic, like |
| 89 | + the `@responseRef` carve-out close). |
| 90 | + |
| 91 | +## Open questions (settle in brainstorming) |
| 92 | + |
| 93 | +1. Operation vocabulary: AIP-style five standard methods + custom, or free-form |
| 94 | + query/command only? |
| 95 | +2. Pagination style per surface: the FR-008 `limit/offset` contract vs AIP |
| 96 | + `page_size/page_token` when emitting proto services — per-binding attr? |
| 97 | +3. Does `endpoint` (a node per HTTP route) deserve first-class existence, or is |
| 98 | + `operation` + `binding.rest` sufficient? (Lean: the latter — fewer types.) |
| 99 | +4. Versioning semantics: sibling `api` nodes (v1, v2) vs `@version` on operations. |
| 100 | +5. Can a contract projection ALSO be a DB view projection (one VO, two consumers), or |
| 101 | + should that be discouraged by validation? |
| 102 | + |
| 103 | +## Dependencies / consumers |
| 104 | + |
| 105 | +- Consumes: `object.value` + `origin.*` (shipped), provider SPI (shipped). |
| 106 | +- Consumed by: FR-022 contract emitters; generated API docs (`meta docs` api surface); |
| 107 | + future MCP exposure (declared operations become discoverable tools). |
0 commit comments