@lotics/cli 0.247.0 → 0.251.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/dist/probe_page.js +9 -43
- package/dist/src/cli.js +7359 -366
- package/docs/building_an_app.md +3 -3
- package/docs/cli_reference.md +4 -4
- package/docs/migration.md +20 -5
- package/package.json +1 -1
package/docs/building_an_app.md
CHANGED
|
@@ -33,7 +33,7 @@ writes nothing and says which apps it would create and which it would regenerate
|
|
|
33
33
|
**An app is stated, not drawn.** In `model.json`, `records` says how a row of each entity is
|
|
34
34
|
recognised — its title, the line under it, its picture, its status, its figure, its deadlines — and
|
|
35
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
|
|
36
|
+
columns and filters, the record's sections in the order its work reaches them, the acts and the checks that guard them.
|
|
37
37
|
That is the whole vocabulary (`lotics docs model/apps`). How each piece looks is the runtime's, one
|
|
38
38
|
way per concept, and no key changes it. `lotics scaffold check` prints each app as its reader will
|
|
39
39
|
see it, with every value a default supplied marked `(default)` — review the app there, before
|
|
@@ -282,7 +282,7 @@ Authoring rules for the body itself: `lotics docs workflows`.
|
|
|
282
282
|
## 7 — Screens
|
|
283
283
|
|
|
284
284
|
**A model-built app's screens are its `app.json`, and `@lotics/app-runtime` draws them.** The
|
|
285
|
-
register, the record each row opens, its
|
|
285
|
+
register, the record each row opens, its sections, the acts and the checks are all stated by
|
|
286
286
|
the model, so a correction is a change to the MODEL followed by `lotics app regenerate` — or, where
|
|
287
287
|
the ruling is about how every app draws, a change to the runtime or the kit that reaches every app
|
|
288
288
|
on its next install.
|
|
@@ -294,7 +294,7 @@ kit, never hand-rolled in one app.
|
|
|
294
294
|
|
|
295
295
|
**A hand-written app composes the kit itself.** Before any JSX, read `lotics docs ui` — the catalog
|
|
296
296
|
and the composition grammar; the pieces the runtime draws a record with (`RecordFrame`,
|
|
297
|
-
`RecordHeader`, `ChecksCallout`, `
|
|
297
|
+
`RecordHeader`, `ChecksCallout`, `ActGroup`, `ActAsksDialog`) and the register's (`Table` with a `Row`
|
|
298
298
|
subject, `FilterBand` with `StatusChips` and `GroupByMenu`) are there to compose rather than rebuild.
|
|
299
299
|
|
|
300
300
|
Two rules for any code the app writes itself — a component or a hand-written screen:
|
package/docs/cli_reference.md
CHANGED
|
@@ -44,7 +44,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
44
44
|
| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` — one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |
|
|
45
45
|
| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** — the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches — while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as "everything". |
|
|
46
46
|
| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
|
|
47
|
-
| `lotics app create <name> [path] [--api]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1. **`--api`** scaffolds an app with NO screens instead: the app's declared queries and workflows are the whole of what it offers, called over HTTP by the customer's own site, server or agent (`POST /v1/apps/{app_id}/queries/{alias}` and `/workflows/{alias}/execute`). It writes `package.json` (the `lotics` block, `typescript` as its only devDependency, `typecheck` as its only script), `tsconfig.json`, the brief in `src/workflows/`, a CI workflow and the two READMEs — no `index.html`, no `src/App.tsx`, no `vite.config.ts`, no kit. Nothing is built and nothing is deployed, so `current_version_id` stays null: `lotics app query set` and `lotics app workflow set` publish each declaration on their own, and `lotics app api publish` snapshots what they promise. The npm registry is not consulted (there is no `@lotics/app-runtime` range to resolve), `npm install` still runs for `typescript` (which `app workflow check` loads), and, for want of a bundle, `app dev` and `app check --screens` (no `vite.config.*`) and `app deploy` (no `build` script) are refused on such a project in one sentence, before a binding is pushed; `app pull` says its project lives in version control, where `app codegen` rebuilds `.lotics/`. `--api` beside `--from` is refused before anything is written — a model's app describes screens. The generated `package.json#name` is the app name FOLDED to ASCII (`Đơn hàng` → `don-hang`), never stripped of it — dropping the marks would treat each accented vowel as a separator and can slug a name away to nothing. The app's display name is unaffected; this is the npm field only. **`--from <model.json>#<app>` builds the app a MODEL states, and it is JSON.** The file is checked as `scaffold check` checks it, the named app (or the only one) is taken, and every entity the app lists, opens or picks is found as the live table this WORKSPACE remembers the alias became — and BY LABEL only for an alias nothing has bound, which is a new entity or a workspace nothing has applied this model to — so a table or a column relabelled since the apply is still the same one, while a bound id the workspace no longer serves is refused by name rather than re-bound to its namesake. A table the workspace lacks is refused first, naming every missing one and `scaffold apply`; then a field a table lacks, a label two tables or two fields share, or a field whose live type is not the model's — all before anything is created. **The app is BOUND to the model's app alias** (`PUT /v1/workspaces/model/binding/apps`) the moment its row exists; an alias already bound to another app is refused before the row is made — `lotics app pull <app_id>` works on that one, `lotics scaffold unbind app <alias>` first makes a new one the model's — and a bound app the workspace no longer holds is refused the same way, never replaced in silence. **What it writes is `app.json`** (version
|
|
47
|
+
| `lotics app create <name> [path] [--api]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1. **`--api`** scaffolds an app with NO screens instead: the app's declared queries and workflows are the whole of what it offers, called over HTTP by the customer's own site, server or agent (`POST /v1/apps/{app_id}/queries/{alias}` and `/workflows/{alias}/execute`). It writes `package.json` (the `lotics` block, `typescript` as its only devDependency, `typecheck` as its only script), `tsconfig.json`, the brief in `src/workflows/`, a CI workflow and the two READMEs — no `index.html`, no `src/App.tsx`, no `vite.config.ts`, no kit. Nothing is built and nothing is deployed, so `current_version_id` stays null: `lotics app query set` and `lotics app workflow set` publish each declaration on their own, and `lotics app api publish` snapshots what they promise. The npm registry is not consulted (there is no `@lotics/app-runtime` range to resolve), `npm install` still runs for `typescript` (which `app workflow check` loads), and, for want of a bundle, `app dev` and `app check --screens` (no `vite.config.*`) and `app deploy` (no `build` script) are refused on such a project in one sentence, before a binding is pushed; `app pull` says its project lives in version control, where `app codegen` rebuilds `.lotics/`. `--api` beside `--from` is refused before anything is written — a model's app describes screens. The generated `package.json#name` is the app name FOLDED to ASCII (`Đơn hàng` → `don-hang`), never stripped of it — dropping the marks would treat each accented vowel as a separator and can slug a name away to nothing. The app's display name is unaffected; this is the npm field only. **`--from <model.json>#<app>` builds the app a MODEL states, and it is JSON.** The file is checked as `scaffold check` checks it, the named app (or the only one) is taken, and every entity the app lists, opens or picks is found as the live table this WORKSPACE remembers the alias became — and BY LABEL only for an alias nothing has bound, which is a new entity or a workspace nothing has applied this model to — so a table or a column relabelled since the apply is still the same one, while a bound id the workspace no longer serves is refused by name rather than re-bound to its namesake. A table the workspace lacks is refused first, naming every missing one and `scaffold apply`; then a field a table lacks, a label two tables or two fields share, or a field whose live type is not the model's — all before anything is created. **The app is BOUND to the model's app alias** (`PUT /v1/workspaces/model/binding/apps`) the moment its row exists; an alias already bound to another app is refused before the row is made — `lotics app pull <app_id>` works on that one, `lotics scaffold unbind app <alias>` first makes a new one the model's — and a bound app the workspace no longer holds is refused the same way, never replaced in silence. **What it writes is `app.json`** (version 3): the register — its columns, filters, order, layout and add — the record its rows open — door, sections (each its stage, fields, blocks and the acts at its foot), comment thread — the acts and the checks, and every entity those name with its fields, its `records` statement and the reads and writes over it. Every id in it is live (`tbl_`, `fld_`, `opt_`). Beside it: `src/main.tsx`, which mounts `@lotics/app-runtime` over the spec (and imports `@lotics/app-runtime/sheets` where the register exports), `src/components/index.ts` seeded empty (a `component` block names one there), one `src/workflows/<alias>.ts` per write, and the README. There is no screen source: **the runtime draws the spec**, so a kit correction reaches the app with its next `npm install` rather than with a regeneration. The manifest declares the reads — `<entity>_list` (param `search`), `<entity>_record` (param `<entity>_id`), `<child>_by_<via>` (the parent's id) for each rows or timeline block — `_<field>_<option>` after it for each option a rows block's `where` narrows to — and `<entity>_<field>_pick` where `write_rules` narrow what a link may point at — each carrying the entity's `read_scope` unless the app states `reads: "shared"`; and the writes — `create_<entity>`, `update_<entity>` (only the changed fields; a many-link or files field as `<alias>_added`/`<alias>_removed`), `remove_<entity>`, and `act_<alias>` per act. **Every write re-checks its gates on the server**: required fields, `min`/`max`, `options_where`, `read_scope`, a `natural_key` match reused rather than duplicated, `default_from`, an act's `when` and `requires`, the checks blocking it, and the status `history` row a move appends. An act that names its own `workflow` gets that file with a generated guard region at its top, between `// <lotics:guards>` and `// </lotics:guards>`; the code below is yours. **And it declares what the app WRITES** — `package.json#lotics.writes`, table alias → the field aliases its workflows change — **and what it DELETES**, `package.json#lotics.deletes`. Seeded once: from then on the declaration is the app's, and `app check` refuses a body that writes or deletes outside it. |
|
|
48
48
|
| `lotics app regenerate [--from <model.json>#<app>] [--dry-run] [--bind-new] [--screens]` | **Recompile an app that already exists from its model, and rewrite what the generator owns.** Run inside the app directory. The model is whatever `package.json#lotics.plan` remembers — written there by `app create --from` and by this command — unless `--from` names another, which is then remembered in its place; no model and no flag is a refusal, as is a directory with no `lotics.app_id`, a model that does not check (every finding printed), and a model that names no such app. It resolves against the LIVE workspace through the same function `app create --from` resolves with, so the refusals and the output are that command's. **Files. The generator owns what it emits.** `app.json` is compiled from the model and rewritten whole — there is nothing in a spec to merge — the entry beside it never varies, and a generated workflow body is the generator's too: what it replaced goes into `.lotics/regenerate-dropped.patch`, file by file, rather than into a three-way merge, because a conflict marker inside a body is a file the server has to parse. A body is compared as its BODY, since `app codegen` wraps every one on disk in the header and `__workflow` envelope the server verifies against, so comparing bytes would read that wrapper as your edit on every app. **Two things are the author's.** `src/components/` is seeded where it is absent, named back where you have changed it, and never rewritten. An act's own `workflow` keeps your code: only its guard region, between `// <lotics:guards>` and `// </lotics:guards>`, is rewritten, and a body carrying those markers is never deleted as retired. A body whose alias the generator has retired is deleted. **Manifest.** `.lotics/generated/manifest.json` records the alias sets each generation declared, and that is what the reconciliation reads: queries and workflow declarations the generator emits replace their counterparts (each `workflow_id` carried over — the server minted it), aliases the last generation emitted and this one does not are removed and listed, and aliases you added by hand are kept. `lotics.writes` gains the generator's columns and `lotics.deletes` its tables, and each loses an entry on two facts only, each named in the summary: a column the live table no longer carries, and one nothing in the app WRITES any more (a delete retires on the second rule alone — it names no column for a rename to strand) (the bodies it holds after the run, plus this generation's own declaration — a column a screen only READS is not a write, and `app check` can never find one, because a declaration covering more than the bodies write refuses nothing). A body the subset refuses, or one calling a tool this CLI's registry does not know, suspends that second rule for the run: nothing is dropped on a guess. **It pushes NOTHING.** What the live app RUNS changes at `lotics app deploy` and nowhere else, because the bundle production serves was built against the bindings it has: a regeneration that replaced a live workflow body or a live picker query left a deployed create dialog posting inputs its workflow no longer declared, and a picker answering nothing. Instead the summary NAMES what a deploy will do to the live app — `add` for an alias it does not have, `change` for one this tree is ahead of, `remove` for one the generator retired that this checkout bound and the app still serves (the next deploy unbinds it; the retirement is recorded in `.lotics/generated/manifest.json` until one does) — read through the same detector `app check` reports from and `app deploy` pushes from, so the preview cannot disagree with the deploy. **`--bind-new`** is the one live effect left: it binds the aliases the app does not have YET and refuses, naming them, to touch one that already exists, because `lotics app dev` forwards its queries to production and a new alias cannot be exercised until something binds it, while adding one the deployed bundle never calls cannot change what that bundle does. **The runtime follows the spec.** The spec is written for the `@lotics/app-runtime` line this CLI generates for, so the directory's `@lotics/app-runtime` moves to the runtime's latest line and a `@lotics/ui` the app lists to the range that runtime depends on, in one install, and the summary names each move; a range already on its line is left as written, an app still listing `@lotics/app-sdk` is migrated as `lotics app kit --published` migrates it, and a runtime installed from a checkout (`lotics app kit`) is left and named. The model app's icon and colour are compared with the live app's and a difference is named — `workspace build` sets it, since this command changes nothing live. Then `app codegen` runs, the summary prints — files written / kept / deleted, what a deploy will change, what was bound ahead, and `lotics app deploy` as the next command — and the fast `app check` runs (`--screens` passes through), with the bindings this run deliberately left ahead reported by the summary rather than failed by the check. `--dry-run` decides the whole run, prints it, and writes nothing: not a file, not a binding; it cannot be combined with `--bind-new`. |
|
|
49
49
|
| `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, install dependencies (`npm ci --ignore-scripts` when a lockfile is present, else `npm install --ignore-scripts`), stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir. **A pull never overwrites a file that differs from what it is about to write** — it writes only what is ABSENT or already identical, keeps the rest, and reports which files it kept plus the commands that close the gap. The same rule covers `src/workflows/<alias>.ts` and `src/agents/<alias>.md`, so an unpushed body or prompt survives too. Those two are written from the LIVE App row (`apps.workflows` / `apps.agents`), which owns them, and the archive's own copy of them is deliberately SKIPPED on extract: a deploy tars the whole source directory, so the tarball holds a deploy-time snapshot that is stale for anything authored since. The comparison is against the app's own content, not git, so it holds for a project that was never a repo. `--force` takes the app's copy and DISCARDS local edits; there is no other way to lose them. **For a KEPT workflow body or agent prompt the pull records the server's fingerprint only when the server has not moved** — that token is `set_app_workflow`/`set_app_agent`'s lost-update precondition, so recording one for text the author has not seen would clear the next push's refusal by disarming the guard, and silently overwrite whoever edited it. When the live text HAS moved, the pull writes it beside the checkout (`.lotics/agents/<alias>.live.md`, `.lotics/workflows/<alias>.live.ts`), names it, and leaves the token stale: the next deploy is refused, which is correct, and the text to merge is now on disk. A file whose prose/body already reads back AS the live text is never "kept" at all — the baselines are healed from it, so a checkout whose prose was pushed out of band (chat, `lotics run set_app_agent`) converges instead of latching. **A pull also REPORTS the files it restored** when refreshing a tree that already claimed a version: a pull mirrors the last DEPLOYED source, so a file deleted locally comes back until the deletion itself ships, and saying so is the only honest fix — nothing can read a deletion off the disk. **When a pull ACROSS versions keeps files, the manifest is left on the OLDER of the two versions** — the tree is then part one and part the other, and claiming the newer would make `deploy`'s `prev_version_id` check pass and ship a half-and-half bundle. Older, not "the one it had": `--from-version` pulls a deliberately old revision, so the version it had is the NEWER side, and holding that would match what the server serves and let the old source ship. Either way a deploy from that tree is refused until you reconcile the listed files by hand and pull again, or take the app's copy with `--force`. The report says which version each side is on, because a kept file is your unshipped work when the project was already current and merely the OLD version when it was behind — and nothing in a byte comparison can tell those apart. **`--from-version <apv_…>`** pulls an OLDER revision instead of the current one (`lotics app versions` lists the ids) — point it at a NEW path to read a previous revision without disturbing the project you are in. The manifest records the version actually written, never the live pointer, so a deploy from that checkout is refused by the version guard rather than shipping old source over newer. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed, AND — AFTER `npm install`, so `node_modules/@lotics/ui` actually exists to read — `.lotics/tsconfig.link.json`'s peer pins and, for a kit old enough to ship one, its `react-native` augmentation (a kit that ships none has the previously-written copy deleted); a pulled project's own `tsc` used to fail until `app codegen` was run by hand, because nothing had regenerated either one after install populated node_modules. AND the runtime `.lotics/app_fields.ts` (the same generation `app codegen` runs, off the app row already fetched). That one is not optional: `app deploy` tars source with `--exclude=.lotics`, so no archive can carry it, and a pulled project whose `src/` imports `F`/`OPT` would fail to build with `Could not resolve "../../.lotics/app_fields"` until `app codegen` was run by hand. Skipped under `--view-as` (the schema is read as that member and silently drops tables they cannot see — a narrowed `F` map compiles and then throws at runtime, worse than the missing module). A schema fetch failure is non-fatal and names the right recovery for what is on disk: an existing file is kept, an ABSENT one warns about the build error and points at `app codegen`. Pull GENERATES but never RECONCILES `.lotics/` — deleting a companion whose alias the manifest no longer declares is `app codegen`'s alone, since pull's authority is the server's alias set and a declared-but-not-yet-`set` alias is supported. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file. A pull writes it from live UNLESS the local file holds unpushed work, in which case it is kept and the live text is parked beside the checkout — the same rule the rest of this row describes. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_tier`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
|
|
50
50
|
| `lotics app deploy [--prune] [--prune-invoked <alias>] [-m <message>] [--acknowledge-breaking-api]` | `-m` is OPTIONAL — omitted, the deploy derives the version message from what it pushed. Runs the app's `npm run typecheck` and `npm run build`, tars source + dist, POST /v1/apps/{id}/versions multipart. **One command ships everything.** Before the bundle moves it pushes every binding the project has ahead of the app — an edited workflow body or declaration, edited agent prose, a changed query — through `set_app_query`, then `set_app_workflow`, then `set_app_agent`, and FAILS the release if any push is refused. That order is required: an agent declares the query and workflow aliases it may call, so pushing it before its own new query is refused. A workflow's `description` rides that push and is compared against the recorded baseline, not the live app — it lives on the workflow ROW, which `getApp` does not carry. Editing `lotics.agents.<alias>.inputs`/`outputs` is pushed the same way, and only those two fields (`set_app_agent` merges, so anything the manifest does not model is left untouched). The deploy never AUTHORS a binding itself, and each push carries the fingerprint the project last saw live (`lotics.synced`), so a stale checkout is refused rather than overwriting another author's edit. It regenerates the `.lotics/*.d.ts` companions and `.lotics/app_fields.ts` before building — the build INLINES the latter — and then typechecks against them; a `package.json` with no `typecheck` script is warned about, never passed in silence. `lotics app check` reports the same set without pushing; neither has a `--strict`. The aliases the version RECORDS as called — the set `remove_app_workflow` / `remove_app_query` / `remove_app_agent` consult to refuse unbinding one the served version still reaches — are read by the SERVER out of the uploaded source archive, never reported by the client that is also what unbinds. **After the ship it unbinds what the GENERATOR retired, unasked**: an alias `.lotics/generated/manifest.json` records as retired by `app regenerate` that this checkout bound and no longer declares — the same transition rule as below, so an alias another checkout bound, or one declared again by hand, is never touched. Every unbind — this one and `--prune`'s — carries the fingerprint the project last saw live, so an alias rewritten since (by chat, by another checkout) is refused and left bound, and a retired one is then dropped from the record, named. Beside that fingerprint alone the invocation guard is lifted, since the recorded runs are then the generated app's own calls to a body nobody rewrote; one that will not unbind for any other reason stays recorded and the next deploy retries it. **Beyond those it reports two things and removes nothing.** Aliases the source CALLS that nothing bound. And bindings this project has RETIRED, which is two transitions, each with its own evidence: an alias the previous bundle called and this one does not (`package.json#lotics.bundle_calls`, recorded by each deploy), and an alias still bound live that this checkout holds a `lotics.synced.<kind>.<alias>` baseline for and no longer DECLARES. Neither piece of evidence present is an alias this checkout has never seen — bound by chat, by another operator, or after this tree was pulled — which is not a removal and is never a prune target. With no `bundle_calls` the first transition reports nothing; that deploy records it and the next can compare. The `bundle_calls` baseline is STICKY: it advances only once the call-site half is settled, so the `--prune` a warning names still finds the transition on a later run. **`--prune` unbinds them, and only when passed.** It runs AFTER the version is live, because the removal tools refuse an alias the SERVED version still declares. When the source computes an alias at run time, the call-site half is left in place with a warning (the scan cannot tell which binding that call reaches); the declaration-removed half is unbound anyway, since deleting a declaration here states the removal outright. A removal DELETES the local declaration too — `package.json#lotics.<kind>.<alias>` and its `synced` baseline — or the next plain deploy would push it straight back; what it deleted is written to `.lotics/pruned/<kind>/<alias>.json` and the ✓ names that file plus the `set` verb that re-binds it. (These trees are never committed, so `lotics app pull --from-version <apv_…>` is the only other route back.) The generated companions are then regenerated from the narrowed manifest; a table named ONLY by a pruned query leaves `F`/`OPT`, which is reported — a workflow that still writes it keeps it, since the codegen set is the queries' tables plus every bound workflow's own `table_ids`. A binding that will not unbind is reported and never fails the release, and neither does a local write that fails: the version is already live, and the report names which aliases were unbound server-side. The server refuses to unbind a WORKFLOW this workspace has actually run — a recorded execution means a caller the source cannot name — printed as `✗ could not unbind …` with the date it last ran. **`--prune-invoked <alias>` lifts that guard for the alias you name** (repeatable, comma-separated; needs `--prune`, and is refused as a no-op without it), keeping the prune's report, undo file and manifest cleanup that a raw `lotics run remove_app_workflow` loses. Finally it refreshes `.lotics/workflows/<alias>.globals.d.ts` for any alias whose `// lotics:declaration` stamp says this deploy moved its declaration — from the manifest, re-wrapping the SAME on-disk body, so local edits survive. Non-fatal: the release has shipped, and stale types never fail it. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version; without it such a write is refused and every breaking change is named (see `app api`). It rides every write the release makes — the bindings pushed ahead of the bundle, the version itself, and a `--prune`'s unbinds — because the answer is about the RELEASE. |
|
|
@@ -54,7 +54,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
54
54
|
| `lotics app codegen [path]` | Regenerate `.lotics/*` from the manifest + workspace schema **without a deploy**. The three `.d.ts` companions (`app_{workflows,queries,agents}.d.ts`) are always rewritten (synchronous, no network). When credentials resolve, also rewrites the **runtime** `.lotics/app_fields.ts` — a real `.ts` exporting `F` (table→field→`"fld_…"`), `OPT` (table→select-field→option→`"opt_…"`), `TBL` (table→`"tbl_…"`) and `GRP` (member group→`"grp_…"`) keyed by display-name aliases, for every table the app's queries READ plus every table its bound workflows WRITE (each binding's recorded `table_ids`, so a table no screen reads is still addressable by alias and a rename fails `tsc` instead of the body), plus every member group in the organization — which one a screen names is not knowable from the manifest, and a `GRP` narrowed by a failed read is a map that is wrong rather than absent, so a failure writes nothing. **There is one form, and that is what makes a starter's source portable**: the keys are slugified DISPLAY NAMES and a starter carries its labels verbatim, so running codegen in a copy's own workspace emits the same keys pointing at that workspace's ids — no binding fetched at load, no prebuilt bundle to keep in step. Beside it, `.lotics/app_siblings.ts` exports `APP` — every app the workspace's model declares, by the model's alias for it (`APP.<alias>` → `"app_…"`), read off the workspace's model binding — so a hand-written app opens a sibling as `openApp(APP.<alias>, route)` and holds no `app_` id; an app archived here is absent, so a call naming it fails `tsc`. The binding is an admin's read: a key that cannot read it keeps the file on disk and says so. Also refreshes each bound workflow's `.lotics/workflows/<alias>.globals.d.ts` + re-wraps its EXISTING `src/workflows/<alias>.ts` body in the current envelope (strips + re-wraps; never re-fetches the body, so local edits survive). **`.lotics/` is reconciled to the manifest, not merely added to** — a `<alias>.globals.d.ts` whose alias the manifest no longer declares is DELETED. Only that exact filename shape is removed; anything else in the directory is left alone. The reconcile runs before the credential branch, so it happens offline too. The authored counterpart is never deleted — a `src/workflows/<alias>.ts` the manifest does not declare is NAMED instead (`check` and `set` both take their alias set from the manifest, so editing an undeclared body is a silent no-op). A getApp / binding / schema / dts-fetch failure is non-fatal (warns, keeps the last-generated files). **Re-silvers `package.json#lotics.agents`** from the live app row whenever its `inputs`/`outputs` disagree, then rewrites the agent `.d.ts` from the refreshed block: that block is a mirror AND the offline seed for `useAgentRun` typings, so a stale copy types the app against an agent that does not exist. The write is surgical and order-preserving, so it changes only the fields that actually differ. A hand edit to that block is therefore reverted — it never changed the agent anyway; to change one, `set_app_agent`. **Types are written for every DECLARED alias, body file or not** — the dts is rendered from the declaration, which is the whole point of the declare → codegen → write → check loop — and codegen NAMES each alias it wrote types for without a body, with the next step. It used to return early on a missing or blank `src/workflows/<alias>.ts` and say nothing, which left `workflow check` pointing at `app workflow pull` (which writes nothing for an alias the server has never bound) on one path and at `app codegen` — the command that had just declined to write them — on the other. **A body whose helper sits ABOVE the `__workflow` wrapper is refused by name and line** rather than wrapped a second time: the strip peels a wrapper only when it is the first line after the header, so a top-level declaration between the two used to leave the whole file read as the body and the re-wrap nested an envelope per run. Move the helper inside the wrapper — the body is one expression sequence. |
|
|
55
55
|
| `lotics app kit <path-to-packages/app-runtime>` \| `lotics app kit --published` | **The app's ONE range, `@lotics/app-runtime`, from a CHECKOUT, or back to the range it listed before**, so a runtime change is proven on a real app before it is published. Run inside the app directory; the path names `packages/app-runtime` in your clone (a kit change reaches an app through the runtime that depends on it — `LOTICS_UI_SRC` links the kit's source into `app dev`). It runs the runtime's own `npm run build`, `npm pack`s it into the app's `.lotics/kit/` (replacing any earlier pack, so the directory holds one tarball), removes the installed copy and the lockfile's entry for it, installs by file specifier, and then **proves the install**: the first non-dot file of the directory the package's own `package.json#files` publishes is hashed on both sides, and a mismatch — or an absence — is a refusal naming the file. That proof is the point. A repack under the SAME name is the normal case (a working copy's version does not move between builds) and the lockfile pins the first tarball's integrity, so on some npm versions the install is a no-op that reports success while the app keeps building against an hour-old kit; nothing else in the loop can tell. **Either way the rest of the manifest follows the runtime**: `@lotics/ui`, where the app lists it, moves to the range the runtime depends on — two ranges are two copies of the kit — and an app still on `@lotics/app-sdk` (the package the SDK was before it moved into the runtime) is MIGRATED: every `@lotics/app-sdk` and `@lotics/app-sdk/router` specifier under `src/` and in the Vite config becomes `@lotics/app-runtime/sdk` and `@lotics/app-runtime/router`, the dependency is dropped, and the summary names each file rewritten. **Refusals, all before anything is written**: a directory that is not a Lotics app (no `package.json#lotics.workspace_id`); a path that is not `@lotics/app-runtime`; a runtime that declares no `@lotics/ui` range while the app lists the kit; `--published` given a path; a build that failed, named with npm's own output tail. **It records what it did in `package.json#lotics.kit.@lotics/app-runtime`** (`version`, `tarball`, the `source` checkout, `packed_at`), which is what `lotics app check` warns about and what `lotics app deploy` REFUSES on unless you pass `--allow-local-kit` — `.lotics/` is excluded from the source archive a deploy uploads, so an app whose manifest names `file:.lotics/kit/…` ships a bundle that runs and a source tree the next clone cannot install. The record also keeps what the app listed before the FIRST checkout went in (`replaced` — a repack keeps it), and `--published` undoes the trial: the runtime and the kit go back to exactly those specs, so a kit trial never moves an app to another line on its way out; an app with no such record (never given a checkout, or given one by a CLI that recorded none, which the run says) moves to `^<latest>` from the registry and the kit to the range that published version depends on. Either way the tarball and the record are deleted, the lock entry and the installed copy are cleared, and a plain install resolves the published package. Needs no credential — npm, the filesystem and (for `--published` with no prior range) the registry. |
|
|
56
56
|
| `lotics setup <apg_id \| model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes — `--name` and `--timezone` apply), then fills its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** — it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the copy or the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as — that address IS the account it holds, not a second one. **The argument decides which of the two forms this is, by SHAPE**: a `*.json` file is a workspace MODEL — in either of ITS two forms, spelled out or `{"from": "<preset-slug>", …}` — and anything else is a package id copied through `library init`. The suffix decides it alone — asking the filesystem would answer a long library id with `ENAMETOOLONG` instead of with a verdict — and a model is checked OFFLINE before an account is created, because a file with a typo in it must not leave an organization behind. The model form creates no apps, so its sign-in link lands on the first table it made. It sends no `adopt`: an entity whose `label` already names a table in the workspace is REFUSED with every collision named, and the refusal adds the line the server cannot — `lotics scaffold apply <model.json>`, the verb that adds to the workspace you already have. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently copies a package into an org the caller did not name — the message says how to do each thing on purpose. Without it, `setup` copies into the account you already have and is a pure alias for `library init`. A path positional is accepted and IGNORED with a warning — `setup` writes nothing to disk — so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** — `organization_id`, `workspace_id`, `app_ids` (alias → id), `apps` (each app's `version_number`, or its `error`), `signin_url`, and `created` — which NAMES what landed (`tables`, `templates` and `knowledge_docs` are alias arrays; `sample_records` is a row count, since rows are not named things). Aliases rather than counts because the next question is about a particular artifact: a copied template carries the publisher's wording and a copied knowledge doc describes how they work, so "which of these should be mine?" is the conversation a copy starts, and a count cannot begin it. **The model form emits `entities`, `roles`, `record_ids` and `rows_skipped`** in place of `app_ids` / `apps` / `created` — a model creates no apps and nothing named for a copier to review. **The model form also runs the file's `apply` list** — each named package copied in after the tables exist, with that entry's `bind`, in order, stopping at a refusal with everything before it kept — and emits `applied: [{package, apps}]` beside them; **the sign-in link then lands on the FIRST app any applied package created**, falling back to the first table when the model applied none. Plus a `warnings` array carrying everything the prose form would have said out of band — an unbindable knowledge doc, a sign-in link that could not be minted, the publisher's-code disclosure, an app that landed without a version. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup …`. |
|
|
57
|
-
| `lotics scaffold check <model.json> [--json]` | **Prove a model before anyone sees it — no network, no credential**, unless the file names a preset. ONE parse of the whole file against the model schema (strict, so `tabels` or `row` is an error rather than a silently dropped key, and a model cannot express what only a starter bundle carries: `fixtures`, `knowledge`, `knowledge_expects`, a file-backed `excel`/`word`/`pdf-form` template; its `apps` are stated over the model's entities, never built code; a retired key — `field_roles`, `table_workflows`, `connections` — is refused by name with what replaced it), then `validateWorkspaceModel` — the caps, every cross-reference, and the first rows themselves (a field the entity does not declare, an unknown option alias, a link naming no row in the file, a duplicate `ref`, a date that is not one, a value on a platform-computed field, a files cell that is not a relative path beside the model or a `fil_` id, a document path with no file beside the model), then `write_rules`, then `records` and `apps`: an entity an app lists, opens or picks with no `records` entry, a title that is an autonumber or a code, a field placed twice on one record, a block whose entity does not link to the record, a `when` on a field that is not a single select, an act setting a field or option that does not exist, a check on a field that is not a yes/no or text formula, a register or rows-block entity with no `singular`. **Reports EVERY problem in one run**, each as `<path>: <message>` in the file's own keys (`entities.0.fields.1.type`, `apps.0.acts.1.set.stage`), so fixing a model is not a round trip per mistake. Exits 1 when there is one; exits 0 with the counts (`N tables, N fields, N links, N views, N roles, N rows`, plus `N apps` when the file states any), then **each app as its reader will see it** — the register's row, columns, filters, add and order; the record's door, header
|
|
57
|
+
| `lotics scaffold check <model.json> [--json]` | **Prove a model before anyone sees it — no network, no credential**, unless the file names a preset. ONE parse of the whole file against the model schema (strict, so `tabels` or `row` is an error rather than a silently dropped key, and a model cannot express what only a starter bundle carries: `fixtures`, `knowledge`, `knowledge_expects`, a file-backed `excel`/`word`/`pdf-form` template; its `apps` are stated over the model's entities, never built code; a retired key — `field_roles`, `table_workflows`, `connections` — is refused by name with what replaced it), then `validateWorkspaceModel` — the caps, every cross-reference, and the first rows themselves (a field the entity does not declare, an unknown option alias, a link naming no row in the file, a duplicate `ref`, a date that is not one, a value on a platform-computed field, a files cell that is not a relative path beside the model or a `fil_` id, a document path with no file beside the model), then `write_rules`, then `records` and `apps`: an entity an app lists, opens or picks with no `records` entry, a title that is an autonumber or a code, the `record.facts` or `record.blocks` that `sections` replaced, a section holding nothing, a field, a block or an act placed twice on one record, an `at` on a record with no status, an act at a staged section's foot offered while the section is not the work, a `where` on a field that is not a single select, a block whose entity does not link to the record, a `when` on a field that is not a single select, an act setting a field or option that does not exist, a check on a field that is not a yes/no or text formula, a register or rows-block entity with no `singular`. **Reports EVERY problem in one run**, each as `<path>: <message>` in the file's own keys (`entities.0.fields.1.type`, `apps.0.acts.1.set.stage`), so fixing a model is not a round trip per mistake. Exits 1 when there is one; exits 0 with the counts (`N tables, N fields, N links, N views, N roles, N rows`, plus `N apps` when the file states any), then **each app as its reader will see it** — the register's row, columns, filters, add and order; the record's door, header and the buttons beside its ⋯, checks, its sections as the reader scrolls them — the stage each is the work of, its fields, its blocks and the acts at its foot — its thread, and its acts with every `when`, `requires` and blocking check — with every value a default supplied marked `(default)`, and a line naming `lotics app preview <model.json>#<app>`, all on stdout. `--json` replaces both with one object and nothing else: `{ok: true, …counts, resolved: {<app>: …}, notes}` or `{ok: false, findings: [{path, message}]}`. **A `preset` is checked as N models, not one** — every variant merged onto the base (its added entities, and its added fields keyed by entity) and put through the same rules, each finding addressed `preset.variants.<slug>.<path>`, so a preset ships with every branch proven: the branch nobody took is the one that fails in the workspace of whoever takes it, who is the one reader who cannot fix it. A variant's `fields` key naming no declared entity is a finding too — the merge keys on the entity, so a typo'd alias adds those fields to nothing. Same verdict the server reaches, because it runs the server's own functions out of `@lotics/shared` rather than a second implementation of them. **A file written as `{"from": "<preset-slug>", …}` is resolved first** — one GET of that preset's file on the website — and that read is the one step on this path that needs the network; it says so when it cannot make it, and a slug nothing serves is answered with the slugs there ARE, read from the listing, rather than with a 404 the author cannot spell their way out of. Resolution is pure (`resolveModelFrom` in `@lotics/shared`): the named variants merged onto the preset's base in order, then `rename` through the same `applyBinding` a `--bind` goes through, then the file's own `entities` appended, and the preset's `records` and `write_rules` merged with the file's, entity by entity (the file's entry replacing the preset's). What comes out is the full form and goes through everything above unchanged, so a `from` file cannot reach a workspace by a route the full form does not. A variant slug the preset does not declare, an alias `rename` names that it does not declare, and a renamed label that is already another table's are each a finding rather than a silent drop — a branch quietly ignored scaffolds the base and looks like it worked. |
|
|
58
58
|
| `lotics scaffold apply <model.json> [--json]` | **Create the model in this workspace**: its tables, fields, select options, links, views, roles and first rows, through `POST /v1/workspaces/scaffold`. Runs `check` first, so a bad file never reaches the network, then resolves and ANNOUNCES its workspace (`lotics → <org> / <workspace>` on stderr) before writing — it is a destructive path. **Additive and re-runnable**: it is the verb that sends `adopt`, so an entity whose `label` already names a table here BINDS to that table and gains the fields, options and views it is missing, while `setup` refuses that same label. Nothing is ever modified or deleted, so applying the same model twice creates nothing the second time — and a declared PAIRING (`sync_both_ways` / `paired_field_alias`) over a link this workspace already has one-way is REFUSED before the first write, naming the alias and `update_fields sync_both_ways`, because a pairing is only ever created with the link and adoption would otherwise finish clean over a half-paired link. **After the first run the WORKSPACE remembers what each alias became**, so every later run binds entity, field, select option, template and role BY ID and a relabel on either side is a RENAME rather than a second table: the run reports each moved name with the command that reconciles it (`lotics field rename` moves the platform, the file and every bound app together; `update_table` moves a table's name), and applies anyway — which of the two names is right is the author's to decide, and the run bound the thing the model has always meant either way. A bound target the workspace no longer holds is refused by name, with `restore_table` and `lotics scaffold unbind` as the two ways out. **Rows land only where every bound table is empty**: one bound table already holding records and none are written anywhere, because sample rows landing among a customer's real ones cannot be told apart from them — it says so and reports `rows_skipped`. Prints `created`/`adopted` per entity with its table id **and the delta that landed on it** (`+8 fields, +2 options, +1 view`, and nothing where the run added nothing) — `adopted` alone cannot report the columns, options and views a later version of a model puts on a table that is already the owner's, and the only other proof was a full re-export and a diff — marking `(bound by name)` the one case where a NAME decided which table the model points at; the same `created`/`adopted` per ROLE with its group id — an adopted role binds a group that already exists, which is how one silently inherits another workspace's members — and rows written per entity. **The binding is the workspace's, not the file's**: a model file is never committed, so memory kept beside it is one author's disk — absent for a teammate, on a second machine or after a delete, and each of those falls silently back to the label join that grows the twin. It holds what each entity, field, option, role and template alias became (`tbl_`/`fld_`/`opt_`/`grp_`/`tpl_`), plus one entry per first ROW under its own `<entity>:<ref>` and `documents` (path → `fil_`), and is read by `diff`, `--documents`, `app create --from`, `app regenerate --from` and `workspace build` before any of them reads a label. **Then it copies in every package the file's `apply` list names, in order** — each one a `library init` with that entry's `bind` and `no_sample_data`, and each sending `adopt`, because by then the workspace holds exactly the tables this same run just created. Order is load-bearing: a later entry may bind onto a table an earlier one made. **A refused entry stops the run and the entries before it stay** — they are separate copies, committed as they land — so the refusal carries the server's own message plus what already landed and the one-package command to retry with. `--json` prints one object and nothing else (`entities`, each with `bound_by`: `created` by this run, `id` through the binding, or `label` for an alias nothing had bound; `roles`, `record_ids`, `rows_skipped`, `drift`, `applied: [{package, apps}]` — always present, empty included, so a reader cannot mistake "applied nothing" for "too old to say" — plus `organization_id`/`workspace_id` and a `warnings` array). Admin-only. A model STATES its apps and `apply` builds none of them — `lotics app create --from <model.json>#<app>` or `lotics workspace build` does; a published package named in `apply` brings its own. |
|
|
59
59
|
| `lotics scaffold apply <model.json> --documents` | **The `files` cells of `rows`, and nothing else** — no table, no field, no row. The half a re-apply cannot redo: rows land only into empty tables, so a model whose binaries were missed the first time has no other way back, and a model whose records are pictured ships with empty pictures until this runs. Each distinct path is uploaded once, recorded against the file it became, and attached with `update_records add_to` onto the record the binding names — joined by the row's own REF (`<entity>:<ref>`), so a row added to or removed from the file since the apply changes nothing about the rest. **A ref this workspace holds no record for is a refusal**, naming it: the alternative is filing its document under whichever row happened to sit at that position. **Idempotent by construction**: an uploaded path is reused from the binding's `documents` map and `add_to` is a set union, so running it twice leaves one id in the cell. The field is bound by the id the binding recorded, and by LABEL only for a field added since — a label that has moved with nothing bound to it is refused, pointing at `lotics scaffold diff`. Admin-only. |
|
|
60
60
|
| `lotics scaffold diff <model.json>` | **Where the file and this workspace have come apart.** Runs `scaffold export` against the selected workspace and prints what the model has and the workspace lacks, the reverse, and every field the two disagree about — a type, the option labels one carries and the other does not, or a PAIRING the model declares over a link that is one-way here, which `apply` refuses and nothing else named — and each `unique` set one side declares and the other lacks, compared as an unordered set of the workspace's fields. **Joined on the ids this workspace remembers**, entity then field then select option, and on LABEL only for an alias nothing has bound — a new entity, or a workspace this model has never been applied to, which is the join the first apply itself makes. A relabel on either side therefore prints as one rename under the alias (`order.state: label: model "Stage" · workspace "Giai đoạn"`) rather than as one thing the workspace lacks and one the model lacks, which is what made an additive apply add a second one. A bound id this workspace no longer serves is named with the command that puts it back, never silently re-bound by label. **Exits 1 on any difference**, so it is a gate: a starter published from a model that has drifted would ship the FILE's labels while the workspace uses others, and nothing else compares them. Checks the file offline first. Admin-only (the export is). |
|
|
@@ -69,8 +69,8 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
69
69
|
| `lotics docs model` \| `lotics docs model/<section>[/…]` | **The model reference, from inside the binary** — the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs → entities and fields → `records` → one app per job → `scaffold check` → `app preview` → `workspace build`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<entity-alias>:<ref>"`), the rules, the `apply` list (packages copied in after the model's own tables, each with an optional `bind` onto them), the `preset` block (a published model's branches and its at-most-two questions), the **`from` form** — `{from, variants, rename, entities, rows, records, write_rules, apps, apply}`, which names a preset by SLUG instead of restating it — and one complete worked example. **Offline, no account.** |
|
|
70
70
|
| `lotics report '<json>'` \| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** — `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` — invoking it IS the consent that passive collection needs an opt-in for — but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** — a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |
|
|
71
71
|
| `lotics app check` | Every pre-flight `deploy` runs, WITHOUT building or shipping. **First, whether this project is even based on the served version** — the one thing a deploy REFUSES outright rather than pushing (the server 409s a stale `prev_version_id`), and the one finding that invalidates every other: a stale tree and the live app are two different apps, so comparing them reports nothing trustworthy. Stale exits 1 naming both versions and stops before the rest; a project with no stamp at all — or an app with no version yet — is a first deploy, not a conflict. `deploy` runs the SAME assertion, so a stale tree fails before it pushes or builds. Then: the manifest's agent schemas against the live app row, every binding a deploy would push, aliases the source calls that nothing bound (queries, workflows AND agents), bindings this bundle stopped calling (the same transition — and the same baseline — `deploy` reports, so the two cannot disagree), capability-gated SDK calls the manifest doesn't declare, a missing icon/theme, a missing app `description` (it heads the capability catalog the chat agent reads every turn, and its absence has no other symptom), a `vite.config.ts` that never defines `global`/`__DEV__` **in a project that still ships react-native** (read off its own `package.json` — an app on the React DOM kit bundles none of it and the check would be advice to define two globals nothing reads), and a `window.open` in the app's own source (each fails ONLY in the deployed app: dev bundles with esbuild and production with rollup, so typecheck, lint, build and `app dev` are all green), an agent whose capability and its reach disagree, in EITHER direction, over any of the five declaration-bound tools (`run_app_query`/`run_app_workflow` against `query_aliases`/`workflow_aliases`; `grep_knowledge`/`read_knowledge`/`list_knowledge` against `knowledge_doc_ids`) — the tool is the capability, the list is the reach, and a tool with no reach means every call it makes is refused while the run still COMPLETES, so it surfaces as a model ignoring its prompt; read off the live row, never the manifest, which mirrors those fields but is pushed by no verb, and a notice for any alias the source computes at runtime (invisible to every check here and to `--prune`'s unbind guard). **And whether the runtime this app builds against has fallen behind what is published** — `@lotics/app-runtime` alone, since the kit an app lists sits at the runtime's range, read from `node_modules` rather than the range because a caret is minor-locked below 1.0 (`^0.13.x` can never resolve `0.14`, and `npm update` does nothing). A release LINE behind is loud and names `lotics app kit --published` and `@lotics/ui`'s `MIGRATION.md`; anything smaller is one quiet `npm update` line, because a warning that fires on every deploy is one the reader stops seeing. An app still listing `@lotics/app-sdk` is told it moved into the runtime and which verb migrates it. The lookup is bounded and every failure is silence: a version check must never become a new way for a deploy to fail. Every deploy finding is the same helper `deploy` calls, so a green check means a deploy will not complain. **And the PORTABILITY gate, the second of two rules `check` runs that a deploy does not** (the first is the undeclared call site below) — the ids an app cannot carry into another workspace, over the working tree, with the same exclusions the deploy tar applies. Two rules. **An id this workspace MINTED**, written into `src/`, a `.md` or the app's own docs — it resolves to nothing in a copy, and in prose it is an instruction the copier's agent follows; this is the one a `library publish` also refuses, on the uploaded archive. **And an id-shaped STAND-IN** too short for the generator that mints its prefix (`"opt_X"`, `"fld_a"` — quoted or in a code span, so a bare `opt_in` stays legal, and never in a test file), which only `check` runs, and which additionally reads a workflow body and the manifest: those two are exempt from the first rule because a publish INVERTS a real id there and cannot invert a fake. **And an `app_` id anywhere in `app.json`**: the spec names no other app, so a pasted one is re-pointed by nothing. Each is reported as `<file>:<line> — <id>` with the one edit that fixes it. This gate reads only the files, but the command around it still needs a resolvable credential and the live app row, so it is not an offline check. **And every bound workflow body, checked as `app workflow check` checks it** — the same isolated per-alias program against the pulled `.lotics/workflows/<alias>.globals.d.ts`, then the server's `verify_only` verdict on each body that passed, which exits 1 on any issue and warns when the server is not reached. The server verifies a body once, at the save that wrote it, so a body whose declared types have since moved stays stored, matching what is live, and is refused by the next writer — a starter copy, in somebody else's workspace. The verdict is as fresh as those types, which `pull`, `workflow pull` and `codegen` refresh. **And the app's own `npm run typecheck`**, after regenerating the `.lotics/*.d.ts` companions from the manifest — the same run a deploy makes before building, so a filter or sort key the query does not project fails here rather than at the first member's request. **Exits 1 on that, on a body the types refuse, on a failing typecheck, and on what a `deploy` would REFUSE or PUSH** — an agent schema that disagrees with the live app, any binding the project has ahead of the app (an edited workflow body or declaration, edited agent prose, a changed query), and a query or workflow `description` over the 300-character capability cap, which is a binding no `set` and no deploy will take (the rule is `@lotics/shared`'s, the same one the server refuses with, and `workflow set` / `query set` ask it before sending anything — so a long line costs one edit rather than a failed push per alias). **And every manifest query declared with NO description**, named in one line: that sentence is what a chat or MCP caller chooses between aliases by, and an alias is a JS identifier. `app create --from` deliberately writes none — a template over a shape's own English reads like a line about the business while saying nothing — so a generated app is told once, here, which lines are the author's to write. Both are things a deploy would act on, so CI gating on a green check means a deploy has nothing left to do; genuine advisories (capabilities, branding, a runtime-computed alias, orphaned bindings) stay advisory and never fail it. **`--screens` adds the rendered surface, and is its own entry below.** It runs after the typecheck and only when it passed — a type error renders nothing to measure — and its findings, and any write it refused, fold into this command's exit code. **It also refuses a call site the manifest no longer declares.** `useQuery`/`useWorkflow` keep a bare-string overload for a computed alias, so deleting or renaming an alias leaves every call site compiling and failing only when the screen renders — the one edit most likely to orphan a call site is the one the generated types cannot catch. The alias literals in `src/` are matched against `package.json#lotics.queries`/`.workflows` (the manifest, not the live row: the server still SERVES an alias whose declaration was just deleted, because a deploy never unbinds), and an undeclared one exits 1. `app codegen` prints the same finding as a warning, since it is the command an author runs right after editing the manifest. **A failing body that is byte-identical to the one the server is running is labelled as such**: its `lotics.synced.workflows.<alias>.content` baseline proves the file has not been edited since it was pushed or pulled, so the failure is a grammar migration the stored body is owed rather than a stale checkout — a link field reads as an id array, so drop `.id` or descend with `linked(…)`. Until the body is edited and `set`, the stored one keeps running as it always has. **And a query that reads past a table's ROW RULE.** A table's `private_filters` bind the CALLER, and an app query's caller is the app's OWNER — the viewer needs no table access, `app:use` is the grant — so the rule passes and every row is served. For each table a declared query reads (`GET /v1/tables/{id}`, the one surface that serves the rule), the viewer predicate — `current_member in_any_group`, or a member field's `is_current_member` / `is_not_current_member` — is attributed to the SCAN it guards: a `from_table`'s own `filter`, or an enclosing `filter` node's predicate, whose rows are the ones that scan produced. So a clause written over one table never silences the finding for the ruled table joined beside it, and every scan no clause covers is named with its table. A table this credential may not read (403, or 404 for one that is gone) is dropped — a rule that cannot be read is not evidence of one — while any other failure of that read fails the command, since "I could not ask" must never render as "there is no rule". Advisory, never part of the exit code: an app that deliberately serves the whole table to a desk of people who may all read it is legitimate, and nothing here can tell the two apart. **And it names the workflows a chat or MCP caller is offered with no description** (one `get_app_capabilities` read, the reader's own view of the app): that text is what those callers choose between aliases by, and without it they choose by the alias. It cannot be counted from the manifest — a workflow bound out of band is not declared there at all. Advisory, never part of the exit code. **It regenerates `.lotics/app_fields.ts` from the live schema before typechecking**, as a deploy does — a gitignored map from a moved schema otherwise passes. **And three claims the app makes**: a bound body's writes against `package.json#lotics.writes`, read as the step tree the server would store, exit 1 on a field no entry covers, and declaring none only warns; a description carrying `<placeholder>` syntax exits 1, since the catalogue escapes angle brackets — state the format as an example; so does one naming a desk `query_apps` does not list. |
|
|
72
|
-
| `lotics app check --screens [--screen <label>] [--width <n>] [--shots <dir>] [--changed]` | **`--screens` adds the rendered surface**: the app is served the way `app dev` serves it (its real data, this key), rendered headless in Chrome (`CHROME_PATH`/`LOTICS_CHROME`, then Playwright's, then system) at 1280 and 375. **The screens are its navigation's destinations** — a `nav` landmark's `a[href]` or `role="link"` (an app's route lives in its router, so the kit's shell renders each screen as a button carrying the link role and no `href`), else the first tab strip, else the root — in that order, because a screen's own lifecycle desk draws a tablist too, and reaching for one first walks a screen's STAGES as if they were the app's. **Each screen is then WALKED THROUGH ITS OWN DOORS**, nothing configured per app: every door is named by something the document states. A record: a row stamped `data-opens="page"`, a real `a[href]`, or an EMPTY DOOR — a named control with no text and no child element, the only legal whole-row press target, which reaches a drawer register, a Schedule row and a hand-written row alike. On a record: the acts menu (`aria-haspopup`), the first child row, the facts behind the fold (`aria-expanded="false"`). On any surface: every dialog a visible primary or secondary act raises — nothing announces one, so a press is kept only where an overlay appeared. **THE RUN IS A READ, AND THE NETWORK IS WHERE THAT IS ENFORCED — never a rule about what the page may draw.** The app frame holds no credential and Chrome runs on a throwaway profile, so every call the app makes reaches the workspace through this CLI and nowhere else, and this CLI serves an ALLOWLIST of reads. Everything outside it is refused before the call leaves the machine — a workflow run, a record write, an upload, an agent run, a comment, and any op this CLI does not know, which is refused because it is not on the list rather than because anyone listed it. A refused call HEADS the report, naming the surface, the control the app had focused and the RPC by its alias (never a payload value), and fails the run on its own: a walk that provoked a write left the app in a state no reader could have put it in, so nothing measured under it means anything. What the walk does is bounded on the page as well: nothing inside an open overlay, nothing typed, no submit and nothing in a form, no act the kit marks costly (`data-tone` danger/warning) and none whose own name is the write, in either language — and nothing is FOCUSED, because focus cannot be taken from one field without leaving another, and a field that saves itself when the reader leaves it saves on exactly that. Where a reading needs the focused state, as the focus-ring rule does, Chrome is asked to PAINT `:focus-visible` and release it again: the cascade answers, focus never moves and no event is dispatched. Each overlay closes with Escape, confirmed closed; a record is descended at most twice; the walk stops at twelve surfaces per screen and NAMES each door it left. Each surface is measured once no request is in flight, and the measurable probes of `@lotics/ui` docs/reviewing.md run over the DOM, each finding printing the rule it IS — its law, its section and the one edit that answers it — so the numbers need no key. What they exempt is what the screen itself declares: a register's ordinal gutter (a column counting to the row count is the shape's numbering, which no app can treat), a strip whose list carries `data-order="sequence"` (a lifecycle rail, which composition.md permits under a screen's tabs), a hairline or `clip-path`-clipped leaf (the visually-hidden node a control plants for a screen reader), and a leaf whose own computed line clamp states a count over a sentence. **A meter counts as an encoding only where it draws a POSITION** — `aria-valuenow` inside a range with room left. A bar pinned at its own maximum — what a meter alarmed AT its maximum draws on every alarmed row — reads the same as every value above it, and one with no maximum states none; both are counted in the census's `devices` and out of its `encoded`, so "nothing drawn" and "drawn and saying nothing" never read alike. The bare values that remain are grouped into columns, each named by the heading over it, so a finding says WHICH slot draws its figures as words. **Two finding classes read what geometry cannot.** `clutter` (docs/hierarchy.md): a second primary act, a value in two places on a record, a box reserving more lines than it holds, markers on over half a form's fields, a second accent. `right_form` (docs/screen.md's device index): a boolean as a two-option select, a day run as a repeated date column where the kit ships `Schedule`. Each prints a line per rule fired, with three offenders. **Four read room and counts** (reviewing.md §8l–§8o): `reserved_blank`, a register column 40px or wider that is blank (no text, no control) on more than half the rows in view, or a row-act gutter leaving 40px beside the acts on them — a row a complete register draws for nothing stored (`data-ghost`, a day nobody filed) is no row of the set; `shed_with_room`, a column the register's width gave up (the kit states them as `data-shed`) that, with the gap between two columns, fits in what its slots hold beyond what each must seat — its heading and widest ink, a fixed column's whole width, the acts most rows have; `action_off_heading`, a section's create (a control stating `data-create`) drawn in its body rather than its heading row; and `partial_count`, a count or `n of N` equal to the rows in hand of a read the server cut on the screen being walked, where the server's own count of the same alias, params and filter — asked by this CLI, a read — says otherwise. **Four read a record's anatomy** (docs/hierarchy.md), over each record on the surface with the title heading it: `record_primary`, a primary act in a block's heading row or among the record's own actions — the record's one primary is its open act, and a block's add is secondary (two under one heading are `clutter`'s); `act_fold`, an open act whose button ends past the first viewport of the width laid out — 800px at 1280, 667px at 375 — unscrolled; `fact_repeats`, a fact whose value reads the same as the header's own words, whatever its case; and `empty_block`, a block that draws nothing under its heading, not even the line saying it is empty. A census line per screen (text runs, money strings, bare values against the devices reading, tab strips) prints first, so a clean verdict over a screen that rendered nothing cannot pass; a screen that renders no text is itself a finding, and one still changing after fifteen seconds is measured as it is. **A cold dependency optimisation is waited out, not measured**: the first paint has its own bound, far longer than settle's, since an app's modules load after its document completes and Vite holds them; only a MOUNTED, idle, textless frame is blank at once, and the finding names the wait and its blocker. It also RELOADS the page under the probe, so a width is measured again once; a second reload is a page that keeps moving and fails. **`--screen <label>` and `--width <n>` narrow a run** (repeatable, comma-separated; the label matches case-insensitively as a substring), for the author iterating on one screen who would otherwise pay a typecheck, a Vite boot and every screen at both widths on every edit; the clean verdict then names only the widths covered, and a `--screen` matching nothing is refused. **`--shots <dir>` writes what the run measured** — a `<surface>@<width>.png` and a `<surface>@<width>.json` per surface, off the SAME settled frame the probes read, so a shot and a finding can never describe different pixels. The PNG is the WHOLE surface: an app scrolls inside a box of its own, so the window is grown to the height the probe measured and put back, and an overlay a resize dismissed is shot as the viewport, the sidecar saying so (`app dev`'s header band is in it — the band the app was laid out under). The JSON is the half a picture cannot carry: the nav's first item's left edge, the title and first section heading, the primary acts by name, label/value pairs against what the folds state, values drawn twice, money that wraps, text the layout cut, and the console errors and uncaught exceptions the frame raised. Named `<nn>-<slug>` plus one `__<step>` per door taken (`__record`, `__menu`, `__child`, `__fold`, `__dialog-<n>`); `<nn>` is the walk's index, which lists the directory in reading order and keeps two labels that fold to one ASCII slug apart. The row a record surface opened from is the sidecar's `opened_from` and that screen's census line, never a file name — it is a person's data. The directory is created if missing, and refused before the dev server boots when it cannot be; `--shots` without `--screens` is refused. Findings exit 1 like the rest. **Where it renders**: the app's files are copied into the render project of its DEPENDENCY SET — `~/.lotics/render/<key>/`, keyed by the manifest's `dependencies` and `devDependencies` as written, a `file:` tarball by its bytes — the one `app preview` renders in, so the install and Vite's dependency optimisation are paid once per set rather than per app; one render holds a project at a time (`<key>.lock` — a second run waits, naming the app and pid, and takes over a dead hold). Where that install resolved another runtime or kit than the app's own, the run says which, since a deploy builds the app's own. The run ends with what each phase cost: `preflight · install|reuse · walk`. **`--changed`** walks only when something the screens READ has moved since this checkout's last walk — each part of `app.json` (every record apart), each declared query, each bundled source file, the field map, the ranges, the kit the render resolved and the CLI version, whose probes measure them — read against `.lotics/screens_check.json`, which every walk writes; with nothing moved, that walk's report is printed again under its timestamp and still fails the run. An app's screens all read its one spec, so a move walks the app whole. It walks, saying why, with no earlier walk, a different `--screen`/`--width`, or `LOTICS_UI_SRC` set (a linked working copy has no fingerprint); the workspace's rows are not an input. `--changed` without `--screens`, or beside `--shots`, is refused. |
|
|
73
|
-
| `lotics app preview <model.json>#<app> [--shots <dir>] [--width <n>] [--screen <label>] [--kit <path>]` | **The app a model states, RENDERED — before a table exists, and with no credential in the process.** The model is checked offline, the named app is compiled against the workspace the model WOULD become (synthetic `tbl_`/`fld_`/`opt_`/`grp_` ids, minted positionally off the file), and every read the app makes is answered from the model's own `rows` — projected under the columns the query names, narrowed by the params it declares and sorted the way it states, so a child block under a record holds that record's rows and not every parent's. An entity the app reads that states no `rows` gets three synthesized, so a register never measures clean over nothing. Then the SAME headless walk `app check --screens` takes: the register, the record its first row opens in its door, and the add dialog, at 1280 and 375, measured by the same probes. `--shots` writes a PNG and a sidecar per surface. **Exits 1 on any finding**, exactly as the check does, and prints what each phase cost — bind, install-or-reuse, walk. **Nothing is created and nothing is reached** — no app row, no table, no workspace; the only network it needs is the npm registry, and only when the kit has moved. It renders in the cached render project of its dependency set (`~/.lotics/render/<key>/`, shared with `app check --screens` and with every model whose app lists the same set, and held by one render at a time — a second run waits, naming the app and pid holding it, and takes over a hold whose process is gone): the runtime and its kit are PUBLISHED packages, so a project is what a preview needs to install them into — the CLI has no renderer of its own. **It installs the runtime this CLI was built for**, and the kit through it, never `latest`: the spec this binary's generator writes is the one that runtime reads, and a version that published mid-session is a runtime nobody checked the model against. A version the registry does not serve yet is refused before npm runs, naming `--kit`. **`--kit <checkout>`** (repeatable) — a checkout's ROOT, which names the app-runtime and the ui it holds, or one of those two packages — renders against that checkout instead — built, packed and proven the way `lotics app kit` installs one into an app — and the run prints that it rendered against a LOCAL CHECKOUT, first and last, so the render is never read as the published one. The project is a CACHE and never a source: `npm install` runs on the first render of a set and again only when a checkout's tarball did, and every file in it is rewritten from the model on each run. **A document a row names by path is served** from the project's own static root, as the named file a workspace would hold, so a files block and a record's picture read what the model states; a `fil_` id is carried by name. **`--record <ref>`** opens that row's record — a row of the app's entity, by the ref the model gives it — where the walk otherwise opens the first; a ref naming no such row is refused with the refs there are. **Every read is answered at the BRIDGE**, not inside the page. The app SDK's design-time fixture (`registerMockFixture` + `?__mock=1`) is the wrong half of this: the flag lives in the app's own url, and the first record page is a navigation the app's router performs — so from that surface onwards the flag is gone and every read falls through anyway. The bridge is the one place every call arrives whatever the url says, so `query`, `field_options`, `members` and `context` are answered there from one reading of the rows, and the generated entry ships EXACTLY as `app create --from` writes it. **A preview writes nothing**: a workflow, an upload or an agent run meets the same read gate `app check --screens` uses, so the app draws its own refusal path rather than reporting a save nothing moved for, and the call is reported above the census the way the check reports one. **What came out empty, it names**: a `formula`, `rollup`, `lookup` or `autonumber` is computed here rather than stated in the model, and the census lists each computed column that stayed empty on EVERY row of a table that has rows — named by outcome, since a total drawn blank is either the app's own answer or a hole and only the run can say which. The census reads the CELLS and not the field types, so nothing is exempt by kind. **A computed column that came out `#ERROR:` is named too, and the run refuses it** before anything is installed: a wall of red measures clean, and a run that drew it and exited 0 would tell its author the app is fine. **It computes in UTC**, because a model states no timezone — a row's `@today`, an autonumber's `{YEAR}` and a record's own clock all land on the run's own date at UTC midnight, so two runs of one model draw the same register wherever they are made. |
|
|
72
|
+
| `lotics app check --screens [--screen <label>] [--width <n>] [--shots <dir>] [--changed]` | **`--screens` adds the rendered surface**: the app is served the way `app dev` serves it (its real data, this key), rendered headless in Chrome (`CHROME_PATH`/`LOTICS_CHROME`, then Playwright's, then system) at 1280 and 375. **The screens are its navigation's destinations** — a `nav` landmark's `a[href]` or `role="link"` (an app's route lives in its router, so the kit's shell renders each screen as a button carrying the link role and no `href`), else the first tab strip, else the root — in that order, because a screen's own lifecycle desk draws a tablist too, and reaching for one first walks a screen's STAGES as if they were the app's. **Each screen is then WALKED THROUGH ITS OWN DOORS**, nothing configured per app: every door is named by something the document states. A record: a row stamped `data-opens="page"`, a real `a[href]`, or an EMPTY DOOR — a named control with no text and no child element, the only legal whole-row press target, which reaches a drawer register, a Schedule row and a hand-written row alike. On a record: the acts menu (`aria-haspopup`), the first child row. On any surface: every dialog a visible primary or secondary act raises — nothing announces one, so a press is kept only where an overlay appeared. **THE RUN IS A READ, AND THE NETWORK IS WHERE THAT IS ENFORCED — never a rule about what the page may draw.** The app frame holds no credential and Chrome runs on a throwaway profile, so every call the app makes reaches the workspace through this CLI and nowhere else, and this CLI serves an ALLOWLIST of reads. Everything outside it is refused before the call leaves the machine — a workflow run, a record write, an upload, an agent run, a comment, and any op this CLI does not know, which is refused because it is not on the list rather than because anyone listed it. A refused call HEADS the report, naming the surface, the control the app had focused and the RPC by its alias (never a payload value), and fails the run on its own: a walk that provoked a write left the app in a state no reader could have put it in, so nothing measured under it means anything. What the walk does is bounded on the page as well: nothing inside an open overlay, nothing typed, no submit and nothing in a form, no act the kit marks costly (`data-tone` danger/warning) and none whose own name is the write, in either language — and nothing is FOCUSED, because focus cannot be taken from one field without leaving another, and a field that saves itself when the reader leaves it saves on exactly that. Where a reading needs the focused state, as the focus-ring rule does, Chrome is asked to PAINT `:focus-visible` and release it again: the cascade answers, focus never moves and no event is dispatched. Each overlay closes with Escape, confirmed closed; a record is descended at most twice; the walk stops at twelve surfaces per screen and NAMES each door it left. Each surface is measured once no request is in flight, and the measurable probes of `@lotics/ui` docs/reviewing.md run over the DOM, each finding printing the rule it IS — its law, its section and the one edit that answers it — so the numbers need no key. What they exempt is what the screen itself declares: a register's ordinal gutter (a column counting to the row count is the shape's numbering, which no app can treat), a strip whose list carries `data-order="sequence"` (a lifecycle rail, which composition.md permits under a screen's tabs), a hairline or `clip-path`-clipped leaf (the visually-hidden node a control plants for a screen reader), and a leaf whose own computed line clamp states a count over a sentence. **A meter counts as an encoding only where it draws a POSITION** — `aria-valuenow` inside a range with room left. A bar pinned at its own maximum — what a meter alarmed AT its maximum draws on every alarmed row — reads the same as every value above it, and one with no maximum states none; both are counted in the census's `devices` and out of its `encoded`, so "nothing drawn" and "drawn and saying nothing" never read alike. The bare values that remain are grouped into columns, each named by the heading over it, so a finding says WHICH slot draws its figures as words. **Two finding classes read what geometry cannot.** `clutter` (docs/hierarchy.md): a second primary act, a value in two places on a record, a box reserving more lines than it holds, markers on over half a form's fields, a second accent. `right_form` (docs/screen.md's device index): a boolean as a two-option select, a day run as a repeated date column where the kit ships `Schedule`. Each prints a line per rule fired, with three offenders. **Four read room and counts** (reviewing.md §8l–§8o): `reserved_blank`, a register column 40px or wider that is blank (no text, no control) on more than half the rows in view, or a row-act gutter leaving 40px beside the acts on them — a row a complete register draws for nothing stored (`data-ghost`, a day nobody filed) is no row of the set; `shed_with_room`, a column the register's width gave up (the kit states them as `data-shed`) that, with the gap between two columns, fits in what its slots hold beyond what each must seat — its heading and widest ink, a fixed column's whole width, the acts most rows have; `action_off_heading`, a section's create (a control stating `data-create`) drawn in its body rather than its heading row; and `partial_count`, a count or `n of N` equal to the rows in hand of a read the server cut on the screen being walked, where the server's own count of the same alias, params and filter — asked by this CLI, a read — says otherwise. **Three read a record's anatomy** (docs/hierarchy.md), over each record on the surface with the title heading it: `record_primary`, a primary act in a block's heading row — a block's add is secondary beside the record's own acts (two under one heading are `clutter`'s); `fact_repeats`, a field whose value reads the same as the header's own words, whatever its case; and `empty_block`, a block that draws nothing under its heading, not even the line saying it is empty. A census line per screen (text runs, money strings, bare values against the devices reading, tab strips) prints first, so a clean verdict over a screen that rendered nothing cannot pass; a screen that renders no text is itself a finding, and one still changing after fifteen seconds is measured as it is. **A cold dependency optimisation is waited out, not measured**: the first paint has its own bound, far longer than settle's, since an app's modules load after its document completes and Vite holds them; only a MOUNTED, idle, textless frame is blank at once, and the finding names the wait and its blocker. It also RELOADS the page under the probe, so a width is measured again once; a second reload is a page that keeps moving and fails. **`--screen <label>` and `--width <n>` narrow a run** (repeatable, comma-separated; the label matches case-insensitively as a substring), for the author iterating on one screen who would otherwise pay a typecheck, a Vite boot and every screen at both widths on every edit; the clean verdict then names only the widths covered, and a `--screen` matching nothing is refused. **`--shots <dir>` writes what the run measured** — a `<surface>@<width>.png` and a `<surface>@<width>.json` per surface, off the SAME settled frame the probes read, so a shot and a finding can never describe different pixels. The PNG is the WHOLE surface: an app scrolls inside a box of its own, so the window is grown to the height the probe measured and put back, and an overlay a resize dismissed is shot as the viewport, the sidecar saying so (`app dev`'s header band is in it — the band the app was laid out under). The JSON is the half a picture cannot carry: the nav's first item's left edge, the title and first section heading, the primary acts by name, label/value pairs against what the folds state, values drawn twice, money that wraps, text the layout cut, and the console errors and uncaught exceptions the frame raised. Named `<nn>-<slug>` plus one `__<step>` per door taken (`__record`, `__menu`, `__child`, `__fold`, `__dialog-<n>`); `<nn>` is the walk's index, which lists the directory in reading order and keeps two labels that fold to one ASCII slug apart. The row a record surface opened from is the sidecar's `opened_from` and that screen's census line, never a file name — it is a person's data. The directory is created if missing, and refused before the dev server boots when it cannot be; `--shots` without `--screens` is refused. Findings exit 1 like the rest. **Where it renders**: the app's files are copied into the render project of its DEPENDENCY SET — `~/.lotics/render/<key>/`, keyed by the manifest's `dependencies` and `devDependencies` as written, a `file:` tarball by its bytes — the one `app preview` renders in, so the install and Vite's dependency optimisation are paid once per set rather than per app; one render holds a project at a time (`<key>.lock` — a second run waits, naming the app and pid, and takes over a dead hold). Where that install resolved another runtime or kit than the app's own, the run says which, since a deploy builds the app's own. The run ends with what each phase cost: `preflight · install|reuse · walk`. **`--changed`** walks only when something the screens READ has moved since this checkout's last walk — each part of `app.json` (every record apart), each declared query, each bundled source file, the field map, the ranges, the kit the render resolved and the CLI version, whose probes measure them — read against `.lotics/screens_check.json`, which every walk writes; with nothing moved, that walk's report is printed again under its timestamp and still fails the run. An app's screens all read its one spec, so a move walks the app whole. It walks, saying why, with no earlier walk, a different `--screen`/`--width`, or `LOTICS_UI_SRC` set (a linked working copy has no fingerprint); the workspace's rows are not an input. `--changed` without `--screens`, or beside `--shots`, is refused. |
|
|
73
|
+
| `lotics app preview <model.json>#<app> [--shots <dir>] [--width <n>] [--screen <label>] [--kit <path>]` | **The app a model states, RENDERED — before a table exists, and with no credential in the process.** The model is checked offline, the named app is compiled against the workspace the model WOULD become (synthetic `tbl_`/`fld_`/`opt_`/`grp_` ids, minted positionally off the file), and every read the app makes is answered from the model's own `rows` — projected under the columns the query names, narrowed by the params it declares and sorted the way it states, so a child block under a record holds that record's rows and not every parent's. An entity the app reads that states no `rows` gets three synthesized, so a register never measures clean over nothing. Then the SAME headless walk `app check --screens` takes: the register, the record its first row opens in its door, and the add dialog, at 1280 and 375, measured by the same probes. `--shots` writes a PNG and a sidecar per surface. **Exits 1 on any finding**, exactly as the check does, and prints what each phase cost — bind, install-or-reuse, walk. **Nothing is created and nothing is reached** — no app row, no table, no workspace; the only network it needs is the npm registry, and only when the kit has moved. It renders in the cached render project of its dependency set (`~/.lotics/render/<key>/`, shared with `app check --screens` and with every model whose app lists the same set, and held by one render at a time — a second run waits, naming the app and pid holding it, and takes over a hold whose process is gone): the runtime and its kit are PUBLISHED packages, so a project is what a preview needs to install them into — the CLI has no renderer of its own. **It installs the runtime this CLI was built for**, and the kit through it, never `latest`: the spec this binary's generator writes is the one that runtime reads, and a version that published mid-session is a runtime nobody checked the model against. A version the registry does not serve yet is refused before npm runs, naming `--kit`. **`--kit <checkout>`** (repeatable) — a checkout's ROOT, which names the app-runtime and the ui it holds, or one of those two packages — renders against that checkout instead — built, packed and proven the way `lotics app kit` installs one into an app — and the run prints that it rendered against a LOCAL CHECKOUT, first and last, so the render is never read as the published one. The project is a CACHE and never a source: `npm install` runs on the first render of a set and again only when a checkout's tarball did, and every file in it is rewritten from the model on each run. **A document a row names by path is served** from the project's own static root, as the named file a workspace would hold, so a files block and a record's picture read what the model states; a `fil_` id is carried by name. **`--record <ref>`** opens that row's record by its own address — a row of the app's entity, by the ref the model gives it, open or closed — where the walk otherwise opens the first row the register lists; a ref naming no such row is refused with the refs there are. **Every read is answered at the BRIDGE**, not inside the page. The app SDK's design-time fixture (`registerMockFixture` + `?__mock=1`) is the wrong half of this: the flag lives in the app's own url, and the first record page is a navigation the app's router performs — so from that surface onwards the flag is gone and every read falls through anyway. The bridge is the one place every call arrives whatever the url says, so `query`, `field_options`, `members` and `context` are answered there from one reading of the rows, and the generated entry ships EXACTLY as `app create --from` writes it. **A preview writes nothing**: a workflow, an upload or an agent run meets the same read gate `app check --screens` uses, so the app draws its own refusal path rather than reporting a save nothing moved for, and the call is reported above the census the way the check reports one. **What came out empty, it names**: a `formula`, `rollup`, `lookup` or `autonumber` is computed here rather than stated in the model, and the census lists each computed column that stayed empty on EVERY row of a table that has rows — named by outcome, since a total drawn blank is either the app's own answer or a hole and only the run can say which. The census reads the CELLS and not the field types, so nothing is exempt by kind. **A computed column that came out `#ERROR:` is named too, and the run refuses it** before anything is installed: a wall of red measures clean, and a run that drew it and exited 0 would tell its author the app is fine. **It computes in UTC**, because a model states no timezone — a row's `@today`, an autonumber's `{YEAR}` and a record's own clock all land on the run's own date at UTC midnight, so two runs of one model draw the same register wherever they are made. |
|
|
74
74
|
| `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` **and the `description`** from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). The `description` is the one line an agent reads when choosing between the app's aliases (the workflow counterpart to a query's) — authored in the manifest so it lives beside the body in version control and rides every push; omit it and the workflow keeps whatever description it already has, so a push can never blank one set elsewhere. When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. A deploy runs this same verb for every alias whose declaration or body is ahead of the app, so this command is the one-alias spelling of what a release does, not a step a release leaves to a person. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. A push also prints any non-blocking verify warnings, including an input the alias declares that the body never reads. A first bind MINTS the workflow row, and the id it echoes is written back into `package.json#lotics.workflows.<alias>.workflow_id` — the same surgical write the derived `outputs` gets. Without it a hand-declared alias ended up shaped unlike its siblings, so anything reading the manifest (an audit, a port to another workspace, a person comparing two blocks) had to treat a missing id as normal, which is exactly how a genuinely missing one stops being visible. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version; without it such a write is refused and every breaking change is named (see `app api`). |
|
|
75
75
|
| `lotics app agent set <alias> [--acknowledge-breaking-api]` | Push `src/agents/<alias>.md` — plus `inputs`/`outputs` when `package.json#lotics.agents.<alias>` declares them — through `set_app_agent`. The agent mirror of `app workflow set`, and the deploy-free authoring path for an agent's prose and its typed edges. **It sends only those fields.** Everything else is absent, and absent means unchanged, so a declaration this CLI does not model cannot be reverted by a push from a checkout that predates it — the chat authoring agent's `knowledge_doc_ids`, another operator's `query_aliases` grant. To change one of those, call `set_app_agent` with just that field (`lotics run set_app_agent '{"app_id":…,"alias":…,"tool_names":[…]}'` — it merges), then `app pull` to bring the manifest back in step. **CREATES the alias when the app has not bound one yet**, so a new agent is authored the same way a new workflow is: write the prose, declare the typed half, push. A create needs the prose file (an agent without instructions is not an agent); it is gated on nothing else, because what keeps a binding alive is a `useAppAgentRun("<alias>")` call site in the shipped bundle — a deploy prunes an agent the bundle never names, manifest entry or not. The prose push is a conditional write against the fingerprint this project last saw, so it is refused rather than allowed to overwrite prose someone else changed. Clear error + non-zero exit when there is no prose file and nothing declared to push instead, when a create has no prose to create from, or when the file is empty once the header is stripped. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version: an agent's alias and its declared `inputs`/`outputs` are in that contract too, so renaming one or changing what it accepts breaks a caller nobody here can redeploy. Without the flag such a write is refused and every breaking change is named (see `app api`). |
|
|
76
76
|
| `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy pushes a DRIFTED declaration through this same verb before it ships (see `app deploy`), so this is the explicit single-alias path, not the only way a query reaches the app. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. **The declaration's fields MERGE**, so the manifest is not a snapshot: deleting `params` from an alias and pushing leaves the live params exactly where they were, because an absent key means "unchanged". Clear one with `params: null`, or replace the map with the set you want. After the push it regenerates `.lotics/app_queries.d.ts` from the manifest, so the types the next `npm run typecheck` reads match what was just pushed. **`--acknowledge-breaking-api`** carries this write out even though it breaks what the app's published API promises, snapshotting the broken contract as a new version; without it such a write is refused and every breaking change is named (see `app api`). |
|
package/docs/migration.md
CHANGED
|
@@ -4,15 +4,30 @@ What an app author has to DO when a release changes the shape of a project the C
|
|
|
4
4
|
other contract is `docs/cli_reference.md`; this file is the one a reader opens once, because
|
|
5
5
|
something already on disk no longer matches what the CLI writes.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## A record's `sections`, `app.json` version 3
|
|
8
|
+
|
|
9
|
+
A record is its `sections`, stacked in the order the work reaches them, replacing `record.facts` and
|
|
10
|
+
`record.blocks`: each section a `title`, its `fields`, the `acts` under them and its `blocks`, and `at`
|
|
11
|
+
naming the status options it is the work of. The comment thread is `record.comments: true`, never a
|
|
12
|
+
block. `lotics scaffold check` refuses `facts`, `blocks` and a comments block by name.
|
|
13
|
+
|
|
14
|
+
1. **Restate each record.** Each fact group becomes a section with its title and fields; move each
|
|
15
|
+
block into the section it belongs to (a leg's fees into that leg's section, narrowed by `where`);
|
|
16
|
+
name in `acts` the acts whose inputs live there — an act no section names is a button in the header.
|
|
17
|
+
A staged section's acts are offered during its stage alone: an act offered at other stages stands
|
|
18
|
+
in their section, in a section with no `at`, or in the header.
|
|
19
|
+
2. **`lotics app regenerate`** in the app directory writes `app.json` version 3. `@lotics/app-runtime`
|
|
20
|
+
reads version 3 alone: a version 2 spec stops at its first render with `app.json is version 2; this
|
|
21
|
+
runtime reads version 3`.
|
|
22
|
+
|
|
23
|
+
## Apps built from a model: `records` and `apps`
|
|
8
24
|
|
|
9
25
|
A model states an app in one vocabulary: `records` says how a row of each entity is recognised (its
|
|
10
26
|
title, the line under it, its picture, its status, its figure, its deadlines), and each entry of
|
|
11
27
|
`apps` is one register over one entity and the record its rows open — its columns and filters, its
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
render with `app.json is version 1; this runtime reads version 2`.
|
|
28
|
+
sections, its acts and its checks (`lotics docs model/apps`). How each piece looks is the runtime's;
|
|
29
|
+
no key changes it. `lotics app create --from` compiles that into `app.json`, and an older spec stops
|
|
30
|
+
at its first render naming the version this runtime reads.
|
|
16
31
|
|
|
17
32
|
What a model no longer holds, each refused by name by `lotics scaffold check`:
|
|
18
33
|
|