Skip to content

Woven CAPI Specification 0.1.0

CAPI format: woven-capi, version 1 · Vocabulary freeze: CA-0 · Release: 2026-10-24

This page is the only authoring context for a CAPI agent. If the brief is complete, return exactly one JSON object conforming to the CAPI JSON Schema, with no surrounding prose or code fences. Lean is the intended downstream consumer; its generic Json.parse checks syntax, while an app-specific decoder and semantic gate must check CAPI meaning. If a required app fact is missing, ask concise questions first; do not emit a partial CAPI or silently choose an assumption.

The app brief supplies the app-specific facts. Ask about missing or contradictory facts; never invent permissions, business rules, retention, costs, errors, or platform support. If CA-0 cannot express a requirement, report it explicitly. Do not change the protocol or vocabulary to fit an app.

1. Boundary and version

CAPI describes the app's data, operations, request and answer types, errors, visibility, domain rules, costed flows, roles, and platform needs. It is backend-neutral. Do not put providers, doors, routes, event names, enclave designs, dataview designs, bindings, or secrets in CAPI JSON. The generic resolver consumes CAPI and designs infrastructure; a binding or generated server is not an appgen output.

The top-level capi_v is the integer CAPI vocabulary version (1 for CA-0). The page version 0.1.0, the vocabulary freeze CA-0, and the app's three-part version are different values. Do not bump capi_v to describe an app release. Use app version 1.0.0 for a new app unless the brief specifies another version.

2. JSON document

The schema fixes the field names, JSON types, enum words, and required members. Each top-level section means:

MemberContent
format, capi_v, app, name, versionFormat marker, frozen vocabulary version, lowercase app slug, display name, app release version.
modelentities and declared values; each model entity has its row/key/filter/patch/query/rows/change/search/ref names, key field, ordered typed fields, exact filter and patch columns, typed private search fields, and the two truthful transitional flags.
entitiesPer-entity holding (served, constant, modelOnly), remotely served verbs, device-kept and on-device verbs, refusal words, visibility, scope, persistence, and size declaration. Its keys must exactly match model.entities.
commandsNamed operations: kind (query, command, subscribe), request and answer CAPI types, optional gating entity, declared errors, and retry rule.
rolesOwner role, person entity/key, all role descriptions, owner columns, and per-entity write-role restrictions. List every role and state the actual owner.
errorsEvery app refusal word and its precise meaning. Reuse one word for one meaning.
rulesStable rule IDs and concise, testable statements of invariants and domain transitions from the brief. Do not turn prose into a new CAPI constructor.
asksOne entry for every exposed CRUD, on-device, or named operation; each has a stable ID, mode, window, total row order, and complete error/failure handling.
flowsTypical user flows as ask call counts, input sizes, and the six-dimensional usage budget. Include every typical use in the brief.
platformsHosts, required device capabilities, and explicit degradations where a host lacks a capability.

The JSON tags are literal strings. For example, CAPI type values are {"kind":"unit"}, {"kind":"scalar","scalar":"text"}, {"kind":"row","entity":"Bookmark"}, {"kind":"value","value":"Progress"}, or {"kind":"opt","of":{"kind":"row","entity":"Bookmark"}}. Column types are the narrower columnTy form in the schema. An ask operation is {"kind":"crud","verb":"list","entity":"Bookmark"}; an audience can be {"kind":"self","field":"owner"} or {"kind":"memberOf","group":"board","via":"Membership"}. Use the schema's exact JSON object shape; do not emit Lean constructor syntax.

Before emitting CAPI, confirm the brief determines the identity/person key representation, each entity's stable key type and who assigns it, immutable versus editable fields and who supplies timestamps, the exact meaning of every refusal word, the table-size classification, and realistic typical-flow call/input/cost bounds. Ask about any missing item. The app version is the only metadata default; never default app behavior or security policy.

model.entities.<Entity>.fields is the ordered row shape. Every field has a stable name, a type, and kind (indexable or sealed). The entity's key names exactly one key field, and that field's type is {"kind":"key","entity":"<same Entity>"}. keyType separately declares the key's scalar wire representation. filterCols must list exactly the required, indexable fields the server compares; it cannot include sealed or optional fields. patchCols lists exactly the writable fields. searchFields gives the typed fields of the private-device Search request; it is never sent to a server. Never use an optional filter to mean “no filter.”

Column type uses columnTy: a key to an entity, a scalar (text, nat, frac, time, flag), or a record of those scalars. CAPI request/answer ty additionally supports int, bytes, unit, sealed, row/key/filter/patch/query/rows/change/search/ref/value references, and recursive opt and list. Millisecond timestamps use time. Keep column schema types distinct from CAPI request/answer types.

