api_binding_ui 3.2.0 → 3.3.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/README.md CHANGED
@@ -41,6 +41,7 @@ export default defineConfig({ plugins: [/* …, */ bindingStudio()] }); // rea
41
41
  - Adds a floating **Bind API** button to every HTML page (SPA and server-rendered). It opens the studio over the app on the screen being shown, found from the `data-component-id` attributes the user can see; anything covered by a layer above it does not count.
42
42
  - **Alt+click** any component to open the studio on it; holding Alt outlines components.
43
43
  - The studio is served by the dev server under `/__binding/`, which reads and writes `bindings.json` and fetches the docs server-side. Writes are same-origin only.
44
+ - On start it generates the API client (runs the app's `orval.config.ts`, when present) and the screen hooks, before the server takes requests, so a project can run without committed generated code.
44
45
  - Every save, and every edit of `registry.json`, regenerates the screen hooks (errors are printed in the dev server's terminal; nothing is written then). Vite does not watch `bindings.json`, so a save never reloads the page under the studio.
45
46
 
46
47
  ## registry.json
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * Serves the studio under /__binding/ and injects a floating button into the app's HTML pages
9
9
  * (SPA and server-rendered alike). Every save regenerates the screen hooks, so Vite reloads the
10
- * screen with the new binding. `vite build` never runs it (apply: "serve").
10
+ * screen with the new binding. On start it generates the API client (Orval, when the app has
11
+ * orval.config.ts) and the screen hooks, so a project needs no committed generated code.
12
+ * `vite build` never runs it (apply: "serve").
11
13
  */
12
14
  import type { IncomingMessage, ServerResponse } from "node:http";
13
15
  export interface BindingStudioOptions {
@@ -39,11 +41,13 @@ export interface VitePluginLike {
39
41
  watcher?: {
40
42
  on: (event: "change", fn: (p: string) => void) => void;
41
43
  };
42
- }) => void;
44
+ }) => Promise<void>;
43
45
  }
44
46
  /** Adds the overlay script before the last </body> of HTML chunks; leaves everything else as it is. */
45
47
  export declare function injectOverlay(res: ServerResponse, tag: string): void;
46
48
  /** Regenerates the hooks after a save; logs the outcome, since the file is already saved either way. */
47
49
  export declare function regenerate(file: string, log?: (line: string) => void): Promise<void>;
50
+ /** Runs the app's Orval config, so the client the hooks import exists. Orval is the app's dependency, not this package's. */
51
+ export declare function generateClient(root: string, log?: (line: string) => void): Promise<void>;
48
52
  export declare function bindingStudio(options?: BindingStudioOptions): VitePluginLike;
49
53
  export {};
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/vite/plugin.ts
2
- import { readFileSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import path4 from "node:path";
4
4
 
5
5
  // src/server/handler.ts
@@ -1055,10 +1055,23 @@ async function regenerate(file, log = console.log) {
1055
1055
  log(`[api-binding] Screen hooks not regenerated: ${e.message}`);
1056
1056
  }
1057
1057
  }
