@lotics/cli 0.262.0 → 0.264.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.
@@ -1,7 +1,7 @@
1
1
  /** Enabled by an explicit `LOTICS_TELEMETRY=1`. Anything else, including unset, is off. */
2
2
  export declare function telemetryEnabled(env?: NodeJS.ProcessEnv): boolean;
3
3
  /**
4
- * The verb path of an invocation — `app.workflow.set`, `run.query_records` —
4
+ * The verb path of an invocation — `model.apply`, `run.query_records` —
5
5
  * from the raw argv positionals.
6
6
  *
7
7
  * Positionals stop at the first token that is a flag, a JSON blob, an `@file`,
@@ -1,113 +1,31 @@
1
1
  # Building an app, end to end
2
2
 
3
- The other references here describe **contracts** — what a query may express, what a workflow body
4
- may say, which props a component takes. This one describes the **sequence**: the order the steps go
5
- in, and why that order and not another. Read it once for the shape, then reach for the area doc
6
- (`lotics docs`) whenever you need the detail.
3
+ The other references here describe **contracts** — what a model may state, what a tool takes. This
4
+ one describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then
5
+ reach for the area doc (`lotics docs`) whenever you need the detail.
7
6
 
8
- Everything below is deploy-free until the last step. That is the point: a deploy is a release, and
9
- using one to find out whether something works is the slowest possible way to learn it.
7
+ **The live app is the only edit surface.** Every change to an app — applying a model, deploying a
8
+ build, setting one query or workflow — mints a new version of it, and rolling back to an earlier
9
+ version is the undo. Nothing about an app lives in a local directory the platform reads back, so
10
+ there is no project to keep in sync and no deploy to find out whether something works.
10
11
 
11
- ---
12
+ **Rolling back restores the app, not the data.** A table change the model made, and every row a
13
+ workflow wrote while you tried it, stay where they are. Try a write on a throwaway record.
12
14
 
13
- ## 1 — Scaffold, or pull what exists
15
+ ---
14
16
 
15
- ```
16
- lotics app preview model.json#<app> --shots shots/ # the model's app rendered: no workspace
17
- lotics app create "<name>" # new: a real Vite + React + TS project, deps installed
18
- lotics app create "<name>" --from model.json#<app> # new, from a checked model: app.json, rendered
19
- lotics app create "<name>" --api # new, no screens: its declarations are the whole surface
20
- cd <dir> && lotics app regenerate # existing + generated: recompile it from the model
21
- cd <dir> && lotics app pull <app_id> # existing: refresh to the latest first, then read its README
22
- lotics workspace build model.json # the whole model: check, apply, then every app in it
23
- ```
17
+ ## 1 — Two kinds of app
24
18
 
25
- `lotics workspace build` is the create and regenerate lines above in one, for a model that
26
- declares several apps: `scaffold check`, `scaffold apply` where this workspace differs from the
27
- model, then per app `create --from` into `<dir-of-model>/<alias-with-dashes>` when that directory
28
- does not exist and `regenerate` there when it does, then `app check` — and `app deploy` under
29
- `--deploy`. An app that conflicts or checks red is named and the rest still run, because the one
30
- you are going to fix is not a reason to leave the others in a state nobody knows. `--dry-run`
31
- writes nothing and says which apps it would create and which it would regenerate.
32
-
33
- **An app is stated, not drawn.** In `model.json`, `records` says how a row of each entity is
34
- recognised — its title, the line under it, its picture, its status, its figure, its deadlines, what it is about — and
35
- each entry of `apps` is one register over one entity and the record its rows open: the register's
36
- columns and filters, the record's sections in the order its work reaches them, the acts and the checks that guard them.
37
- That is the whole vocabulary (`lotics docs model/apps`). How each piece looks is the runtime's, one
38
- way per concept, and no key changes it. `lotics scaffold check` prints each app as its reader will
39
- see it, with every value a default supplied marked `(default)` — review the app there, before
40
- anything exists.
41
-
42
- **Work people are assigned is a task list, stated like any other.** Give the child entity a stored
43
- status select with `closed` options, a member field, and a deadline under `due`, and state
44
- `"task": true` in its `records`. Every table of its rows — a rows block of the record it belongs to,
45
- its own register, the rows filed under one of them (its steps) — then draws each row as a task: its
46
- ring ticks it done by the act of that entity setting a closed option — stamping and recording what
47
- that act does — else by its own save, unticks it by the act moving it back, and its status moves in
48
- place. State a second act `of` the child with `on: "rows"` to complete several at once.
49
- `starts: { "<member>": "me" }` fills the assignee from whoever adds the row. `opens: { "<member>":
50
- "me" }` opens a register on the reader's own rows, and a reading's `where` takes `"me"` the same
51
- way — both narrowed on the server. A register over that child — "My tasks" — rings its rows by
52
- its own acts the same way. `scaffold check` prints each ring's write — a ring no write reaches is
53
- drawn disabled, and the check notes it.
54
-
55
- **Look at it before anything exists.** `lotics app preview <model.json>#<app>` compiles the app
56
- against the model's own ids, answers every read from the model's `rows` (a few synthesized for an
57
- entity that states none), renders the register, the record in its door and the add dialog at 1280
58
- and 375, and runs the probes `app check --screens` runs — no workspace, no credential, nothing
59
- created. `--shots <dir>` writes what it measured as PNGs.
60
-
61
- `--from` takes the model file `lotics scaffold check` approved and the app in it, and writes
62
- **`app.json`** — the app compiled against the live `tbl_`/`fld_`/`opt_` ids its entities became —
63
- beside a `src/main.tsx` that mounts `@lotics/app-runtime` over it, one `src/workflows/<alias>.ts`
64
- per write and an empty `src/components/index.ts`. Every write — a create, an edit, a remove, each
65
- act — is a generated workflow that re-checks on the server what the model states: required fields,
66
- `write_rules`, an act's `when` and `requires`, and the checks that block it. The runtime draws the
67
- spec; there is no screen source to edit (`node_modules/@lotics/app-runtime/AGENTS.md`). The tables
68
- have to exist (`lotics scaffold apply` first); a table or field the workspace lacks is refused by
69
- name. The app is recorded against its model alias, so a second `--from` for the same alias is
70
- refused. The project's `README.md` is the app in prose: its register, its record and the tables
71
- behind them.
72
-
73
- `--api` scaffolds the app something OUTSIDE Lotics calls (§ 9): the manifest, `src/workflows/`, a
74
- CI job and the two briefs, with no `index.html`, no `src/App.tsx`, no Vite config and no kit. Its
75
- one devDependency is `typescript`, because `app workflow check` runs the project's own compiler.
76
- Nothing is built and nothing is deployed, so `current_version_id` stays null and the surface goes
77
- live through `app query set` / `app workflow set` instead. It cannot be combined with `--from`,
78
- which describes screens; `app dev` and `app check --screens` refuse a project with no Vite
79
- config, `app deploy` one with no `build` script, and `app pull` rebuilds its project from the app row,
80
- since there is no archive. Steps 7 and 8 below do not apply to
81
- it, and step 9 ships it without a build: `npm run typecheck` (it declares no `lint` and no `test`
82
- script), `lotics app check` on its own, then `lotics app api publish` where a screens app deploys.
83
- Every other step applies unchanged.
84
-
85
- A pull writes more than source: one `src/workflows/<alias>.ts` per bound workflow, one
86
- `src/agents/<alias>.md` per bound agent, and the `.lotics/` type companions — so an existing app
87
- arrives fully editable rather than as an archive you have to reconstruct.
88
-
89
- **The model changes after the app is built, and `lotics app regenerate` is how it lands.** Run it
90
- inside the app; the model is the one `package.json#lotics.plan` remembers, unless `--from` names
91
- another. The generator owns what it emits: `app.json` is compiled from the model and rewritten
92
- whole, and a generated workflow body is rewritten too, with the text it replaced parked in
93
- `.lotics/regenerate-dropped.patch`. Two things are yours and survive every regeneration:
94
- `src/components/` (seeded where it is absent, never rewritten), and the body of an act's own
95
- `workflow` — only the guard region at its top, between the `<lotics:guards>` markers, is the
96
- generator's. The manifest is reconciled by ownership — the generator's aliases replaced, the ones
97
- it retired removed, the ones you added kept — then codegen and `app check` run. A generated
98
- workflow is declared with the inputs it reads and the `outputs` its return hands back, so a
99
- contract an earlier generation bound is replaced at the next deploy; your own act's `workflow`
100
- keeps the outputs the server derived from your body. That body follows the guard region, which
101
- already declares `i` and, on one record, reads it into `row` by `i.record_id`: reuse them, never
102
- declare them again.
103
-
104
- **Nothing is pushed.** What the live app RUNS changes at `lotics app deploy` and nowhere else: the
105
- bundle production serves was built against the bindings it has, so a regeneration that replaced a
106
- live workflow body left a deployed dialog posting inputs that workflow no longer declared. The
107
- summary names what a deploy will add, change and leave bound instead. `--bind-new` binds the
108
- aliases the app does not have YET — `lotics app dev` forwards queries to production, so a new alias
109
- cannot be tried before something binds it — and refuses, naming them, to touch one that already
110
- exists. `--dry-run` prints the whole run and writes nothing.
19
+ - **An app stated in a model** — the default, and the right one for almost every job. `model.json`
20
+ says how a row of each entity is recognised (`records`) and, in `apps`, one register over one
21
+ entity and the record its rows open: its columns and filters, its sections in the order its work
22
+ reaches them, its acts and the checks that guard them (`lotics docs model/apps`). The platform
23
+ compiles each app and draws it with the runtime every such app shares, so how each piece looks is
24
+ the platform's, one way per concept, and no key changes it. Every write the app makes is a
25
+ generated workflow that re-checks on the server what the model states.
26
+ - **A custom-code app** — React you write, for a surface the model's vocabulary cannot state. It
27
+ reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever
28
+ React you choose.
111
29
 
112
30
  ## 2 — Clarify what is being asked, before modelling it
113
31
 
@@ -126,12 +44,11 @@ wrong. Ask until there is no ambiguity left:
126
44
  This is the step that gets skipped under time pressure, and it is the only one whose mistakes are
127
45
  invisible in review: every later artifact is correct with respect to the wrong definition.
128
46
 
129
- ## 3 — The data model, before any code
47
+ ## 3 — The data model, before any app
130
48
 
131
- Get this wrong and nothing above it can be precise. Each entity is its own table with
132
- `record_link`s into the spine; attributes and evidence are fields on their owner. A single table
133
- with a `type` column standing in for three entities collapses the distinctions every later query
134
- needs.
49
+ Get this wrong and nothing above it can be precise. Each entity is its own table with links into
50
+ the spine; attributes and evidence are fields on their owner. A single table with a `type` column
51
+ standing in for three entities collapses the distinctions every later query needs.
135
52
 
136
53
  **Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and
137
54
  look at fill rates — a field that is present and empty on 90% of rows will not support the screen
@@ -139,339 +56,101 @@ you are about to design.
139
56
 
140
57
  **That includes imagery.** If the entity has a likeness — a product, a property, a vehicle, a
141
58
  person — its picture is the strongest identifier a register row can carry, and an empty image field
142
- is a data gap to fill before you design around it (see `lotics docs composition`, §Character comes
143
- from the DATA). Fill it the same way you fill any other field: put the file on the record. Where the
144
- images do not exist yet, generate them out of band and `lotics file upload` + `update_records` them
145
- on — and keep one style across the whole set, because a catalogue whose shots disagree about
146
- lighting and background reads worse than one with no pictures at all.
59
+ is a data gap to fill before you design around it: `lotics file upload`, then `update_records`.
147
60
 
148
61
  **One fact, one column — and check before you add one.** Read the table's existing fields before
149
62
  adding any, because the fact is often already there in another shape: a place written as text
150
- beside a `select_record_link` to the place record, a status word beside the select that decides it,
151
- a total beside the formula that computes it. Two columns for one fact never stay equal — some
152
- writer sets only one of them, and nothing reports the divergence because both rows look correct
153
- alone. Prefer the link, the select, or the formula, and compose the text when you READ. Renaming or
154
- re-pointing the existing field beats adding a second one; a field is addressed by key, so a rename
155
- breaks nothing. Superseding a field means deleting it, not leaving it beside its replacement.
156
-
157
- **Match on ids and option keys, never on rendered text.** A reader that compares display strings
158
- treats "Acme" and "Acme Ltd" as different records, and a value spelled `Net 30` as different from
159
- the option labelled `Net 30 days` — so an import creates a duplicate every time it runs, silently,
160
- because each row looks right on its own. Resolve a name to its `rec_…` or `opt_…` once, at the boundary, and
161
- compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.
162
-
163
- **How the tables RELATE is `lotics docs data_model`** — one entity per table and the overlap probe
164
- that says when a split has broken, one vocabulary wherever values are copied between tables, a copy
165
- boundary that accounts for every source field, provenance as a link, a declared natural key, and why
166
- derived depth costs more than row count. Read it before designing a schema; those decisions outlive
167
- any one app, and most of them are unfixable once a second screen depends on the copy.
168
-
169
- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a real
170
- distinction, and a column whose values are all `1` may be the volume band nobody has needed yet.
171
- Read what a field MEANS before you remove it; "unused in this data" is not evidence it is wrong.
172
-
173
- ## 4 — Typed field access
63
+ beside a link to the place record, a status word beside the select that decides it, a total beside
64
+ the formula that computes it. Two columns for one fact never stay equal. Prefer the link, the
65
+ select, or the formula, and compose the text when you READ.
174
66
 
175
- ```
176
- lotics app codegen # no deploy, no version bump
177
- ```
178
-
179
- Regenerates `.lotics/`: the three `.d.ts` companions that type `useQuery` / `useWorkflow` /
180
- `useAgentRun`, and — when credentials resolve — `app_fields.ts`, exporting four maps keyed by
181
- display-name aliases: `F` (table → field → `"fld_…"`), `OPT` (table → select field → option →
182
- `"opt_…"`), `TBL` (table → `"tbl_…"`) and `GRP` (member group → `"grp_…"`).
183
-
184
- Address every id by alias, never by a pasted one — a table id and a member group included:
185
-
186
- ```tsx
187
- row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
188
- useCommentCounts({ table_id: TBL.SHIPMENT });
189
- useMembers({ group: GRP.sale });
190
- ```
67
+ **Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_…` or `opt_…`
68
+ once, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.
191
69
 
192
- An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
193
- and every call site on the old alias fails `tsc` until it names the new one. **Re-run after any
194
- schema change** — local typecheck is only honest if the generated ids are current, and you never
195
- deploy to refresh types.
70
+ **How the tables RELATE is `lotics docs data_model`** — read it before designing a schema; those
71
+ decisions outlive any one app.
196
72
 
197
- ## 5 — Named queries
198
-
199
- Author them as `kind: "project"` with a `filter`. A bare `from_table` over-exposes columns and
200
- degrades at scale. Scope per-user reads with `is_current_member` **inside the template** — a
201
- `member_id` passed from the client is an IDOR, since the caller chooses it.
202
-
203
- Name each projected column — `{ "source": "fld_…", "output": "total" }` — and the row is then read
204
- as `r.total`, with no field map on the read path; `lotics app create --from` emits exactly that.
205
- Write one `description` per alias too: it is the line a chat or MCP caller chooses between them by,
206
- and `lotics app check` exits 1 naming any alias that has none.
207
-
208
- Decode cells with the `row.*` helpers (`row.text`, `row.opt`, `readSelect`, `readLinks`), never by
209
- reaching into the raw shape: a select cell is `[{key,label}]`, and a hand-rolled reader silently
210
- returns the wrong half.
73
+ **Empty is not the same as redundant.** A field nothing fills may still be the only home for a real
74
+ distinction. Read what a field MEANS before you remove it.
211
75
 
212
- Iterating a query needs no deploy either:
76
+ ## 4 — An app stated in a model
213
77
 
214
78
  ```
215
- lotics app query set <alias> # pushes package.json#lotics.queries.<alias>, server-validated
79
+ lotics docs model # how to write model.json, with a worked example
80
+ lotics model apply model.json # check it, apply the tables, mint a version of every app
81
+ lotics model apply model.json --app orders # only the apps named; the tables are applied whole
82
+ lotics model pull -o model.json # the workspace's model, as the file apply reads
216
83
  ```
217
84
 
218
- Details: `lotics docs queries`.
219
-
220
- ## 6 — Workflows, the only way an app writes
221
-
222
- **A workflow binding is SOURCE, and it deploys with the app.** Its two halves are
223
- `package.json#lotics.workflows.<alias>` and `src/workflows/<alias>.ts`; `lotics app pull` writes
224
- both, and `lotics app deploy` pushes whatever differs from what it last saw live — through
225
- `set_app_workflow`, before the bundle ships, so a version that shipped always reproduces what runs.
226
- Nothing is bound by hand after a deploy, and nothing needs to be: an alias whose body and
227
- declaration are in the repo is an alias the next clone can build. Three consequences worth knowing
228
- before you write one:
229
-
230
- - **A push refuses when the live body moved past the copy you edited.** The baseline rides as a
231
- precondition, so two people editing one alias is a refusal naming it, never a silent clobber.
232
- - **An unchanged alias is not pushed.** An update is a diff, so a deploy that changes one screen
233
- does not re-push nine workflows.
234
- - **Deleting the declaration and the file does not unbind it.** The binding keeps serving, and
235
- keeps being published to chat and to MCP. `lotics app deploy --prune` unbinds what the project no
236
- longer names, and it is opt-in because an alias can be invoked from outside the bundle. An alias
237
- `lotics app regenerate` retired is the exception: the next deploy unbinds it unasked.
238
-
239
- A workflow body is a file you open and edit. The new-alias path is typed from the first line:
240
-
241
- 1. Declare it in `package.json#lotics.workflows.<alias>` — its `inputs`, and `outputs` only to
242
- narrow beyond what the body infers.
243
- 2. Write `src/workflows/<alias>.ts`.
244
- 3. `lotics app codegen` — the dts is generated **from your declaration**, so the body gets real
245
- types (`trigger.app_workflow.inputs.*`, the tool globals) before the alias is bound at all.
246
- 4. `lotics app workflow check` — runs the server's parse and typecheck locally, which catch the
247
- JS-subset rejections that read as ordinary TypeScript (an `async` function declaration, a typed
248
- parameter). A body clean there is then sent to the server, which answers what `set` would
249
- (resolve names → lint → structural validate, table reach, the draft and API guards) and writes
250
- nothing; its issues print at `file:line`. With no credentials or network it warns and exits on
251
- the local verdict.
252
- 5. `lotics app workflow set <alias>` — the server verifies again and saves.
253
-
254
- `outputs` are declared, else **derived** from `return({ status, message, data })` — so a workflow
255
- that returns an id must keep its `data` clause or the app receives nothing. When derived, `set`
256
- writes the schema back into the manifest and refreshes that alias's types in place.
257
-
258
- The alias's `description` rides along from the manifest. It is the one line an agent reads when
259
- choosing between your workflows, so write it rather than leaving the generated placeholder — and
260
- keep it inside 300 characters, the cap a push holds both a workflow's and a query's `description`
261
- to, because both ride the capability catalog into the agent's prompt on every run. `app check`
262
- names every alias over it and exits 1, and `workflow set` / `query set` refuse before sending
263
- anything, so a long line costs one edit rather than a failed push per alias.
264
-
265
- **Every workflow you declare is also the chat agent's write surface.** So the alias's *shape* is an
266
- agent-facing decision, not only a screen-facing one — take a list where one job covers many
267
- records; say in an optional input's own `description` what omitting it means, since the agent
268
- cannot see the default your body applies; make the write survive running twice on the same input;
269
- and gate anything irreversible with `wait_for_approval` inside the body. `lotics docs ai` and
270
- `lotics docs workflows` carry each of those.
271
-
272
- **Static green is not a run.** `check` and `set` prove parse, types, name resolution and lint —
273
- they evaluate nothing. Rehearse before the first live run:
274
-
275
- ```
276
- lotics run dry_run_workflow '{"trigger_type":"app_workflow","trigger_payload":{…},"live_reads":true}'
277
- ```
85
+ `model apply` checks the whole file first — every problem in one run, before anything is uploaded
86
+ or written. It then adopts or creates each table (an existing table of the same label is adopted
87
+ and given what it lacks; no stored value changes), writes the first rows only where every bound
88
+ table is empty, and mints one version per app, printing each app's id and the version minted
89
+ (`unchanged` when there was nothing to mint). Applying the same file again mints nothing.
278
90
 
279
- It walks the real step tree and returns the resolved plan plus expression and tool-input errors,
280
- dispatching no write. Pass the raw body — the file carries a wrapper that `set` strips and this
281
- tool does not. **`live_reads: true` matters whenever the body reads anything**: without it every
282
- read returns a stub, so a duplicate check finds no duplicate and every data-gated branch takes the
283
- empty path — green, and proving nothing about the branch you care about.
91
+ **The model changes after the app exists, and applying it again is how it lands.** Edit the file,
92
+ apply it. An act whose write the model cannot say names its own `workflow`; that workflow's body is
93
+ the live one, and `lotics run set_app_workflow` changes it.
284
94
 
285
- Then prove it end to end without a screen:
95
+ ## 5 — A custom-code app
286
96
 
287
97
  ```
288
- lotics run run_app_workflow '{"app_id":"app_…","alias":"…","inputs":{…}}'
289
- # exits non-zero on error, so it is assertable
98
+ lotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk
99
+ cd <dir>
100
+ # edit src/App.tsx — node_modules/@lotics/app-sdk/AGENTS.md is the reference
101
+ npm run typecheck && npm run lint && npm test
102
+ lotics app deploy -m "<what changed>" # build, upload, a new version live
290
103
  ```
291
104
 
292
- A workflow is also how an app **produces a document** — an invoice, a debit note, a shipping
293
- label. The `generate_*_from_template` tools fill a template you registered once, and the file
294
- comes back to the app on `result.files[]`, **not** through `return({ data })`; that channel
295
- carries values, never files. Registering the template is a CLI job, not an app one:
296
- `lotics docs document_templates`.
297
-
298
- Authoring rules for the body itself: `lotics docs workflows`.
105
+ What the app reads and writes is bound on the app, never in the project: `lotics run
106
+ set_app_query` binds a named query, `lotics run set_app_workflow` a workflow body, and each mints a
107
+ version. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build
108
+ and carries every binding forward unchanged.
299
109
 
300
- ## 7 — Screens
110
+ **Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column
111
+ (`{ "source": "fld_…", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with
112
+ `is_current_member` **inside the query** — a member id passed from the client is chosen by the
113
+ caller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.
301
114
 
302
- **A model-built app's screens are its `app.json`, and `@lotics/app-runtime` draws them.** The
303
- register, the record each row opens, its sections, the acts and the checks are all stated by
304
- the model, so a correction is a change to the MODEL followed by `lotics app regenerate` — or, where
305
- the ruling is about how every app draws, a change to the runtime or the kit that reaches every app
306
- on its next install.
115
+ **Workflows are the only way an app writes.** Every workflow bound to an app is also the chat
116
+ agent's write surface, so its shape is an agent-facing decision: take a list where one job covers
117
+ many records, say in an optional input's `description` what omitting it means, make the write
118
+ survive running twice, and gate anything irreversible with `wait_for_approval`.
307
119
 
308
- **Where the vocabulary has no word for what a record needs,** a `component` block names one of the
309
- app's own components (`lotics docs components`), and an act whose write `set` cannot say names its
310
- own `workflow`, which runs after the generated guards. A component the kit lacks is built into the
311
- kit, never hand-rolled in one app.
120
+ ## 6 — Prove it, without a screen
312
121
 
313
- **A hand-written app composes the kit itself.** Before any JSX, read `lotics docs ui` — the catalog
314
- and the composition grammar; the pieces the runtime draws a record with (`RecordFrame`,
315
- `RecordHeader`, `ChecksCallout`, `ActGroup`, `ActAsksDialog`) and the register's (`Table` with a `Row`
316
- subject, `FilterBand` with `StatusChips` and `GroupByMenu`) are there to compose rather than rebuild.
317
-
318
- Two rules for any code the app writes itself — a component or a hand-written screen:
319
-
320
- - **Never copy server data into `useState`.** Derive from `useQuery` / `useWorkflow` with
321
- `useMemo`; a copy goes stale the moment anything else writes.
322
- - **Design the loading, empty and error states.** Reserve their space so the layout does not jump.
323
-
324
- **The kit is a strong recommendation, not a requirement.** `@lotics/app-runtime/sdk` is data and
325
- RPC only — its peers are `react`, `react-dom` and `react-router` — so an app can be plain DOM React
326
- with your own CSS and still use every hook, deploy the same way, and run the same server-side.
327
- What the scaffold buys you is the part that is hard to get right by hand: a screen that looks
328
- deliberate, states that are already designed, and behaviour that matches the rest of the product.
329
- Building without it means owning all of that, so reach for it unless you have a specific reason not
330
- to — and if you do, the ONLY thing you give up is the components.
331
-
332
- ## 8 — Run it locally, and prove it
122
+ Static checks prove parse, types and names — they evaluate nothing. Rehearse a write first:
333
123
 
334
124
  ```
335
- lotics app dev
125
+ lotics run dry_run_workflow '{"trigger_type":"app_workflow","trigger_payload":{…},"live_reads":true}'
336
126
  ```
337
127
 
338
- Vite plus an RPC-forwarding server, in a sandboxed iframe matching production, with real data and
339
- auth and HMR. File flows work too — the dev server relays the bytes, so upload, preview and
340
- download are all exercisable locally.
341
-
342
- **Navigate with `waitUntil: "domcontentloaded"`.** A dev app never goes network-quiet — the Vite
343
- client and the app's own cross-origin frame each keep a connection open — so a driver that opts
344
- into `networkidle` waits out its timeout on an app that rendered fine, and the timeout reads as the
345
- app being broken. Any in-app path opens directly (`http://localhost:<port>/lo/rec_…`); the wrapper
346
- serves every path that is not one of its own `/_…` routes.
347
-
348
- **Drive it with a browser, in this order** — each step's failure means something different:
349
-
350
- 1. **Does it render at all?** A blank iframe is almost always a bundling problem, not your code.
351
- 2. **Read the console before the DOM.** A React error boundary shows a blank region; the reason is
352
- only in the console.
353
- 3. **Does the data arrive?** Check the query result before blaming the layout — an empty list and a
354
- broken list look identical.
355
- 4. **Then interact.** Click the real control rather than calling the handler: an element that is
356
- covered, disabled, or outside the viewport fails only under a real click.
357
-
358
- **Everything inside the app is a separate frame.** The app renders in a sandboxed iframe served
359
- from a different port, so it is cross-origin to the wrapper: parent-page JavaScript cannot reach
360
- `contentDocument`, and a selector run against the page finds nothing. Address it through the frame
361
- — `page.frameLocator("iframe")`, or the frame refs an accessibility snapshot gives you — and run
362
- any injected script in the frame's own context, or its `window` and coordinates are the wrong ones.
363
-
364
- Three kit anatomies then need driving deliberately rather than clicked: a pressable row's named
365
- button always intercepts pointer events, overlays portal to the top of the DOM, and custom pointer
366
- drag ignores `dragTo`. All three are by design and all three read as bugs — `lotics docs testing`.
367
-
368
- Read a failure by what it *cannot* be. A control that takes its value while its list never appears
369
- is not a wiring bug — the list is rendered somewhere the harness cannot see. Assert what the DOM
370
- actually carries, not what the source says it should.
128
+ It walks the real step tree and returns the resolved plan plus expression and tool-input errors,
129
+ dispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it
130
+ every read returns a stub, and every data-gated branch takes the empty path.
371
131
 
372
- ## 9 — Verify, then ship
132
+ Then run it end to end, on a throwaway record:
373
133
 
374
134
  ```
375
- npm run typecheck && npm run lint && npm test
376
- lotics app check --screens --shots shots/
377
- # every pre-flight a deploy runs, without building or shipping,
378
- # plus the portability gate a library publish applies, plus
379
- # every screen rendered at 1280 and 375, measured, and written
380
- # to shots/ as a PNG per screen and per record it opened
381
- lotics app deploy -m "<what changed + why>"
135
+ lotics run run_app_query '{"app_id":"app_…","alias":"…","params":{…}}'
136
+ lotics run run_app_workflow '{"app_id":"app_…","alias":"…","inputs":{…}}'
137
+ # exits non-zero when the run failed, so it is assertable
382
138
  ```
383
139
 
384
- **A green suite says nothing about how the screen LOOKS**, and that half starts with
385
- `--screens`: it renders the app over its real data (Chrome needed), walks every tab at both
386
- widths, and refuses what a review would; what it measures is in `lotics docs cli_reference`. It
387
- is a READ, enforced at the network rather than by what the walk presses — the CLI serves the app
388
- an allowlist of read ops and refuses everything else before the call leaves your machine, and a
389
- refused call heads the report and fails the run. What
390
- it cannot measure is in `lotics docs reviewing` — which is why `--shots <dir>` is on the same
391
- line: it photographs the frame each probe read, so LOOKING at the app is the same run rather than
392
- a dev server and a browser pass per screen. Open the 375 shots first. Both before the deploy, not
393
- as an audit someone schedules after a complaint.
394
-
395
- Every check above reads the SOURCE; none of them renders it. So the entire class of defect that
396
- lives in the pixels — wrong form for the subject, a treatment that contradicts what an element
397
- means, a surface that measures clean and still tells the reader nothing — passes all of them
398
- silently, and arrives later as "it looks bad", which is a report about a cause the reporter cannot
399
- name. That is what the review is for, and why it belongs here rather than in whoever remembers to
400
- ask for it.
401
-
402
- `-m` is optional; the deploy derives a message from what it pushed. Write one when the *why* is
403
- worth keeping.
404
-
405
- Then set the icon, the colour and the app's own `description` — the most-forgotten step, which is
406
- why `deploy` and `check` both warn while any of them is unset:
140
+ A workflow is also how an app **produces a document** — the `generate_*_from_template` tools fill a
141
+ template you registered once (`lotics docs document_templates`).
407
142
 
408
- ```
409
- lotics app rename "<the app's name>" --icon <lucide-name> --theme blue \
410
- --description "<what the app is for, and the standing job it does>"
411
- ```
143
+ ## 7 — Look at it, then name it
412
144
 
413
- The name is repeated because it is the same act: `app rename` is the one verb that sets an app's
414
- display metadata, and it also folds the new name into `package.json#name`, which nothing else
415
- does. Renaming to the name it already has changes nothing on the server.
145
+ Open the app and look at it at a desktop width and at a phone's — every check above reads the
146
+ definition, none of them the pixels. A version that looks wrong is one `lotics run rollback_app`
147
+ away from the one before.
416
148
 
417
- The `description` is not a label. It heads the capability listing the member's chat agent reads on
418
- **every** turn, so a standing process the app expects that agent to carry out belongs there and
419
- nowhere else — not in workspace instructions and not in a knowledge doc, both of which apply to
420
- every app at once (`lotics docs ai`).
149
+ Then set the icon, the colour and the app's own `description` through `lotics run update_app`. The
150
+ `description` heads the capability listing the member's chat agent reads on **every** turn, so a
151
+ standing process the app expects that agent to carry out belongs there and nowhere else.
421
152
 
422
153
  **A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key
423
- *Only selected ones* → that app, reaches every alias the app declares; no alias can be held back.
424
- So whatever outsiders may call is a SECOND app over the same tables: only the queries they may
425
- read, each projecting only the columns they may see, and only the workflows they may run — a form
426
- anyone can reach is workflows and no queries at all. It needs no screens, so scaffold it with
427
- `lotics app create "<name>" --api` (§ 1): push its aliases with `app query set` /
428
- `app workflow set`, share it or give a key to it, and `app api publish` once a caller depends on
429
- it — there is no deploy anywhere in that loop. The desk the team works in stays a separate,
430
- private app.
431
-
432
- Finally, update the app's own `README.md` on any model or behaviour change and redeploy, so the
433
- brief travels with the app rather than living in whoever built it.
434
-
435
- ---
436
-
437
- ## The inner loop
438
-
439
- Once scaffolded, everything below happens locally:
440
-
441
- ```
442
- edit src/workflows/<alias>.ts # or a component, or a query
443
- lotics app preview model.json#<app> --shots shots/ # when the MODEL moved: see it before a table does
444
- lotics app regenerate --dry-run # then: what the new generation would do to this app
445
- lotics app codegen # after any schema change
446
- npm run typecheck # honest, because codegen is current
447
- lotics run run_app_workflow '{"app_id":…,"alias":…}' # prove the mutation path
448
- lotics app dev # prove the screen
449
- lotics app check --screens --shots shots/ # measure it AND photograph it, before anyone looks
450
- …
451
- lotics app workflow set <alias> # push the body; the server verifies
452
- lotics app deploy -m "…" # pushes pending bindings, then ships
453
- ```
454
-
455
- **An app lists `@lotics/app-runtime` and nothing else of ours** — its SDK is every app's data
456
- layer, and it depends on the kit it draws with. Where the app's own code imports the kit, it lists
457
- `@lotics/ui` at **the runtime's range**, never its own; `lotics app kit` keeps the two in step.
458
- **Proving a change to the runtime before it is published** is
459
- `lotics app kit <path-to-packages/app-runtime>`: it builds the runtime, packs it into this app's
460
- `.lotics/kit/`, installs it by file specifier and hashes one built file on both sides to prove the
461
- install took — a repack under the same name is otherwise served from the lockfile's first tarball,
462
- and every other signal reads as success. `app check` then says the app depends on a build that
463
- exists on one machine, and `app deploy` refuses it (the tarball is not in the source archive, so a
464
- clone could not install) unless you pass `--allow-local-kit`. `lotics app kit --published` puts
465
- back the range the app listed before the first checkout — an app with none recorded moves to the
466
- registry version — and migrates an app still on `@lotics/app-sdk`, the package the SDK was before
467
- it moved into the runtime (`lotics docs migration`). A kit change reaches an app through the runtime that depends on
468
- it; `LOTICS_UI_SRC` links the kit's source into `lotics app dev`.
469
-
470
- A deploy pushes any workflow body, agent prose or query that is ahead of the app **before** it
471
- ships the bundle, so a forgotten `set` cannot ship a bundle typed against a binding that does not
472
- exist. It asks the server about every query and body first and pushes none when one would be
473
- refused, so a release never stops half way with the app reading a mix of new and old bindings. It then regenerates the `.lotics` types and runs `npm run typecheck` before building — Vite
474
- strips types, so that run is what makes them a gate. `lotics app check` reports the same set, holds every
475
- query to the server's own gate, and runs the same typecheck, without pushing.
476
-
477
- Keep the CLI current: an old one silently drops manifest fields it does not model.
154
+ access to it, reaches every alias the app declares; no alias can be held back. So whatever
155
+ outsiders may call is a SECOND app over the same tables: only the queries they may read and the
156
+ workflows they may run. The desk the team works in stays a separate, private app.