api_binding_ui 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,53 +1,65 @@
1
- {
2
- "name": "api_binding_ui",
3
- "version": "2.0.1",
4
- "description": "API binding framework: bind any UI component to documented APIs at design time, run the bindings in production.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- },
14
- "./studio": {
15
- "types": "./dist/studio/index.d.ts",
16
- "import": "./dist/studio/index.js"
17
- },
18
- "./studio.css": "./dist/studio/index.css",
19
- "./package.json": "./package.json"
20
- },
21
- "files": [
22
- "dist",
23
- "spec/FRAMEWORK.md",
24
- "README.md",
25
- "SKILL.md"
26
- ],
27
- "sideEffects": [
28
- "**/*.css"
29
- ],
30
- "scripts": {
31
- "dev": "bun --port=5174 ./index.html",
32
- "build": "rm -rf dist && bun build src/index.ts src/studio/index.ts --outdir dist --root src --target browser --splitting --sourcemap=linked --external react --external react-dom --external react/jsx-runtime --external yaml && node ./node_modules/typescript/bin/tsc -p tsconfig.build.json",
33
- "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
34
- "test": "bun test",
35
- "prepublishOnly": "bun run typecheck && bun test && bun run build"
36
- },
37
- "peerDependencies": {
38
- "react": ">=18.0.0",
39
- "react-dom": ">=18.0.0"
40
- },
41
- "devDependencies": {
42
- "@types/node": "^22.13.0",
43
- "@types/react": "^19.2.0",
44
- "@types/react-dom": "^19.2.0",
45
- "react": "^19.2.0",
46
- "react-dom": "^19.2.0",
47
- "typescript": "^5.8.0",
48
- "vitest": "^3.0.0"
49
- },
50
- "dependencies": {
51
- "yaml": "^2.8.1"
52
- }
53
- }
1
+ {
2
+ "name": "api_binding_ui",
3
+ "version": "3.0.0",
4
+ "description": "Design-time API binding: register screen components, bind them to documented APIs in a studio, and generate plain React Query hooks over an Orval client. Nothing ships in the app.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./studio": {
15
+ "types": "./dist/studio/index.d.ts",
16
+ "import": "./dist/studio/index.js"
17
+ },
18
+ "./studio.css": "./dist/studio/index.css",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "spec/FRAMEWORK.md",
24
+ "README.md",
25
+ "SKILL.md"
26
+ ],
27
+ "sideEffects": [
28
+ "**/*.css"
29
+ ],
30
+ "scripts": {
31
+ "dev": "bun run build:app && node dist/cli.js studio src/dev/sample/bindings.json",
32
+ "build:app": "rm -rf dist/app && bun build ./index.html --outdir dist/app --minify",
33
+ "build": "rm -rf dist && bun build src/index.ts src/studio/index.ts --outdir dist --root src --target browser --splitting --sourcemap=linked --external react --external react-dom --external react/jsx-runtime --external yaml && bun build src/cli/cli.ts --outfile dist/cli.js --target node --external yaml --banner '#!/usr/bin/env node' && bun run build:app && node ./node_modules/typescript/bin/tsc -p tsconfig.build.json",
34
+ "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit -p tsconfig.json",
35
+ "test": "bun test",
36
+ "prepublishOnly": "bun run typecheck && bun test && bun run build"
37
+ },
38
+ "peerDependencies": {
39
+ "react": ">=18.0.0",
40
+ "react-dom": ">=18.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.13.0",
44
+ "@types/react": "^19.2.0",
45
+ "@types/react-dom": "^19.2.0",
46
+ "react": "^19.2.0",
47
+ "react-dom": "^19.2.0",
48
+ "typescript": "^5.8.0",
49
+ "vitest": "^3.0.0"
50
+ },
51
+ "dependencies": {
52
+ "yaml": "^2.8.1"
53
+ },
54
+ "bin": {
55
+ "api-binding": "dist/cli.js"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "react": {
59
+ "optional": true
60
+ },
61
+ "react-dom": {
62
+ "optional": true
63
+ }
64
+ }
65
+ }
package/spec/FRAMEWORK.md CHANGED
@@ -1,271 +1,176 @@
1
- # API Binding Framework — v1 (draft)
1
+ # API Binding Framework — v3
2
2
 
