@lotics/cli 0.263.0 → 0.266.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/AGENTS.md +37 -57
- package/README.md +50 -206
- package/dist/src/cli.js +31155 -84614
- package/dist/src/cli.js.LEGAL.txt +0 -16
- package/dist/src/client.d.ts +18 -1231
- package/dist/src/client.js +14 -727
- package/dist/src/invocation.d.ts +1 -1
- package/docs/building_an_app.md +91 -409
- package/docs/cli_reference.md +13 -46
- package/docs/document_templates.md +7 -17
- package/docs/knowledge_docs.md +0 -8
- package/docs/migration.md +70 -97
- package/package.json +1 -1
- package/dist/probe_page.js +0 -2381
- package/dist/render_page.js +0 -67559
- package/dist/render_page.js.LEGAL.txt +0 -14
package/dist/src/invocation.d.ts
CHANGED
|
@@ -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 — `
|
|
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`,
|
package/docs/building_an_app.md
CHANGED
|
@@ -1,113 +1,31 @@
|
|
|
1
1
|
# Building an app, end to end
|
|
2
2
|
|
|
3
|
-
The other references here describe **contracts** — what a
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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,104 @@ 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
|
|
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
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
## 4 — An app stated in a model
|
|
213
77
|
|
|
214
78
|
```
|
|
215
|
-
lotics
|
|
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 apply model.json --plan # what the apply would change, writing nothing
|
|
83
|
+
lotics model pull -o model.json # the workspace's model, as the file apply reads
|
|
216
84
|
```
|
|
217
85
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
```
|
|
86
|
+
`model apply` checks the whole file first — every problem in one run, before anything is uploaded
|
|
87
|
+
or written. It then adopts or creates each table (the table this workspace bound it to, else one of the same
|
|
88
|
+
label, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound
|
|
89
|
+
table is empty, and mints one version per app, printing each app's id, the version minted
|
|
90
|
+
(`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.
|
|
91
|
+
A table change is not undone by rolling an app back, so `--plan` first says what the apply would
|
|
92
|
+
create or change in the tables and which apps it would create, update or refuse — writing nothing.
|
|
278
93
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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.
|
|
94
|
+
**The model changes after the app exists, and applying it again is how it lands.** Edit the file,
|
|
95
|
+
apply it. An act whose write the model cannot say names its own `workflow`; that workflow's body is
|
|
96
|
+
the live one, and `lotics run set_app_workflow` changes it.
|
|
284
97
|
|
|
285
|
-
|
|
98
|
+
## 5 — A custom-code app
|
|
286
99
|
|
|
287
100
|
```
|
|
288
|
-
lotics
|
|
289
|
-
|
|
101
|
+
lotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk
|
|
102
|
+
cd <dir>
|
|
103
|
+
# edit src/App.tsx — node_modules/@lotics/app-sdk/AGENTS.md is the reference
|
|
104
|
+
npm run typecheck && npm run lint && npm test
|
|
105
|
+
lotics app deploy -m "<what changed>" # build, upload, a new version live
|
|
290
106
|
```
|
|
291
107
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
carries
|
|
296
|
-
`lotics docs document_templates`.
|
|
297
|
-
|
|
298
|
-
Authoring rules for the body itself: `lotics docs workflows`.
|
|
108
|
+
What the app reads and writes is bound on the app, never in the project: `lotics run
|
|
109
|
+
set_app_query` binds a named query, `lotics run set_app_workflow` a workflow body, and each mints a
|
|
110
|
+
version. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build
|
|
111
|
+
and carries every binding forward unchanged.
|
|
299
112
|
|
|
300
|
-
|
|
113
|
+
**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column
|
|
114
|
+
(`{ "source": "fld_…", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with
|
|
115
|
+
`is_current_member` **inside the query** — a member id passed from the client is chosen by the
|
|
116
|
+
caller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.
|
|
301
117
|
|
|
302
|
-
**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
on its next install.
|
|
118
|
+
**Workflows are the only way an app writes.** Every workflow bound to an app is also the chat
|
|
119
|
+
agent's write surface, so its shape is an agent-facing decision: take a list where one job covers
|
|
120
|
+
many records, say in an optional input's `description` what omitting it means, make the write
|
|
121
|
+
survive running twice, and gate anything irreversible with `wait_for_approval`.
|
|
307
122
|
|
|
308
|
-
|
|
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.
|
|
123
|
+
## 6 — Prove it, without a screen
|
|
312
124
|
|
|
313
|
-
|
|
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
|
|
125
|
+
Static checks prove parse, types and names — they evaluate nothing. Rehearse a write first:
|
|
333
126
|
|
|
334
127
|
```
|
|
335
|
-
lotics
|
|
128
|
+
lotics run dry_run_workflow '{"trigger_type":"app_workflow","trigger_payload":{…},"live_reads":true}'
|
|
336
129
|
```
|
|
337
130
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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.
|
|
131
|
+
It walks the real step tree and returns the resolved plan plus expression and tool-input errors,
|
|
132
|
+
dispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it
|
|
133
|
+
every read returns a stub, and every data-gated branch takes the empty path.
|
|
371
134
|
|
|
372
|
-
|
|
135
|
+
Then run it end to end, on a throwaway record:
|
|
373
136
|
|
|
374
137
|
```
|
|
375
|
-
|
|
376
|
-
lotics
|
|
377
|
-
|
|
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>"
|
|
138
|
+
lotics run run_app_query '{"app_id":"app_…","alias":"…","params":{…}}'
|
|
139
|
+
lotics run run_app_workflow '{"app_id":"app_…","alias":"…","inputs":{…}}'
|
|
140
|
+
# exits non-zero when the run failed, so it is assertable
|
|
382
141
|
```
|
|
383
142
|
|
|
384
|
-
|
|
385
|
-
|
|
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:
|
|
143
|
+
A workflow is also how an app **produces a document** — the `generate_*_from_template` tools fill a
|
|
144
|
+
template you registered once (`lotics docs document_templates`).
|
|
407
145
|
|
|
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
|
-
```
|
|
146
|
+
## 7 — Look at it, then name it
|
|
412
147
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
148
|
+
Open the app and look at it at a desktop width and at a phone's — every check above reads the
|
|
149
|
+
definition, none of them the pixels. A version that looks wrong is one `lotics run rollback_app`
|
|
150
|
+
away from the one before.
|
|
416
151
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
every app at once (`lotics docs ai`).
|
|
152
|
+
Then set the icon, the colour and the app's own `description` through `lotics run update_app`. The
|
|
153
|
+
`description` heads the capability listing the member's chat agent reads on **every** turn, so a
|
|
154
|
+
standing process the app expects that agent to carry out belongs there and nowhere else.
|
|
421
155
|
|
|
422
156
|
**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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.
|
|
157
|
+
access to it, reaches every alias the app declares; no alias can be held back. So whatever
|
|
158
|
+
outsiders may call is a SECOND app over the same tables: only the queries they may read and the
|
|
159
|
+
workflows they may run. The desk the team works in stays a separate, private app.
|