1058
+ async function generateClient(root, log = console.log) {
1059
+ const config = path4.join(root, "orval.config.ts");
1060
+ if (!existsSync(config))
1061
+ return;
1062
+ try {
1063
+ const orval = "orval";
1064
+ const { generate } = await import(orval);
1065
+ await generate(config, root);
1066
+ log("[api-binding] API client generated.");
1067
+ } catch (e) {
1068
+ log(`[api-binding] API client not generated: ${e.message}`);
1069
+ }
1070
+ }
1058
1071
  function bindingStudio(options = {}) {
1059
1072
  const base = (options.base ?? "/__binding").replace(/\/+$/, "");
1060
1073
  const tag = `<script type="module" src="${base}/overlay.js"></script>`;
1061
- let file = "";
1074
+ let file = "", root = "";
1062
1075
  const norm = (s) => process.platform === "win32" ? path4.resolve(s).toLowerCase() : path4.resolve(s);
1063
1076
  const isBindingsFile = (p) => {
1064
1077
  const abs = norm(p), f = norm(file);
@@ -1076,11 +1089,16 @@ function bindingStudio(options = {}) {
1076
1089
  name: "api-binding-studio",
1077
1090
  apply: "serve",
1078
1091
  configResolved(config) {
1079
- file = path4.resolve(config.root, options.file ?? "bindings.json");
1092
+ root = config.root;
1093
+ file = path4.resolve(root, options.file ?? "bindings.json");
1080
1094
  },
1081
1095
  transformIndexHtml: (html) => html.includes(tag) ? html : html.replace(/<\/body>/i, `${tag}</body>`),
1082
1096
  config: () => ({ server: { watch: { ignored: [(p) => isBindingsFile(p)] } } }),
1083
- configureServer(server) {
1097
+ async configureServer(server) {
1098
+ if (existsSync(file)) {
1099
+ await generateClient(root);
1100
+ await regenerate(file);
1101
+ }
1084
1102
  const allowed = (req) => {
1085
1103
  const origin = req.headers.origin;
1086
1104
  return req.method === "GET" || !origin || new URL(origin).host === req.headers.host;
@@ -1108,6 +1126,7 @@ function bindingStudio(options = {}) {
1108
1126
  }
1109
1127
  export {
1110
1128
  bindingStudio,
1129
+ generateClient,
1111
1130
  injectOverlay,
1112
1131
  regenerate
1113
1132
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api_binding_ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/spec/FRAMEWORK.md CHANGED
@@ -1,178 +1,178 @@
1
- # API Binding Framework — v3
2
-
3
- Normative types: [`contract.ts`](../src/framework/contract.ts).
4
-
5
- The framework connects **UI components** to **documented API operations** at design time and turns the result into **plain code**. Binding lives in two files: `registry.json` (what exists: kinds and the screen → component → part tree) and `bindings.json` (how it is wired to the API). After design time the app holds only ordinary React Query hooks over an Orval client; no framework code, runtime or manifest ships.
6
-
7
- ---
8
-
9
- ## 1. Principles
10
-
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. **Two files.** `registry.json` says what exists (kinds, screens, components); `bindings.json` says how it is wired (output settings, bindings). The studio writes only `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.
17
-
18
- ---
19
-
20
- ## 2. The pieces
21
-
22
- ```
23
- OpenAPI docs ──► Orval ──────────────► src/api/generated (typed client)
24
- │
25
- └──► api-binding studio ◄──► bindings.json (+ registry.json) ──► api-binding generate ──► src/api/screens (one hook per screen)
26
- │
27
- app components ── use<Screen>Api(state) ┘
28
- ```
29
-
30
- | Piece | What it is | Owned by |
31
- |---|---|---|
32
- | Kinds | A component kind's slots, with JSON Schema shapes | Design system, in `registry.json` → `kinds` |
33
- | Screens and components | Every screen and component instance, by id | The app, in `registry.json` (a tree; nesting gives each part its parent) |
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` |
37
-
38
- ---
39
-
40
- ## 3. Ids
41
-
42
- One namespace for screens and components, used everywhere: in `componentId` props, in the file and in expressions.
43
-
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
52
- ```
53
-
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`.
59
-
60
- ---
61
-
62
- ## 4. Kinds and slots
63
-
64
- A kind declares slots with one of three **roles**:
65
-
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" |
71
-
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).
75
-
76
- ---
77
-
78
- ## 5. The bindings file
79
-
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
- ```
93
-
94
- | Slot role | Binding type | Contents |
95
- |---|---|---|
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` |
99
- | trigger | `none` | reviewed: this event calls no API |
100
-
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.
102
-
103
- ---
104
-
105
- ## 6. Expressions
106
-
107
- | Form | Resolves to |
108
- |---|---|
109
- | `<componentId>:<slot>[.path]` | A produce slot on the same screen, e.g. `collections.worklist.add-notes.subject:value` |
110
- | `<expr>[*].<path>` | A property of every item, as a list |
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 |
114
-
115
- `:` ends a component id, so ids can contain dots. Nothing outside this grammar is valid.
116
-
117
- ---
118
-
119
- ## 7. Generated code
120
-
121
- `api-binding generate` writes into `output.dir`:
122
-
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.
126
-
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.
128
-
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 |
134
-
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`.
138
-
139
- ---
140
-
141
- ## 8. Suggestions
142
-
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.
144
-
145
- ---
146
-
147
- ## 9. Validation
148
-
149
- `api-binding generate` fails, and writes nothing, on any error:
150
-
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
156
-
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.
158
-
159
- ---
160
-
161
- ## 10. Studio rules
162
-
163
- - Runs from the Vite dev plugin (`api_binding_ui/vite`, serve only) or the CLI (`api-binding studio`). Either server reads `registry.json`, reads and writes `bindings.json`, and fetches the docs server-side, so the browser needs no CORS access.
164
- - Under the Vite plugin every save regenerates the screen hooks, so a confirmed binding is live in the running app after the save. Vite does not watch `bindings.json`.
165
- - The dev plugin's floating button opens the studio over the app on the screen shown: the screen most `data-component-id` values the user can see belong to; a component covered by any layer above it does not count. Alt+click opens it on one component. Nothing reaches a build.
166
- - Navigation and component lists come from the ids. Icons come from kinds.
167
- - Three slot editors, one per role, generated from slot schemas. A new kind never needs studio code.
168
- - Shows only what validates.
169
-
170
- ---
171
-
172
- ## 11. Conformance rules for an app
173
-
174
- 1. Every bindable component has a `componentId` in the format above, registered in `registry.json`.
175
- 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>`.
176
- 3. No direct API calls in screens other than through generated hooks; no hardcoded data an API should provide.
177
- 4. Opening, closing, navigation, toasts and reloads stay in the app's UI code.
178
- 5. Orval output and screen hooks are regenerated, never edited by hand.
1
+ # API Binding Framework — v3
2
+
3
+ Normative types: [`contract.ts`](../src/framework/contract.ts).
4
+
5
+ The framework connects **UI components** to **documented API operations** at design time and turns the result into **plain code**. Binding lives in two files: `registry.json` (what exists: kinds and the screen → component → part tree) and `bindings.json` (how it is wired to the API). After design time the app holds only ordinary React Query hooks over an Orval client; no framework code, runtime or manifest ships.
6
+
7
+ ---
8
+
9
+ ## 1. Principles
10
+
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. **Two files.** `registry.json` says what exists (kinds, screens, components); `bindings.json` says how it is wired (output settings, bindings). The studio writes only `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.
17
+
18
+ ---
19
+
20
+ ## 2. The pieces
21
+
22
+ ```
23
+ OpenAPI docs ──► Orval ──────────────► src/api/generated (typed client)
24
+ │
25
+ └──► api-binding studio ◄──► bindings.json (+ registry.json) ──► api-binding generate ──► src/api/screens (one hook per screen)
26
+ │
27
+ app components ── use<Screen>Api(state) ┘
28
+ ```
29
+
30
+ | Piece | What it is | Owned by |
31
+ |---|---|---|
32
+ | Kinds | A component kind's slots, with JSON Schema shapes | Design system, in `registry.json` → `kinds` |
33
+ | Screens and components | Every screen and component instance, by id | The app, in `registry.json` (a tree; nesting gives each part its parent) |
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` |
37
+
38
+ ---
39
+
40
+ ## 3. Ids
41
+
42
+ One namespace for screens and components, used everywhere: in `componentId` props, in the file and in expressions.
43
+
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
52
+ ```
53
+
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`.
59
+
60
+ ---
61
+
62
+ ## 4. Kinds and slots
63
+
64
+ A kind declares slots with one of three **roles**:
65
+
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" |
71
+
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).
75
+
76
+ ---
77
+
78
+ ## 5. The bindings file
79
+
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
+ ```
93
+
94
+ | Slot role | Binding type | Contents |
95
+ |---|---|---|
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` |
99
+ | trigger | `none` | reviewed: this event calls no API |
100
+
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.
102
+
103
+ ---
104
+
105
+ ## 6. Expressions
106
+
107
+ | Form | Resolves to |
108
+ |---|---|
109
+ | `<componentId>:<slot>[.path]` | A produce slot on the same screen, e.g. `collections.worklist.add-notes.subject:value` |
110
+ | `<expr>[*].<path>` | A property of every item, as a list |
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 |
114
+
115
+ `:` ends a component id, so ids can contain dots. Nothing outside this grammar is valid.
116
+
117
+ ---
118
+
119
+ ## 7. Generated code
120
+
121
+ `api-binding generate` writes into `output.dir`:
122
+
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.
126
+
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.
128
+
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 |
134
+
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`.
138
+
139
+ ---
140
+
141
+ ## 8. Suggestions
142
+
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.
144
+
145
+ ---
146
+
147
+ ## 9. Validation
148
+
149
+ `api-binding generate` fails, and writes nothing, on any error:
150
+
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
156
+
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.
158
+
159
+ ---
160
+
161
+ ## 10. Studio rules
162
+
163
+ - Runs from the Vite dev plugin (`api_binding_ui/vite`, serve only) or the CLI (`api-binding studio`). Either server reads `registry.json`, reads and writes `bindings.json`, and fetches the docs server-side, so the browser needs no CORS access.
164
+ - Under the Vite plugin every save regenerates the screen hooks, so a confirmed binding is live in the running app after the save. Vite does not watch `bindings.json`. On start it also generates the API client (the app's `orval.config.ts`) and the hooks, so a project needs no committed generated code.
165
+ - The dev plugin's floating button opens the studio over the app on the screen shown: the screen most `data-component-id` values the user can see belong to; a component covered by any layer above it does not count. Alt+click opens it on one component. Nothing reaches a build.
166
+ - Navigation and component lists come from the ids. Icons come from kinds.
167
+ - Three slot editors, one per role, generated from slot schemas. A new kind never needs studio code.
168
+ - Shows only what validates.
169
+
170
+ ---
171
+
172
+ ## 11. Conformance rules for an app
173
+
174
+ 1. Every bindable component has a `componentId` in the format above, registered in `registry.json`.
175
+ 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>`.
176
+ 3. No direct API calls in screens other than through generated hooks; no hardcoded data an API should provide.
177
+ 4. Opening, closing, navigation, toasts and reloads stay in the app's UI code.
178
+ 5. Orval output and screen hooks are regenerated, never edited by hand.