3
- Status: draft for review. Normative types: [`contract.ts`](./contract.ts).
3
+ Normative types: [`contract.ts`](../src/framework/contract.ts).
4
4
 
5
- This framework connects **UI components** to **documented API operations** during the design phase, and produces a reviewed, versioned **binding manifest** for every screen. Any product can adopt it. It contains no knowledge of any component kind, product, operation or field: all of that arrives as data or through registration.
5
+ The framework connects **UI components** to **documented API operations** at design time and turns the result into **plain code**. Everything about binding lives in one file, `bindings.json`. After design time the app holds only ordinary React Query hooks over an Orval client; no framework code, runtime or manifest ships.
6
6
 
7
7
  ---
8
8
 
9
9
  ## 1. Principles
10
10
 
11
- 1. **Nothing concrete in the framework.** No component kinds, product names, operation ids, field names or vocabularies are written into framework code. If something has to be known, it is declared as data (descriptor, registry entry, catalog, glossary) or registered as an extension.
12
- 2. **The API docs are the only source of operations.** The catalog is generated from the published OpenAPI documents. Nothing is supplied by the app instead: no local hooks, hardcoded option lists or free-text overrides.
13
- 3. **UI first.** The unit of work is a screen and its components. APIs are attached to component slots, not the other way round.
14
- 4. **Components describe themselves.** Every bindable component ships a descriptor stating its slots. A component built next year works with the same studio, the same manifest and the same runtime, unchanged.
15
- 5. **Types flow through bindings.** The shape of a selection, a row or a dialog's context is derived from what the source slot is bound to, never typed in by hand.
16
- 6. **Design-time and runtime are separate.** The studio never ships to production. The runtime reads manifests only.
17
- 7. **Every contract is versioned.** Descriptors, registry, catalog and manifest each carry a version, and changes are detected, not discovered in production.
11
+ 1. **Nothing concrete in the framework.** Component kinds, products, operations and fields arrive as data: kinds and the registry in the bindings file, operations from the OpenAPI docs.
12
+ 2. **The API docs are the only source of operations.** No local hooks, hardcoded option lists or free-text overrides.
13
+ 3. **UI first.** The unit of work is a screen and its components; APIs attach to component slots.
14
+ 4. **One file.** Kinds, screens, components, bindings and output settings are all in `bindings.json`.
15
+ 5. **No trace at run time.** The generator writes plain hooks; the app never imports this package.
16
+ 6. **Deterministic.** The same file and docs always produce the same code. Generated files carry an `@generated` mark and are never edited by hand.
18
17
 
19
18
  ---
20
19
 
21
- ## 2. The pieces and who owns them
22
-
23
- | Piece | What it is | Owned by | Format |
24
- |---|---|---|---|
25
- | Component descriptor | A kind's slots, with JSON Schema shapes | Component library (design system) | `ComponentDescriptor` |
26
- | Screen registry | Every screen in the app, with its component instances | Each product | `ScreenRegistry` (JSON) |
27
- | Catalog | Normalized operations from the API docs | Generated from OpenAPI with `catalogFromOpenApi` | `Catalog` (JSON) |
28
- | Binding manifest | Which operation or expression feeds each slot | Designers in the studio, reviewed by engineers | `BindingManifest` (JSON, one per screen) |
29
- | Extensions | Transforms, context providers, matchers, glossaries, persistence | Framework defaults + products | `FrameworkRegistry` |
30
- | Studio | Design-time editor that reads all of the above | This package | UI, design mode only |
31
- | Runtime | Executes manifests in the app | This package | Small library, production |
20
+ ## 2. The pieces
32
21
 
33
22
  ```
34
- OpenAPI docs ──► Catalog ─┐
35
- Descriptors ──────────────┼──► Studio ──► Manifests (repo) ──► Runtime (production)
36
- Screen registry ──────────┘
23
+ OpenAPI docs ──► Orval ──────────────► src/api/generated (typed client)
24
+ │
25
+ └──► api-binding studio ◄──► bindings.json ──► api-binding generate ──► src/api/screens (one hook per screen)
26
+ │
27
+ app components ── use<Screen>Api(state) ┘
37
28
  ```
38
29
 
