@lotics/cli 0.315.18 → 0.316.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -24
- package/README.md +42 -37
- package/dist/src/cli.js +27346 -49103
- package/dist/src/client.d.ts +12 -88
- package/dist/src/client.js +10 -74
- package/dist/src/invocation.d.ts +1 -1
- package/docs/agent_surface.md +121 -114
- package/docs/app_architecture.md +39 -0
- package/docs/app_bindings.md +24 -62
- package/docs/building_an_app.md +97 -81
- package/docs/cli_reference.md +18 -19
- package/docs/data_model.md +129 -183
- package/docs/design.md +156 -170
- package/docs/document_templates.md +135 -99
- package/docs/examples.md +1599 -1238
- package/docs/field_values.md +14 -11
- package/docs/filters.md +47 -9
- package/docs/first_build.md +180 -0
- package/docs/getting_started.md +15 -0
- package/docs/knowledge_docs.md +73 -209
- package/docs/migration.md +22 -26
- package/docs/workflow_steps.md +22 -579
- package/docs/workflows.md +409 -209
- package/docs/working_in_a_workspace.md +60 -0
- package/package.json +1 -1
package/docs/cli_reference.md
CHANGED
|
@@ -4,9 +4,10 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
4
4
|
|
|
5
5
|
| Command | What it does |
|
|
6
6
|
|---|---|
|
|
7
|
-
| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (§ COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics
|
|
7
|
+
| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (§ COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics app --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |
|
|
8
8
|
| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |
|
|
9
9
|
| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open — `https://lotics.ai/cli_login/<request_id>`, also mailed — and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential — `docs` among them — claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |
|
|
10
|
+
| — | **A flag is refused on a verb that does not read it**, naming the verbs that do, before any request or credential: `--check` on `lotics run` would otherwise be parsed and ignored, and the write it was meant to stop would run. `--json`, `--workspace`, `--api-key` and `--timeout` are read by every verb. |
|
|
10
11
|
| `lotics auth api-key [key]` | `whoami` → **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |
|
|
11
12
|
| `lotics auth web` | Send a magic link email to access the web app (requires auth) |
|
|
12
13
|
| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key — read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |
|
|
@@ -27,33 +28,31 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
27
28
|
| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \| lotics run <tool> -`) — both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone — the error names both escapes. |
|
|
28
29
|
| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
|
|
29
30
|
| `lotics run <tool>` — **file cells** | A file in a tool's result carries its `fil_…` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file — this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |
|
|
30
|
-
| — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `
|
|
31
|
-
| — | **An app's queries and workflows take more than their params and inputs** (`lotics docs agent_surface`). `run_app_query '{"app_id","alias","params"}'` also takes `filters` and `sort` over the query's output columns, `fields` (the columns to return), `count` with `count_by` or `count_where`, `aggregate`, `limit` (200 rows at most; 50 where none is stated) and `cursor`, and `runs` — up to 20 reads in one call, each `{"alias", …}`, a refused one answered with its `error`; one call's rows total at most 262,144 bytes. `run_app_workflow '{"app_id","alias","inputs"}'` also takes `
|
|
32
|
-
| — | **The exit code reports the WORK, not just the call — for the
|
|
31
|
+
| — | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_query`) and every one that builds: `set_tables` (a list of table documents), `set_app` (one app), `set_app_queries`, `set_app_workflow`. A command exists only for work that touches a local file: `app apply`, `app create --custom`, `app pull`, `app deploy`. |
|
|
32
|
+
| — | **An app's queries and workflows take more than their params and inputs** (`lotics docs agent_surface`). `run_app_query '{"app_id","alias","params"}'` also takes `filters` and `sort` over the query's output columns, `fields` (the columns to return), `count` with `count_by` or `count_where`, `aggregate`, `limit` (200 rows at most; 50 where none is stated) and `cursor`, and `runs` — up to 20 reads in one call, each `{"alias", …}`, a refused one answered with its `error`; one call's rows total at most 262,144 bytes. `run_app_workflow '{"app_id","alias","inputs"}'` also takes `dry_run: true` — a rehearsal on the real inputs, nothing written or sent; the answer is what the run would return, with the reads it made under `dry_run.reads`, the calls it would make under `dry_run.writes` and the papers it rendered under `dry_run.papers` and the parts of the answer it could not say under `dry_run.unpredicted`; a workflow no dry run is offered of answers `status: "not_previewed"` — and `runs` — up to 50 runs in order, the first refused, failed or not previewed ending the call, an input `{"from_run": <index>, "path": "data.<key>"}` reading an earlier run's answer. |
|
|
33
|
+
| — | **The exit code reports the WORK, not just the call — for the tool that RUNS one.** `run_app_workflow` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`, or `not_previewed` — a dry run asked for and not given) exit non-zero and print `<tool> → <status>: <message>` to stderr, so `lotics run … && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE — an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |
|
|
33
34
|
| `lotics run <tool> '<json>' -o <dir>` | Also save every **signed link** the answer carries — an object with a `url`, a `filename` and the `expires_at` its signature ends at, at any depth, such as `screenshot_app`'s images. Each is fetched into `<dir>` (created if missing) under its `filename`, taking a free spelling beside a file of that name already there, and a link's `snapshot` text is written beside it as `<name>.txt`. The written paths follow the answer on **stdout**, one per line (on stderr under `--json`, whose stdout stays the one object). A link is signed for an hour; one the store no longer serves exits non-zero. A workspace file states no `expires_at` and is left to `lotics file download`. |
|
|
35
|
+
| `lotics run set_member '{"members": {"<email or member id>": {"role": "member", "name": "…"}}}'` | Invite, rename, remove (`null`) or restore members, admin only: the whole map is checked before an email is sent. A member's `lotics tools` does not list it, nor `set_group` or `set_workspace`. View-as is the web app's preview alone; the CLI acts as the signed-in member. |
|
|
34
36
|
| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |
|
|
35
37
|
| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes — harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |
|
|
36
38
|
| `lotics file upload <file\|dir...>` (alias `lotics upload`) · `--stdin` · `--base64` · `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files`, several such files to a request up to half that route's 100 MiB cap; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose — one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** — an attachment decoded in memory, a generated document, a signed download link — each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY — `Buffer.from(s, "base64")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |
|
|
37
|
-
| `lotics file download <file_id> [<path>]` · `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there — the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once — a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <
|
|
39
|
+
| `lotics file download <file_id> [<path>]` · `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there — the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once — a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_alias> [-o <dir>]` (the field named by the alias `get_record` shows it under) spreads every file on a record's file field over a DIRECTORY — there is no single file for N files to be. |
|
|
38
40
|
| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first — id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset — pass it back as given — so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to "what is in here" short of reading Postgres. |
|
|
39
41
|
| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. Deleting an original deletes every version of it; a file's current version cannot be deleted on its own, and a document template's file, or a version of it, only by a member who manages the template. **Refused while a record cell, a comment, a knowledge doc, a document template, a voice session or a pending change request still references the file or a version it takes along** — the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what "deleted" means here. There is no `lotics delete`: the verb needs its noun. |
|
|
40
|
-
| `lotics knowledge list [--include-hidden \| --deleted]` | `GET /v1/knowledge_docs` — a table of id, name, tags, description (`--json` for the docs). **REST, not
|
|
41
|
-
| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \| --content <str>)` | Read the body client-side (a file XOR an inline string — exactly one required), then call `
|
|
42
|
-
| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) → the doc with its **hydrated `content`** (the one content
|
|
43
|
-
| `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `
|
|
42
|
+
| `lotics knowledge list [--include-hidden \| --deleted]` | `GET /v1/knowledge_docs` — a table of id, name, tags, description (`--json` for the docs). **REST, not `get_workspace` kind `knowledge`**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. **`--deleted` is the one tool-backed form**: it calls `get_workspace` with `kind: "knowledge"` and `include_archived: true`, page by page, and prints the deleted docs you can manage (id, name, deleted time, description); it cannot be combined with `--include-hidden`. |
|
|
43
|
+
| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \| --content <str>)` | Read the body client-side (a file XOR an inline string — exactly one required), then call `set_knowledge` with `{ name, description, content, tags? }` and no `knowledge_doc_id`, which makes a doc (description defaults to `""`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |
|
|
44
|
+
| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) → the doc with its **hydrated `content`** (the one whole-content read for a non-sandbox client; the tool `read_knowledge` reads a line range). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |
|
|
45
|
+
| `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `set_knowledge` with a `knowledge_doc_id` and **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set — the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |
|
|
44
46
|
| `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
|
-
| `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,
|
|
47
|
+
| `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, and the corpus `get_workspace` lists and its `search` 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
48
|
| `lotics knowledge rm <id...>` | Delete docs via `delete_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a live doc you manage that no app or workflow names, or nothing is deleted. Sharing and history are kept. The REST execute path does not gate `needsApproval`, so this runs unattended. |
|
|
47
49
|
| `lotics knowledge restore <id...>` | Bring deleted docs back via `restore_knowledge` (`{ knowledge_doc_ids }`), in one call: every id must be a deleted doc you manage, or nothing is restored. Refuses the no-argument form. |
|
|
48
|
-
| `lotics
|
|
49
|
-
| `lotics
|
|
50
|
-
| `lotics
|
|
51
|
-
| `lotics
|
|
52
|
-
| `lotics
|
|
53
|
-
| `lotics app pull [app_id] [path]` | **A custom-code app's live source, as a project ready to deploy on it.** The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** — but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`get_app_types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). A JSON app has no source tree: the server's refusal names `get_model`, and `lotics model pull` is its pull. |
|
|
54
|
-
| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent; a type error in the app's own files stops the deploy, one inside an installed package is reported and does not) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged — those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |
|
|
55
|
-
| `lotics docs` \| `lotics docs <area>[/<section>]` \| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** — the model reference, this index, and every doc under `docs/` — so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal — a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`, with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` — the dialect `grep_knowledge` reads — each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |
|
|
50
|
+
| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty — before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`GET /v1/apps/{id}/types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: a JSON app is made by `lotics app apply` (the tool `set_app`). The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |
|
|
51
|
+
| `lotics app pull [app_id] [path]` | **An app's live definition, pulled.** The app is read with `get_app`: its author is shown a body, a JSON **tree** (with the addresses of its nodes) pulled as that tree and a custom-code app (the identity alone) as its source; an app whose stored tree cannot be read is refused with the reason `get_app` gives, and so is an app that shows the caller no body (the caller does not author it, or it has no version). **A JSON app**: the tree is written as JSON to `[path]` (a directory receives `./<name>.app.json`; default `./<name>.app.json`), and the app's id and current version beside it in `<file>.lotics.json` (`{ "app_id": "…", "version_id": "…", "digest": "…" }`, the digest being the file as the CLI last left it; a record without all three is refused by `app apply`, naming `lotics app pull`) — a custom-code project records both in `package.json#lotics`, a single file has no such place — so the next `lotics app apply <file>` edits that app, and is refused if the app has since moved past that version. An existing file is overwritten only when its `.lotics.json` names this same app and the file is unchanged since it was last pulled or applied; a file with edits, or whose record names the app but no digest, is refused, naming `--force`, which overwrites it. `--force` is a flag of `app pull` alone. `--json` prints `{workspace_id, app_id, path, outcome}`. **A custom-code app**: its live source, as a project ready to deploy on it. The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** — but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`GET /v1/apps/{id}/types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). |
|
|
52
|
+
| `lotics app apply <file> [--app <app_id>] [--check] [--json]` | **A JSON app, one at a time, through the tool `set_app`.** The file is an app's tree (`lotics docs model`: the App node with the nodes under it, its tables and templates named by alias). **No app known** (no `--app`, no `<file>.lotics.json` beside it): the file is sent as `{ "body": … }` to make a new app, named by the body's `name`, and the id made is printed, then recorded with its version in `<file>.lotics.json`. **An app known**: its current tree is read (`get_app`) and the **RFC 7396 merge patch** that turns it into the file is sent as `body` with the `app_id` and the version the patch was computed against as `base_version_id` — a key the file drops is `null` in the patch, a list is replaced whole, what the file leaves alone is left alone, and a tree the app already has sends nothing and says so. **A file pulled at a version the app has since left is refused** before anything is sent, naming both versions and `lotics app pull <app_id> <path>`: pull the current tree into another path, carry the edits over, apply that file. `--app` naming an app other than the sibling file's is refused too. A write that races the apply is refused by the server, which names the app's current version. A file that sets an object key to `null` is refused naming the key, since a merge patch reads `null` as a removal. `--app` names the app, else the sibling file does; an app whose `get_app` shows no body (it is not yours) or whose stored tree cannot be read is refused, and a custom-code app takes its identity (name, description, icon, colour) only, and no `<file>.lotics.json` records it, since an identity write makes no version — its source deploys with `app deploy`. **The file is never rewritten**: a node the tree leaves out draws as the rows call for it and is stored nowhere (`lotics docs design`). It prints what `set_app` answered and reports its `outcome`: its notes and findings, every finding at once, and the saves, adds and Deletes the write grants and removes. **`--check` sends `dry_run`** (a flag of `app apply` alone, as is `--app`) — what would refuse the tree, and what it would grant and remove — and writes nothing, the file and its sibling included; an `outcome` of `refused` exits 1. A tree the app already has, or one `set_app` answers `unchanged`, records the version the app stays at in the sibling. **To go back to an earlier version, apply the earlier tree again.** Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, app_id, outcome, write}`, `outcome` being `set_app`'s: `created`, `updated`, `unchanged` or `refused` (with `--check`, what it would be). |
|
|
53
|
+
| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`GET /v1/apps/{id}/types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent; a type error in the app's own files stops the deploy, one inside an installed package is reported and does not) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged — those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |
|
|
54
|
+
| `lotics docs` \| `lotics docs <area>[/<section>]` \| `lotics docs [<area>[/<section>]] --grep <text>` | **This CLI's own references, carried inside the binary** — the model reference, this index, and every doc under `docs/` — so the doc a reader opens describes the binary answering, listed by the job a reader comes to do. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. **A reference the binary's copy lacks, or a part of one the server serves, is read from the server's `docs` tool** with this machine's credential, after the copy's refusal — a page the server added since this binary was built, which a server refusal can cite. A name matching more than one reference, or a part missing from a guide to this CLI, is answered by the copy alone. **`--grep` searches the references the server serves** (not this CLI's own guides, which it refuses), with this machine's credential, narrowed to the area or section named: literal text unless `--regex`; a literal search that finds nothing while the pattern read as an expression matches (`a|b`) returns those lines under a note saying so, and `--literal` keeps the literal zero; with `--case-sensitive`, `--diacritic-insensitive`, `--context-lines <n>` and `--limit <n>` — the dialect the knowledge lines of `get_workspace` with `search` are read in — each hit under the address that opens its page; any of those options without `--grep` is refused. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |
|
|
56
55
|
| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script — the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe — a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |
|
|
57
|
-
| `lotics docs model` \| `lotics docs model/<section>[/…]` | **The
|
|
56
|
+
| `lotics docs model` \| `lotics docs model/<section>[/…]` | **The reference for tables and apps, from inside the binary** — at this CLI's version; `lotics docs` lists it under "Build an app", after `design`. Its first page is what the documents compose with, the working order (jobs → tables and fields → `records` → one app per job → one `set_tables` call with every table, then every `set_app` call) and the section addresses; every page of it is whole. Every key of a table document and an app body, every field `type` with the config each one needs, the option / view / inline-template shapes, `records` (how a row of a table is recognised), `write_rules`, the apps (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `"<table-ref>:<row-ref>"`), the rules, the caps of one `set_tables` call, and a worked example; it points at `https://lotics.ai/presets/index.json` for example files of several trades, which an agent reads and passes to `set_tables`. **Offline, no account.** |
|
|
58
57
|
| `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. |
|
|
59
58
|
|
package/docs/data_model.md
CHANGED
|
@@ -10,8 +10,8 @@ under **Fields**, what each field type takes.
|
|
|
10
10
|
|
|
11
11
|
## One fact, one column
|
|
12
12
|
|
|
13
|
-
**Read the table's existing fields first, and add nothing that stores a fact the table already
|
|
14
|
-
stores.** A value written as text and the same value held as a `
|
|
13
|
+
**Read the table's existing fields first (`get_table`), and add nothing that stores a fact the table already
|
|
14
|
+
stores.** A value written as text and the same value held as a `link` are one fact in
|
|
15
15
|
two columns — a customer's city typed into a text box beside a link to the city record, a status
|
|
16
16
|
word beside the select that decides it, a total beside the formula that computes it.
|
|
17
17
|
|
|
@@ -28,6 +28,8 @@ it runs.
|
|
|
28
28
|
description that says which one is real.
|
|
29
29
|
- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a
|
|
30
30
|
real distinction — read what it MEANS before removing it.
|
|
31
|
+
- **A stored field becomes required only once every row holds a value**, so it always holds one: fill the
|
|
32
|
+
empty cells first, then set it. A computed field's flag is checked as each row is written.
|
|
31
33
|
|
|
32
34
|
## One entity, one table — and the test is measurable
|
|
33
35
|
|
|
@@ -43,22 +45,19 @@ on suspicion is a large repoint bought for nothing. So do not argue it in the ab
|
|
|
43
45
|
None means the split is holding. One means it has broken — the usual cause is a party you begin to
|
|
44
46
|
invoice as well as buy from — and the fix is to merge before a second screen depends on the copy.
|
|
45
47
|
|
|
46
|
-
Worth writing as a test rather than a note, because a note about a condition nobody re-checks goes
|
|
47
|
-
stale in silence.
|
|
48
|
-
|
|
49
48
|
## One vocabulary wherever values are COPIED between tables
|
|
50
49
|
|
|
51
|
-
Two `select` fields for one concept carry
|
|
52
|
-
|
|
50
|
+
Two `select` fields for one concept carry different option keys even when their labels match. Copy an option by its
|
|
51
|
+
alias or label: the record tools match an option by alias or label, so a value copied by alias lands
|
|
52
|
+
on the option of that alias on the other field. Prefer a link to a shared reference table where the set is open or
|
|
53
|
+
growing. Where one side holds more values than the other, the wider side must **refuse** what the narrower one cannot
|
|
54
|
+
express rather than quietly picking the nearest value.
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
open or growing; keep a map only for a small closed set.
|
|
56
|
+
A value read through a link or a lookup reads exactly as the same field would on its own row: an option written by its alias in
|
|
57
|
+
a body and read as its key, a member or a link by its id. A value a check matches
|
|
58
|
+
across tables is a link to one table: two selects never compare, and a save that compares them is refused.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
telling the truth. The wider side must **refuse** what the narrower one cannot express rather than
|
|
61
|
-
quietly picking the nearest value.
|
|
60
|
+
A lookup reads as one flat list of the values it looks up, in a check and in every body: `first(record.x)` is the one value through a one-row link and `includes(record.x, v)` asks whether any row holds `v`; two selects never compare, so a value matched across tables is a link to one table. A single member, a single select, a number or a text reads as the value itself, and `first(...)` of one is refused when the body is saved. A rollup that adds up (a sum, a count) over no rows is 0, never empty, as `sum` over an empty list is; a min, a max or an average over no rows is empty.
|
|
62
61
|
|
|
63
62
|
## A copy boundary accounts for EVERY source field
|
|
64
63
|
|
|
@@ -80,53 +79,47 @@ row beside it.
|
|
|
80
79
|
|
|
81
80
|
## Say what makes two rows the SAME row
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Anything that imports, reconciles or de-duplicates has to decide identity, and with no declared key
|
|
86
|
-
it falls back to comparing displayed text — which is how one company arrives three times under three
|
|
87
|
-
spellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_…`
|
|
88
|
-
and `opt_…`, never on rendered labels.
|
|
89
|
-
|
|
90
|
-
## A state's HISTORY is rows, not columns
|
|
82
|
+
State it as the table's `natural_key` (`model/write-rules`); a text key is also `unique`.
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Measuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,
|
|
96
|
-
the state entered, when. Hold those states as the source field's own `opt_` keys so the log carries
|
|
97
|
-
no second vocabulary, and write the row in the workflow that makes the move.
|
|
98
|
-
|
|
99
|
-
## Keep derived chains shallow
|
|
84
|
+
Anything that imports, reconciles or de-duplicates has to decide identity, and with no declared key it falls back to
|
|
85
|
+
comparing displayed text — which is how one company arrives three times under three spellings. Name the key: a reference
|
|
86
|
+
number, a tax id, a link plus a period. Then match on record ids and option aliases, never on rendered labels.
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
recomputes with it. A rollup over a formula over a formula is paid three times on every touch, and
|
|
103
|
-
again for every row upstream of it.
|
|
88
|
+
## A stage's moment is a date on the row
|
|
104
89
|
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
Who changed what, and when, is the platform's audit trail, which an agent already reads (`query_audit_logs`) under
|
|
91
|
+
its own access to the table — so it is never a table of yours, and an app shows none of it: a history an app shows is
|
|
92
|
+
table-backed (`records.status.dates`, `records.status.history`, an event table an act makes).
|
|
107
93
|
|
|
108
|
-
|
|
94
|
+
When the business reports, filters or prints the moment a row reached a stage (completed on, cancelled on), that
|
|
95
|
+
moment is a `date` field of the row, as `completed_at` and `canceled_at` are in SQL. State it as `records.status.dates`
|
|
96
|
+
(`model/status`): option → date. The record write path stamps it as a row is made at the option or moved into it,
|
|
97
|
+
whoever writes, so no one has to remember; a value the same write states wins, which loads past data as it happened. The
|
|
98
|
+
platform never clears a stamp: each date is when the row last entered the option, and leaving keeps it, and putting a
|
|
99
|
+
change back restamps nothing. A date range is a span, never a moment, so it is refused. It is the one way a status is
|
|
100
|
+
timed: a stage's `within` runs from its date, a straight-line process is one select whose options are its steps, each
|
|
101
|
+
dated, and the stage is never read off a run of dates.
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
the
|
|
103
|
+
`records.status.history` is for a business that reports on every move — moves per person, or time in a stage a row
|
|
104
|
+
re-enters — and the platform appends one row per move. An event that carries facts of its own (a reason, an amount, a
|
|
105
|
+
signature, an inspection's findings) is its own table with those fields, made by the act that records it.
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
2. Make the change.
|
|
115
|
-
3. Diff. Identical is the pass.
|
|
107
|
+
## Keep derived chains shallow
|
|
116
108
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
Formulas and rollups are computed and STORED when a row is written, and one that reads another recomputes with it. A
|
|
110
|
+
rollup over a formula over a formula is paid three times on every touch, and again for every row upstream of it.
|
|
111
|
+
**Depth costs more than row count**: chain depth is yours to keep down, row scanning is the platform's.
|
|
120
112
|
|
|
121
113
|
## Fields
|
|
122
114
|
|
|
123
|
-
|
|
115
|
+
A field is stated inside its table's document in a `set_tables` call, keyed by alias. Every field `type`, the
|
|
116
|
+
keys each one takes (notation, defaults, options, links, formulas, rollups, lookups, copies) and a worked
|
|
117
|
+
example are in `docs path="model/field"`; this page holds the choices around them.
|
|
124
118
|
|
|
125
119
|
### Types and formats
|
|
126
120
|
|
|
127
|
-
`type` is one of `text`, `number`, `date`, `boolean`, `select`, `
|
|
128
|
-
`
|
|
129
|
-
existing button field keeps running, but none is created, converted to or edited.
|
|
121
|
+
`type` is one of `text`, `number`, `date`, `boolean`, `select`, `member`,
|
|
122
|
+
`link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`.
|
|
130
123
|
|
|
131
124
|
A URL, an email, markdown, a checkbox, a datetime, money, a quantity or a percentage is a `format`
|
|
132
125
|
or a `notation` on another type, never a `type`:
|
|
@@ -143,142 +136,95 @@ or a `notation` on another type, never a `type`:
|
|
|
143
136
|
| Datetime | `{ type: "date", format: "datetime" }` |
|
|
144
137
|
| Date range | `{ type: "date", format: "date_range" }` |
|
|
145
138
|
|
|
146
|
-
###
|
|
147
|
-
|
|
148
|
-
A type's properties sit **directly on the field object** — there is no `config` wrapper. A property
|
|
149
|
-
may itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay
|
|
150
|
-
inside it. Each type takes only its own:
|
|
151
|
-
|
|
152
|
-
- **text** — `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).
|
|
153
|
-
- **number** — `notation?`, `default_value?` (a number). `notation` is how the figure reads, one
|
|
154
|
-
of three; on `update_fields` a stated one replaces the field's whole.
|
|
155
|
-
- `{ style: "decimal" }` — a plain number, as a field stating none reads.
|
|
156
|
-
- `{ style: "currency", currency: "VND" }` — money in an ISO 4217 code.
|
|
157
|
-
- `{ style: "unit", unit }` — a quantity: `"percent"` (10 reads "10%", never multiplied), a
|
|
158
|
-
measured code — g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day — or a counted noun such
|
|
159
|
-
as `kiện`. A change between two units of one dimension converts every stored figure; any other
|
|
160
|
-
change relabels.
|
|
161
|
-
- A currency or unit read on each row is `{ per_row: "ĐVT" }` in its place: a single select on
|
|
162
|
-
the same row, or a lookup of one through a one-link, named by name or key, whose chosen option
|
|
163
|
-
is that row's currency or unit — every option label an ISO 4217 code, or a unit as `unit`
|
|
164
|
-
takes one.
|
|
165
|
-
- **date** — `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,
|
|
166
|
-
`derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row's
|
|
167
|
-
creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no
|
|
168
|
-
`default_value`, and holds only the `date` and `datetime` formats.
|
|
169
|
-
- **boolean** — `default_value?` (`true` | `false`).
|
|
170
|
-
- **select** — `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`. An option stated without a `color` gets one no sibling option holds.
|
|
171
|
-
- **select_member** — `multi?`, `default_value?` (member ids).
|
|
172
|
-
- **select_record_link** — `table_id`, `sync_both_ways?`, `cardinality?`, `paired_field_name?`. A
|
|
173
|
-
link shows the row it names by the one field its table names its rows by — the table's
|
|
174
|
-
`title_field_key`, stated when the table is made or changed, or by `records.<entity>.title` in the
|
|
175
|
-
model; a link states no display fields of its own, and a combined name is a formula field named there.
|
|
176
|
-
`sync_both_ways: true` on an existing one-way link makes it two-way — never add a second link for
|
|
177
|
-
that. `cardinality: "one"` holds a single row, paired or not; a paired partner of a `"one"` side
|
|
178
|
-
lists that side's rows as its children. Two paired sides cannot both be `"one"`. `paired_field_name`
|
|
179
|
-
names the back-reference the target gets.
|
|
180
|
-
- **files** — no properties.
|
|
181
|
-
- **autonumber** — `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,
|
|
182
|
-
`{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1–20) for `PREFIX-0001`.
|
|
183
|
-
- **any type**, at create — `confirm_before_update?`: a person confirms each later edit.
|
|
184
|
-
|
|
185
|
-
`default_value` fills the field on a NEW record that states no value; existing records are never
|
|
186
|
-
backfilled, and null clears it. A select's or a member field's default is an ARRAY even when the
|
|
187
|
-
field holds one (`["Unread"]`, never `"Unread"`) — option names in `add_fields`, where the keys do
|
|
188
|
-
not exist yet, and option keys (`opt_…`) in `update_fields`.
|
|
189
|
-
|
|
190
|
-
`default_from` (text, number, date, boolean, select, select_member, select_record_link) copies a
|
|
191
|
-
value onto a NEW record off the row one of its links names: `{ link_field_key, field_key,
|
|
192
|
-
options? }`, the link of cardinality `"one"` and not itself copied, the source a field of the linked
|
|
193
|
-
table of this field's own type, date format and notation (a link one to the same table) — a formula,
|
|
194
|
-
a rollup, or a lookup through a one-row link (or an ordered one) read as the value it holds; several
|
|
195
|
-
values only into a field holding several; never into a date `derive_from` stamps. It is
|
|
196
|
-
copied when the record is made and never re-read, so a later change to the source leaves the record
|
|
197
|
-
alone (a line's price as it was when the line was booked). A value the write states wins, then the
|
|
198
|
-
copy, then `default_value`. A select source's `options` maps its option keys onto this field's
|
|
199
|
-
(`update_fields`, once the keys exist); an option left unmapped is not copied. null clears it. Deleting the link a
|
|
200
|
-
copy reads through, or the field it copies off, is refused even with `force` until the copy is
|
|
201
|
-
cleared; the dependency report lists the copying field as `copy_link` or `copy_source` under
|
|
202
|
-
`requires_manual_action`. Any other schema change that leaves a copy unable to stand — its source
|
|
203
|
-
retyped, its link made to hold many — is refused, naming the copy.
|
|
204
|
-
|
|
205
|
-
### Computed fields
|
|
206
|
-
|
|
207
|
-
Read-only in records. `formula` is one key holding an object; a rollup's and a lookup's properties
|
|
208
|
-
are separate keys on the field — `rollup: {…}` and `lookup: {…}` are refused as unrecognized.
|
|
209
|
-
|
|
210
|
-
formula: { expression, notation?, link? }
|
|
211
|
-
|
|
212
|
-
- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never
|
|
213
|
-
breaks the formula. A reference to no field, or a call to something that is no helper, is refused.
|
|
214
|
-
- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,
|
|
215
|
-
`includes({Tags}, "opt_…")` for a multi.
|
|
216
|
-
- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` — a model
|
|
217
|
-
names fields by alias where a table tool names them by name or key; the language is the same.
|
|
218
|
-
- `notation` is how a number result reads, as a number field's; `link: true` says the result is a
|
|
219
|
-
URL, drawn as a link that opens.
|
|
220
|
-
- On `update_fields` a key left out keeps its stored value, and a stated `notation` replaces the
|
|
221
|
-
stored one whole. `get_table` marks a formula that is null when every field it reads is empty.
|
|
222
|
-
|
|
223
|
-
rollup — `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`
|
|
224
|
-
|
|
225
|
-
- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a
|
|
226
|
-
field of the linked table, required by every operation but `count`, which counts linked records.
|
|
227
|
-
- Operations by the aggregated field's type — none: `count`; any: `empty`, `filled`,
|
|
228
|
-
`percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,
|
|
229
|
-
`min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.
|
|
230
|
-
- The cell's type comes from the OPERATION: `earliest` and `latest` hold a date, every other
|
|
231
|
-
operation a number (`date_range` counts days). A "most recent linked date" is `latest` — `min` and
|
|
232
|
-
`max` are numeric and refuse a date. A rollup takes no notation of its own: `sum` over money
|
|
233
|
-
reads in the aggregated field's currency, over a weight in its unit, a count as a plain number
|
|
234
|
-
and a `percent_*` in the unit `percent`.
|
|
235
|
-
- Over a figure read in each row's own unit or currency (a notation's `per_row`), `sum`,
|
|
236
|
-
`avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the
|
|
237
|
-
link paired with `source_field_key`, of a single select on this table — the total reads in this
|
|
238
|
-
row's option of it. A lookup of such a field stands only where its own table also looks up that select through the same one-link, and reads in that lookup: a line looks up the item's unit through its link to the item, then may look up the item's stock through the same link.
|
|
239
|
-
- `filter` aggregates only the linked records it matches: a full group over the LINKED table's
|
|
240
|
-
fields — `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,
|
|
241
|
-
operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.
|
|
242
|
-
|
|
243
|
-
lookup — `source_field_key`, `lookup_field_key`, `order_by?`
|
|
244
|
-
|
|
245
|
-
- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked
|
|
246
|
-
record's value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds
|
|
247
|
-
the picked field of the single extreme record — a "latest linked X" that maintains itself.
|
|
248
|
-
Ordered lookups sharing one `order_by` resolve to the SAME record.
|
|
249
|
-
|
|
250
|
-
### Examples
|
|
139
|
+
### Changing a field
|
|
251
140
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
141
|
+
Read the table with `get_table`, which answers its document exactly as `set_tables` takes it, then send `set_tables` with
|
|
142
|
+
the `table_id` and only what changes. The table is edited by an RFC 7396 merge patch of its document, so what the patch
|
|
143
|
+
leaves alone is left alone and `null` deletes the field it names. Every key a table and a field take is in
|
|
144
|
+
`docs path="model/tables"` and `model/field`.
|
|
145
|
+
|
|
146
|
+
A table is named by its alias wherever a call names one (`table_id`, a link's `table`, a lookup, `records.history`), and a new table in the call is named by its `ref`, which becomes its alias. The alias is kept while the table exists, archived or not: asking for one an archived table holds is refused, offering to restore that table or to choose another alias. A filter in a table document (a rollup's `filter`, `options_where`, a colour's condition, a check's lookup) takes the record tools' operators and values (`is`, `is_any_of`, `gte`, a plain date as a day). An `autonumber` field takes `next` (`"next": 232`): the number its next row gets, refused below the highest already issued.
|
|
147
|
+
|
|
148
|
+
- **A list is replaced whole**, so an option cannot be nulled. To rename, recolour, add or remove an option, restate the
|
|
149
|
+
field's whole `options` list, each option with its alias (`model/field/select`). A rename changes the label and every
|
|
150
|
+
record keeps its value; an alias left out is removed, which needs `force` while rows hold it.
|
|
151
|
+
- **A number field becomes an `autonumber`** when every row holds its own whole number of 1 or more: the numbers are kept
|
|
152
|
+
and the next row takes the one after the highest (a `next` must be above it). Otherwise the call is refused, saying how
|
|
153
|
+
many rows are blank, repeated or not whole, with up to 5 row ids. No other type converts to or from an autonumber; add
|
|
154
|
+
one as a new field, which numbers the rows already there from 1 (or from `next`) in creation order.
|
|
155
|
+
- **Splitting an option is a rename plus an add.** The old option keeps its alias and takes the first part's label, so its
|
|
156
|
+
rows and every app reference to it keep working; the second part is a new option. Rows that belong to the second part
|
|
157
|
+
are then moved by a filtered `update_records`.
|
|
158
|
+
- **A status history holds every option its status ever had** and follows them in the same write: an option added or renamed
|
|
159
|
+
on the status is added or renamed on the history, which a call never restates. Removing an option on the status leaves it
|
|
160
|
+
on the history, because the moves already written hold it. A call that changes nothing of the status's options leaves the
|
|
161
|
+
history alone, and options only the history holds stay. Deleting a row deletes its history rows with it, and restoring
|
|
162
|
+
the row restores them; they never hold a delete back.
|
|
163
|
+
- A link names its table by id, or by the `ref` of another table of the same call, and states its `multi`. A two-way link
|
|
164
|
+
states `pair` on one table, as `true` or as a free alias, and the platform makes the partner on the other table with the
|
|
165
|
+
inverse cardinality; a many-to-many link states both sides, since the inverse of many is not decided.
|
|
166
|
+
A link already holding rows is paired the same way, with its partner stated beside it in the call: the partner is filled from
|
|
167
|
+
the links the rows hold and no cell of the link is rewritten. `pair: null` lets a pair go, both fields staying one-way links
|
|
168
|
+
with their values; a field that holds one row, the partner or the link itself, refuses a pair two rows would crowd, naming the rows.
|
|
169
|
+
The partner may stand on a table the call does not name: `pair: "<alias>"` makes it there, or pairs the one-way link that table
|
|
170
|
+
already holds under that alias, and the answer lists that table among the call's. Only someone who manages that table may.
|
|
171
|
+
- A formula's `output_type` is inferred from its expression; state it only to override.
|
|
172
|
+
- **A call's removals are judged together, on what the call leaves.** A table and the links and lookups naming it, removed in one call, pass; every reader left outside the call is named in one answer, not one per retry. An app reading a field or an option that leaves is rebuilt in the same call, and the answer (a dry run too) lists each node of its tree that changed: a column, a filter, a sort key or a field of a part that only drew the field is dropped, and an option leaves every list of options it stood in. What has no meaning without it refuses the call, naming the node: an act that sets a removed option, a part left with nothing to draw, a list that held only the removed option. An app drawing a field that is retyped, or a link pointed at another table, is rebuilt too, its tree unchanged: where the field no longer lets the tree draw what it drew, the call refuses, naming the node, and a field that became a formula, rollup, lookup or autonumber leaves the app's allowlist of saves. A workflow body its author wrote is never rewritten: it is checked again against the tables as the call leaves them, and the call refuses, naming the workflow, only if it no longer checks. A table's check or workflow that reads a retyped field or a link pointed elsewhere is judged the same way and refuses, naming it, only if it no longer holds. Deleting one field through the table's field route drops no node: an app whose tree draws the field refuses it, naming the node.
|
|
173
|
+
- **A field something reads cannot go.** A query, a workflow, a check, an app only its owner can rebuild, or another
|
|
174
|
+
table's formula, rollup, lookup or copy that reads the field refuses its deletion, naming the reader. Change or remove
|
|
175
|
+
the reader first, or in the same call.
|
|
176
|
+
- **`force` consents to losing values:** deleting a field rows hold, removing an option rows hold, and a change of type
|
|
177
|
+
that does not convert every value.
|
|
178
|
+
- A money formula is proven by its numbers. `aggregate_records` the affected totals before the change and after, and
|
|
179
|
+
compare. A new field in a formula is tested for the value you want, never for differing from an empty cell, which reads
|
|
180
|
+
`""` and differs from every option.
|
|
181
|
+
- A check's expressions are written in the language of `workflows/table-checks`.
|
|
182
|
+
- `views` are stated when the table is created; afterwards they change in chat.
|
|
183
|
+
- A refused edit writes nothing and lists everything wrong; `dry_run` is for reading the grants it would make or deciding `force`.
|
|
184
|
+
|
|
185
|
+
A worked call: add the option `cancelled` to `stage` (the whole list restated), add a check, and consent to losing
|
|
186
|
+
what rows hold.
|
|
187
|
+
|
|
188
|
+
```json5
|
|
189
|
+
{
|
|
190
|
+
"tables": [
|
|
191
|
+
{
|
|
192
|
+
"table_id": "<id of the table>",
|
|
193
|
+
"fields": {
|
|
194
|
+
"stage": {
|
|
195
|
+
"options": [
|
|
196
|
+
{ "alias": "draft", "label": "Draft" },
|
|
197
|
+
{ "alias": "confirmed", "label": "Confirmed" },
|
|
198
|
+
{ "alias": "cancelled", "label": "Cancelled", "color": "red" }
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"checks": {
|
|
203
|
+
"returns_after_leaves": {
|
|
204
|
+
"on": ["create", "update"],
|
|
205
|
+
"when": "!isNull(record.returns)",
|
|
206
|
+
"fail_when": "record.returns < record.leaves",
|
|
207
|
+
"message": "\"Return before departure\"",
|
|
208
|
+
"field": "returns"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
],
|
|
213
|
+
"force": true,
|
|
214
|
+
"dry_run": true
|
|
215
|
+
}
|
|
264
216
|
```
|
|
265
217
|
|
|
266
|
-
|
|
218
|
+
Splitting `queued` into waiting and underway is one call of the same shape:
|
|
219
|
+
|
|
220
|
+
```json5
|
|
221
|
+
{ "tables": [{ "table_id": "<id of the table>", "fields": { "stage": { "options": [
|
|
222
|
+
{ "alias": "draft", "label": "Draft" },
|
|
223
|
+
{ "alias": "queued", "label": "Waiting" },
|
|
224
|
+
{ "alias": "underway", "label": "Underway", "color": "green" }
|
|
225
|
+
] } } }] }
|
|
226
|
+
```
|
|
267
227
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
An `update_fields` entry is `{ field_key, name?, description?, convert_to?, … }` with the same flat
|
|
272
|
-
properties as `add_fields`, plus what only an update does:
|
|
273
|
-
|
|
274
|
-
- A select's options change through `add_options` `[{ name, color?, mark? }]`, `update_options`
|
|
275
|
-
`[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing
|
|
276
|
-
option once, in order; options added in the same call follow). An option is named by its `opt_…`
|
|
277
|
-
key (its name also resolves); a rename keeps every record's value. `mark` is the option's brand
|
|
278
|
-
(`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in
|
|
279
|
-
place of its colour dot — every option of a field has one or none does, so marking sets them all
|
|
280
|
-
in one call and `mark: null` on each clears them. `distinct_option_colors: true` re-colours a
|
|
281
|
-
select so every option holds its own: the first option of each colour keeps it, the rest take
|
|
282
|
-
colours no option holds.
|
|
283
|
-
- A link's `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its
|
|
284
|
-
record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.
|
|
228
|
+
`table_id` picks the table patched; `fields` merges by alias, so only `stage` changes; `checks` adds one under its alias
|
|
229
|
+
(`null` removes it); `force` consents to losing values (leave it out unless rows hold a removed option); `dry_run`
|
|
230
|
+
checks and writes nothing, for reading the grants or deciding `force` before the call that writes.
|