The names object supplies the real generated type name for row, key, filter, patch, query, rows, change, search, and ref; a value declaration supplies its tsName. Unless the brief names types, use the entity ID as the row name and append Key, Filter, Patch, Query, Rows, Change, Search, or Ref for the corresponding type. Never guess a different neighboring type name. rowTyped and fieldsNamed report the current model truthfully; false is not evidence that a row is fully typed. opaque values are not usable in typed operations until defined.

sealed means opaque ciphertext held and returned verbatim. No server may compare, order, filter, group, index, or key by it. No provider receives plaintext or a person's secret key. Patches use optional fields to mean “leave unchanged”; do not nest options. Use an explicit tagged value when “clear” differs from “unchanged.”

Only served entities have remote CRUD verbs. constant and modelOnly entities are not server data and must not appear in a remote ask. A model-only identity declaration has no persisted rows or verbs and uses a nobody audience for all three visibility actions.

3. Operations, policy, and asks

The six uniform verbs have fixed request/answer types:

VerbRequestAnswer
getkey of entityoptional row
listquery containing filter and windowrows containing row list and ended
watchsame queryrows frames; ask mode keptOpen
putcomplete rowentity key
changekey plus patchunit
removeentity keyunit

A named command is for behavior the six verbs cannot express. Its kind, request, answer, ent, errors, and retry are explicit in commands. Do not hide a row read/write in a named command; use the entity verb. backoff is allowed only for an idempotent serving operation; reopen only for a subscription.

Visibility is per entity and per action: read, write, and remove. Audience JSON uses these constructors: anyone; self with a person field; role; memberOf with the row's group field and membership entity; since with those plus datum stamp and interval; is with field/value; typed along with field/target/nested audience; and recursive both, either, not. These are policy rules, not a private bit. windows may contain only unsignedClaim or plainBag; declaring a transition window does not itself open writes.

An ask's op is crud, onDevice, or named; each ask ID is unique and each operation appears exactly once. window is zero only for a non-many answer. Every list/watch has a positive maximum and a non-empty total order ending in that entity's key. Every declared refusal is listed under handles.errors; handles.failures covers door, wire, decode, timeout, and auth. An unknown refusal word is a door failure. A more n page returns the next rows in the stated order; ended is true only when no rows remain. Cursor identity is the asker's filter, independent of page size. Watch replay and live frames use the same audience rule.

4. Roles, rules, and budgets

State the owner role, person entity/key, every role, owner columns, group and membership fields used by audiences, and the roles allowed to write each entity. ownerCols lists fields on app rows that point to their owner; the person's identity key belongs in roles.person.key, not in ownerCols. An empty write-role list means there is no additional role-specific restriction; it never overrides the entity's visibility. Keep authorization (“who may commit”) separate from domain validity (“is this transition allowed”). Rules must state the valid states/transitions, patch meaning, invariants, and refusal words needed by the brief. If the brief requires a rule CA-0 cannot state, call it out instead of weakening it.

A flow call records an ask, maximum calls, and input size for non-windowed calls. Its budget has exactly these non-negative counters: stmt, rowOut, rowScan, rowWrite, fStep, sigCheck (statements, returned rows, scanned rows, written rows, fold steps, signature checks). State an honest bound; unknown or unbounded is not zero. A windowed read is costed at its ask's declared window. Do not claim the budget proves a particular backend implementation.

5. Validation boundary

First validate the JSON against the linked JSON Schema. Lean's Json.parse then checks JSON syntax. An app-specific decoder and semantic gate must additionally check facts the schema cannot express: unique and resolved identifiers; model/entity key equality; valid field, type, value, role, command, and audience references; exact filter/patch columns; no sealed comparison; fixed verb signatures; one ask per exposed operation; ask mode/window/order/handler completeness; declared retry safety; and valid flow references and budgets. rules[].statement is a human-readable contract assertion; neither JSON Schema nor generic JSON parsing proves its meaning. Test those rules separately. A valid JSON document is not, by itself, proof that generated servers implement it. Resolver checks are separate and consume CAPI without adding app-specific bindings to the input.

For the frozen Lean gate, accepts manifest binding = true means only that its current refusal list is empty; it does not prove infrastructure placement or runtime behavior. CAPI JSON contains no binding and cannot reach that typed gate until a JSON-to-vocabulary decoder exists. Report unsupported or pending facts explicitly. The Lean CAPI vocabulary is frozen at CA-0; CA-R1's request to remove rowTyped and fieldsNamed is pending and does not authorize changing or reinterpreting them.