39
- ---
40
-
41
- ## 3. Component descriptors
42
-
43
- A descriptor is published once per component kind, next to the component, by whoever builds the component. The framework only understands three slot **roles**:
44
-
45
- | Role | Meaning | Bound to |
30
+ | Piece | What it is | Owned by |
46
31
  |---|---|---|
47
- | `consume` | The component needs data | An operation (via request + response selection) or an expression |
48
- | `produce` | The component exposes data | Nothing; it is a **source** others can read |
49
- | `trigger` | The component emits an event | An ordered list of steps |
32
+ | Kinds | A component kind's slots, with JSON Schema shapes | Design system, in `bindings.json` → `kinds` |
33
+ | Screens and components | Every screen and component instance, by id | The app, in `bindings.json` |
34
+ | Bindings | Which operation or value feeds each slot | Designers in the studio, reviewed by engineers |
35
+ | Orval client | Typed functions per operation | Generated by Orval from the same docs |
36
+ | Screen hooks | `use<Screen>Api(state)` per screen | Generated by `api-binding generate` |
50
37
 
51
- Each slot carries a JSON Schema (`schema`) and an optional `many`. That is all the studio needs to render an editor, propose matches and validate.
52
-
53
- **Scoped slots.** A consume slot can be evaluated per item of a parent slot (`scope.parentSlot`). Inside it, `$item` is the parent's current item. This covers cells in rows, items in a list, cards in a lane, without the framework knowing what a row, list or lane is.
54
-
55
- **Feeding requests.** A consume slot can name produce slots of the same component in `feeds` (for example, paging or sorting state). The studio offers them as sources when mapping that slot's request.
38
+ ---
56
39
 
57
- ### Illustrations (not part of the framework)
40
+ ## 3. Ids
58
41
 
59
- These show three unrelated kinds expressed with the same vocabulary. Names are examples.
42
+ One namespace for screens and components, used everywhere: in `componentId` props, in the file and in expressions.
60
43
 
61
- ```jsonc
62
- // A data grid
63
- { "kind": "example-ds/grid", "version": 1, "label": "Grid",
64
- "children": { "scopedTo": "rows" },
65
- "slots": [
66
- { "id": "rows", "role": "consume", "many": true, "required": true, "label": "Rows",
67
- "schema": { "type": "object" }, "feeds": ["page", "sort", "filter"] },
68
- { "id": "total", "role": "consume", "label": "Total", "schema": { "type": "integer" } },
69
- { "id": "page", "role": "produce", "label": "Page", "schema": { "type": "object", "properties": { "offset": { "type": "integer" }, "limit": { "type": "integer" } } } },
70
- { "id": "sort", "role": "produce", "label": "Sort", "schema": { "type": "object", "properties": { "field": { "type": "string" }, "direction": { "enum": ["asc", "desc"] } } } },
71
- { "id": "filter", "role": "produce", "label": "Filter", "schema": { "type": "object" } },
72
- { "id": "selection", "role": "produce", "many": true, "label": "Selected rows", "schema": { "$ref": "#slot/rows" } },
73
- { "id": "rowClick", "role": "trigger", "label": "Row clicked", "schema": { "$ref": "#slot/rows" } }
74
- ] }
75
-
76
- // A single-choice input
77
- { "kind": "example-ds/select", "version": 1, "label": "Dropdown",
78
- "slots": [
79
- { "id": "options", "role": "consume", "many": true, "label": "Options",
80
- "schema": { "type": "object", "required": ["value", "label"], "properties": { "value": {}, "label": { "type": "string" } } } },
81
- { "id": "selected", "role": "produce", "label": "Selected option", "schema": { "$ref": "#slot/options" } },
82
- { "id": "change", "role": "trigger", "label": "Changed", "schema": { "$ref": "#slot/options" } }
83
- ] }
84
-
85
- // A dropdown publishes the whole chosen option, not only its display value: every field the options API
86
- // returns (an id, a code, a name) is then available as a separate, correctly typed value.
87
-
88
- // A component that does not exist yet
89
- { "kind": "example-ds/timeline", "version": 1, "label": "Timeline",
90
- "slots": [
91
- { "id": "events", "role": "consume", "many": true, "label": "Events",
92
- "schema": { "type": "object", "required": ["at", "title"], "properties": { "at": { "type": "string", "format": "date-time" }, "title": { "type": "string" }, "detail": { "type": "string" } } } },
93
- { "id": "eventClick", "role": "trigger", "label": "Event clicked", "schema": { "$ref": "#slot/events" } }
94
- ] }
44
+ ```
45
+ <product>.<screen>[.<sub-screen>…].<component>[.<part>…]
46
+ collections.worklist screen
47
+ collections.worklist.add-notes sub-screen (a dialog over the worklist)
48
+ collections.worklist.grid component on collections.worklist
49
+ collections.worklist.grid.column.customer-name
50
+ collections.worklist.grid.row.add-notes
51
+ collections.worklist.add-notes.notes-type component on collections.worklist.add-notes
95
52
  ```
96
53
 
97
- `"$ref": "#slot/<id>"` means "the same shape as that slot after binding". This is how a grid's selection gets the row shape of whatever operation feeds its rows, and how a dropdown's selected option gets the fields of the operation that feeds its options. Until that slot is bound, the shape is unknown and the studio offers nothing from it; it shows which slot to connect first.
98
-
99
- ---
100
-
101
- ## 4. Screen registry
102
-
103
- The registry lists the screens that exist in the app, and only those. It is the studio's inventory, so thousands of screens can be bound without opening the app.
104
-
105
- - Each screen ships its registry entry next to its code (a JSON file or an exported constant). The app's build collects the entries into one registry file.
106
- - A screen that is not in the app has no entry. Nothing is listed ahead of time.
107
- - Every component in an entry must have a registered descriptor. An unknown kind fails validation at build time, so the studio never shows a component it cannot bind.
108
- - `context` declares what a screen receives from outside. Its shape is either a schema or a reference to another screen's produce slot (`shape.from`), so a dialog opened from a grid toolbar automatically knows the selected rows' fields.
109
- - `parent` groups a dialog or drawer under the screen it appears over, for studio navigation only. How it opens and closes is the app's own UI code.
54
+ - Segments are lowercase kebab-case (`[a-z0-9-]`), joined by dots: `^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$`.
55
+ - A component belongs to the **longest** registered screen id that prefixes it. A screen's parent is the longest other screen id that prefixes it. Navigation is built from this; there is no `path` or `parent` to maintain.
56
+ - A component id may not equal a screen id (it would capture that screen's components).
57
+ - Ids are stable: renaming one orphans its bindings. They never depend on labels.
58
+ - In the app, each component gets `componentId="<id>"`; design-system components render it as `data-component-id`.
110
59
 
111
60
  ---
112
61
 
113
- ## 5. Catalog
62
+ ## 4. Kinds and slots
114
63
 
115
- `api-binding catalog pull <url-or-file>` normalizes one or more OpenAPI 3.x documents into a `Catalog`:
64
+ A kind declares slots with one of three **roles**:
116
65
 
117
- - Every operation, with parameters as request pointers (`/path/…`, `/query/…`, `/header/…`, `/body/…`), body schema and response schemas.
118
- - `intent` comes from HTTP semantics (safe methods are reads) unless the docs declare it with an extension. It is never inferred from names.
119
- - All `x-*` extensions are kept verbatim, so new docs metadata reaches matchers without a framework change.
120
- - A content hash pins the catalog. Manifests record the hash they were made with.
66
+ | Role | Meaning | Bound to |
67
+ |---|---|---|
68
+ | `consume` | The component needs data | An operation (request + response selection) or an expression |
69
+ | `produce` | The component exposes data | Nothing; a source others read |
70
+ | `trigger` | The component emits an event | Ordered API call steps, or "no API" |
121
71
 
122
- List locations in responses are discovered from schemas (any array of objects is a candidate selector). Envelope shapes are never assumed.
72
+ - `scope.parentSlot`: the slot is evaluated per item of the parent's slot (a cell in a row, an action on a row); `$item` is that item.
73
+ - `feeds`: produce slots of the same component offered as request sources (paging, sorting).
74
+ - `"$ref": "#slot/<id>"`: the same shape as that slot after binding (a grid's selection has the shape of its rows).
123
75
 
124
76
  ---
125
77
 
126
- ## 6. Binding manifest
78
+ ## 5. The bindings file
127
79
 
128
- One JSON file per screen in the product repository (for example `bindings/<screen-id>.binding.json`). It references only: bindIds and slot ids from the registry and descriptors, operation ids from the catalog, and expressions.
80
+ ```jsonc
81
+ {
82
+ "version": 3,
83
+ "openapi": "https://apidocs.example.com/index.yaml", // or a path next to this file
84
+ "output": { "dir": "src/api/screens", "client": "@/api/generated" },
85
+ "kinds": [ /* ComponentDescriptor */ ],
86
+ "screens": [ { "id": "collections.worklist.add-notes", "label": "Add Notes",
87
+ "context": [ { "id": "selection", "label": "Selected rows", "many": true,
88
+ "shape": { "from": { "screen": "collections.worklist", "component": "collections.worklist.grid", "slot": "selection" } } } ] } ],
89
+ "components": [ { "id": "collections.worklist.add-notes.notes-type", "kind": "hr-ds/select", "label": "Notes Type" } ],
90
+ "bindings": [ /* SlotBinding */ ]
91
+ }
92
+ ```
129
93
 
130
94
  | Slot role | Binding type | Contents |
131
95
  |---|---|---|
132
- | consume | `operation` | operation, request mapping (pointer → expression), response `select`, optional `map` to the slot's properties, load policy |
133
- | consume | `expression` | a single expression (scoped item field, another component's produce slot, context) |
134
- | trigger | `steps` | ordered API `call` steps; a call may run `forEach` over a list |
96
+ | consume | `operation` | operation, request mapping (pointer → expression), response `select`, optional `map`, `load` |
97
+ | consume | `expression` | one expression |
98
+ | trigger | `steps` | ordered `call` steps; a step may run `forEach` over a list; `onError: stop \| continue` |
135
99
  | trigger | `none` | reviewed: this event calls no API |
136
- | produce | — | never bound; produce slots are sources |
137
-
138
- Bindings describe API calls and nothing else. Opening and closing screens, navigation, reloading after a save and every other UI behaviour belong to the app's code, never to a manifest.
139
100
 
140
- Every binding records `origin` (manual or suggested) and `state` (draft or confirmed). Only confirmed bindings run in production.
101
+ Only `confirmed` bindings are generated; drafts are reported as warnings. Bindings describe API calls only: opening, closing, navigation, toasts and reloads are app code.
141
102
 
142
103
  ---
143
104
 
144
- ## 7. Expressions
145
-
146
- One grammar for every source. Nothing outside it is allowed.
105
+ ## 6. Expressions
147
106
 
148
107
  | Form | Resolves to |
149
108
  |---|---|
150
- | `<bindId>.<slot>` | A produce slot of a component on this screen, e.g. `subject.value`, `grid.selection`, `status.selected` |
151
- | `<bindId>.<slot>.<path>` | A property inside it |
109
+ | `<componentId>:<slot>[.path]` | A produce slot on the same screen, e.g. `collections.worklist.add-notes.subject:value` |
152
110
  | `<expr>[*].<path>` | A property of every item, as a list |
153
- | `$item.<path>` | The current item in a scoped slot or `forEach` |
154
- | `$context.<id>.<path>` | A value declared in the screen's `context` |
155
- | `$<provider>.<path>` | A registered context provider |
156
- | `$steps.<stepId>.response.<path>` | The response of an earlier `call` step |
157
- | `<expr> \| <transform>(<args>)` | A registered transform |
111
+ | `$item[.path]` | The current item: a scoped slot's item, a `forEach` item, or the trigger's payload |
112
+ | `$context.<id>[.path]` | A value declared in the screen's `context` |
113
+ | `$steps.<stepId>.response[.path]` | The response of an earlier step |
158
114
 
159
- Literal values are allowed only when the target schema defines them (`enum`, `const` or `default`). There is no free-text source.
115
+ `:` ends a component id, so ids can contain dots. Nothing outside this grammar is valid.
160
116
 
161
117
  ---
162
118
 
163
- ## 8. Suggestions
119
+ ## 7. Generated code
164
120
 
165
- The studio ranks **all** catalog operations for each slot and shows the best ones first. The user can always choose any operation. Ranking is done by registered `Matcher`s, combined by weight. The default matchers use only metadata already present in the inputs:
121
+ `api-binding generate` writes into `output.dir`:
166
122
 
167
- | Matcher | Signal |
168
- |---|---|
169
- | Shape | Compatibility between the slot's schema and the operation's response (consume) or request (trigger), including `many` and scalar types |
170
- | Intent | Slot role and `hints.intents` against the operation's `intent` |
171
- | Names | Token similarity between screen, component and slot labels and the operation's summary, tags and description |
172
- | Glossary | Synonym groups from registered glossaries (data files per product) |
173
- | Fields | For request and `map` entries: name and type similarity between available sources and target fields |
123
+ - `screen-helpers.ts`: the few helpers the hooks share (React Query only).
124
+ - `<screen id>.ts` per screen: `<Screen>State`, `<Screen>Api` and `use<Screen>Api(state)`.
125
+ - `index.ts`: re-exports.
174
126
 
175
- Rules for matchers: no product words in code, every score comes with plain reasons, and proposals stay `origin: "suggested"` until a person confirms them.
127
+ **The signature comes from the registry, the body from the bindings.** `<Screen>State` has one optional entry per component that produces values; `<Screen>Api` has one entry per consume and trigger slot. Changing bindings never changes either type, so call sites stay put. Adding or removing components does, and `tsc` points at every call site to update.
176
128
 
177
- ---
178
-
179
- ## 9. Validation
180
-
181
- The studio and CI (a script calling `validateRegistry` and `validateBindings`) apply the same rules. All are generic:
182
-
183
- - A `required` slot is unbound.
184
- - A binding refers to a bindId, slot, operation or descriptor version that no longer exists.
185
- - Schema incompatibility between a source expression and its target (type, `many` vs single, required request fields unmapped).
186
- - An expression uses a source not available on this screen.
187
- - A literal is not defined by the target schema.
188
- - The catalog hash changed: each affected binding is listed with what changed (field removed, renamed or newly required).
189
-
190
- ---
191
-
192
- ## 10. Studio rules
193
-
194
- The studio renders entirely from descriptors, registry, catalog and manifest:
129
+ | Slot | Returns | When unbound |
130
+ |---|---|---|
131
+ | consume | `Data<T>`: `{ bound, data, loading, error }` | `bound: false`; keep the UI's own value |
132
+ | consume, scoped | `Cell<T>`: `{ bound, read(item) }` | `read` returns `undefined` |
133
+ | trigger | `Action`: `{ bound, run(payload?), running, error }` | `run` resolves at once |
195
134
 
196
- - Navigation comes from `ScreenEntry.path` and `parent`. It scales to any number of screens.
197
- - Component lists come from `ScreenEntry.components`. Icons come from descriptors.
198
- - There are exactly three slot editors, chosen by role, and their forms are generated from the slot's schema. A new kind never needs studio code.
199
- - Every consume and trigger slot offers the full catalog in a searchable picker, with matcher suggestions on top.
200
- - The studio shows only what validates. A component with an unknown kind, or a binding that points at an operation no longer in the catalog, is reported by validation and kept out of the studio until it is fixed.
135
+ - Queries call the Orval function for the operation: path params in path order, then the body, then the query params object, then fetch options (headers). A query is enabled only when every required input has a value; `load: { everySeconds }` polls.
136
+ - Actions run steps in order. A failed call rejects `run` (`stop`) or is collected and rejected after the rest run (`continue`).
137
+ - Responses are unwrapped from Orval's `{ data, status, headers }`; a non-2xx status throws `ApiError`.
201
138
 
202
139
  ---
203
140
 
204
- ## 11. Runtime
205
-
206
- Entry `api_binding_ui`. Production-safe: it contains no studio code and no styles.
207
-
208
- ```tsx
209
- <BindingProvider catalog={catalog} manifests={manifests} fetcher={fetcher} transforms={…} providers={…}>
210
- <BindingScreen id="collections.worklist.add-notes" context={{ selection: selectedRows }}>
211
- …
212
- </BindingScreen>
213
- </BindingProvider>
214
- ```
215
-
216
- | Hook | Slot role | Returns |
217
- |---|---|---|
218
- | `useSlot(bindId, slot)` | consume | `{ status, data, error, reload }`; status is `unbound`, `waiting` (a required input has no value yet), `loading`, `ready` or `error` |
219
- | `useCell(bindId, slot)` | scoped consume | `(item) => value` for one row or item |
220
- | `useProduce(bindId, slot, value)` | produce | publishes the component's current value to the screen |
221
- | `useTrigger(bindId, slot)` | trigger | `{ bound, running, run(item?) }`; `run` performs the API calls and returns each step's outcome |
141
+ ## 8. Suggestions
222
142
 
223
- - Only **confirmed** bindings run. Drafts exist only in the studio.
224
- - Requests are built from request pointers and sent through the host's `fetcher`; base URL, authentication and error display stay in the app.
225
- - A component re-renders only when the values its own binding reads change. Identical requests on one screen share one call.
226
- - Without a confirmed binding a consume slot is `unbound` and a trigger has `bound: false`. What the component shows or does then is the app's decision.
227
- - Bindings never open, close, navigate or reload anything. After `run()` resolves, the app decides what happens next (for example close a dialog and call `reload()` on a grid).
143
+ The studio ranks **all** operations for each slot and shows the best first; any operation can be chosen. Ranking uses metadata only: shape compatibility, intent, name similarity, glossary synonyms. Request fields are proposed from the values the screen has (produce slots, context, earlier steps, the current item), ranked by name and type. Proposals stay `origin: "suggested"` until a person confirms them.
228
144
 
229
145
  ---
230
146
 
231
- ## 12. Design-time integration
232
-
233
- Entry `api_binding_ui/studio` (plus `api_binding_ui/studio.css`). Import it only from design-time code so production bundles never contain it.
147
+ ## 9. Validation
234
148
 
235
- - The app renders `<BindingWidget framework={{ descriptors, registry, transforms, providers }} />` once, inside `BindingProvider`, in design mode only. It is a floating button that opens the studio in a modal over the app, on the most recently shown `BindingScreen` (an open dialog wins over its page). Views that stay mounted while hidden (inactive tabs) are wrapped in `<BindingActive value={isVisible}>` so their screens are skipped. When the studio closes, the runtime reloads the saved manifests of mounted screens, so edits apply without a page reload.
236
- - `<BindingStudio>` is the same studio as a plain component, for hosts that mount it on their own page.
237
- - The studio loads the catalog from the published docs (`docsUrl`, default `https://apidocs.highradius.com/documentation-ui/index.yaml`; JSON or YAML; products from `x-tagGroups`). Each user can change the URL in Settings. A `catalog` passed in `framework` is used instead.
238
- - Settings also download and import manifests as JSON, so bindings made in the studio can be committed with the app.
239
- - The widget saves manifests through `BindingProvider persistence` (browser storage by default), so the app runs them immediately. Committed manifests come from Settings → Download.
149
+ `api-binding generate` fails, and writes nothing, on any error:
240
150
 
241
- ---
151
+ - an id that breaks the format, is duplicated, or clashes with a screen id; a component under no screen
152
+ - an unknown kind, or a parent that is not registered
153
+ - an expression reading a slot that is not produced on the screen, an undeclared context, a later step, or `$item` where there is none
154
+ - an operation not in the docs, a request input the operation does not have, an unconnected path parameter
155
+ - two components whose names become the same TypeScript key
242
156
 
243
- ## 13. Versioning
244
-
245
- | Contract | Version field | On change |
246
- |---|---|---|
247
- | Descriptor | `version` per kind | Manifests record `kinds`; the studio offers a migration when a kind's version rises |
248
- | Registry | `registryVersion` | Bumped when the shape changes; a migration ships with the package version that changes it |
249
- | Catalog | `catalogVersion`, `hash` | bindings to operations no longer in the catalog are reported by validation and hidden in the studio |
250
- | Manifest | `manifestVersion` | Bumped when the shape changes; a migration ships with the package version that changes it |
157
+ Warnings: drafts, unconnected required body fields, and with `--src` every registered id missing from the source (and every `componentId` literal not registered). `--check` exits 1 when generated files are out of date.
251
158
 
252
159
  ---
253
160
 
254
- ## 14. Conformance rules for an app (for example, an app built with Lovable)
161
+ ## 10. Studio rules
255
162
 
256
- 1. Every interactive or data-showing component comes from a library that publishes a descriptor for its kind.
257
- 2. Every component instance has a stable `bindId`, unique within its screen and independent of its label.
258
- 3. Every screen is wrapped in `BindingScreen` with a stable screen id and declares its `context`.
259
- 4. Components get data and call APIs only through their slots. No direct API calls, no hardcoded option lists, no local data hooks in screens. Opening, closing and navigation stay in the app's UI code.
260
- 5. Dialogs and drawers are separate screens with `parent` set to the screen they appear over.
261
- 6. Manifests are committed to the repository and reviewed like code.
262
- 7. The catalog is regenerated from the docs, never edited by hand.
163
+ - Runs locally from the CLI (`api-binding studio`); it reads and writes `bindings.json` and fetches the docs server-side, so the browser needs no CORS access.
164
+ - Navigation and component lists come from the ids. Icons come from kinds.
165
+ - Three slot editors, one per role, generated from slot schemas. A new kind never needs studio code.
166
+ - Shows only what validates.
263
167
 
264
168
  ---
265
169
 
266
- ## 15. Out of scope for the framework
170
+ ## 11. Conformance rules for an app
267
171
 
268
- - Any list of component kinds or slots (they belong to descriptors).
269
- - Any product, module, operation or field name (they belong to the registry, catalog and manifests).
270
- - Any vocabulary or naming rule (it belongs to glossaries and docs extensions).
271
- - Business behavior that is not expressed as slots and steps.
172
+ 1. Every bindable component has a `componentId` in the format above, registered in `bindings.json`.
173
+ 2. Each screen calls its generated `use<Screen>Api(state)` once, passes what its components produce and what it was opened with, and renders `bound ? data : <its own value>`.
174
+ 3. No direct API calls in screens other than through generated hooks; no hardcoded data an API should provide.
175
+ 4. Opening, closing, navigation, toasts and reloads stay in the app's UI code.
176
+ 5. Orval output and screen hooks are regenerated, never edited by hand.
@@ -1,36 +0,0 @@
1
- /** Builds requests, reads responses and runs trigger steps. */
2
- import type { CatalogOperation, OperationConsumeBinding, Step } from "./contract";
3
- import { type Scope } from "./expr";
4
- export interface HttpRequest {
5
- method: string;
6
- /** Path with parameters filled in, e.g. "/cls/v1/notes/42". */
7
- path: string;
8
- query: Record<string, string>;
9
- headers: Record<string, string>;
10
- body?: unknown;
11
- }
12
- /** Sends a request. Supplied by the host (base URL, auth, errors). */
13
- export type Fetcher = (request: HttpRequest, operation: CatalogOperation) => Promise<unknown>;
14
- export declare class BindingError extends Error {
15
- readonly detail?: unknown | undefined;
16
- constructor(message: string, detail?: unknown | undefined);
17
- }
18
- export declare function buildRequest(op: CatalogOperation, request: Record<string, string>, scope: Scope): HttpRequest;
19
- /** Identical requests share a key, and so one fetch. */
20
- export declare const requestKey: (r: HttpRequest) => string;
21
- /** "$", "$.a.b" or "$.a.items[*]" applied to a response. */
22
- export declare function select(response: unknown, selector: string): unknown;
23
- /**
24
- * Applies a consume binding's `map` to each item. Mapped properties are added to the item, not substituted for it,
25
- * so a produce slot shaped by this slot (a dropdown's selected option) keeps every field the API returned.
26
- */
27
- export declare function mapItems(data: unknown, binding: OperationConsumeBinding, scope: Scope): unknown;
28
- export declare function runConsume(binding: OperationConsumeBinding, op: CatalogOperation, scope: Scope, fetcher: Fetcher): Promise<unknown>;
29
- export interface StepOutcome {
30
- step: string;
31
- ok: boolean;
32
- response?: unknown;
33
- error?: unknown;
34
- }
35
- /** Runs call steps in order. `forEach` runs a call once per item; `onError: "stop"` ends the run at the first failure. */
36
- export declare function runSteps(steps: Step[], operations: Map<string, CatalogOperation>, scope: Scope, fetcher: Fetcher): Promise<StepOutcome[]>;